task-179: 禁止记录清单审计(语句级扫描 src/main log.*,Token/Cookie/完整body/二进制/凭据/APIKey 白名单校验)+ 8 条测试
- 多行感知扫描:body={}/authorization/Bearer/token 语句必须过 maskChatBody/maskSecretsForLog/maskForLog/toJsonForLog/abbreviate 等脱敏,否则判违规
- Cookie/凭据/API Key/二进制类字段一律不允许出现;LLM 脱敏后的 body 诊断保留(allowed_fields_kept)
- 审计结果:现网 LLM 客户端日志均已脱敏,零裸漏,无需修正;纯扫描可重复
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
package com.nanri.aiimage.config;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* task-179:禁止记录清单审计(spec 11 §2:Token/Cookie/完整请求体/文件二进制/凭据/敏感 API Key
|
||||
* 不入日志;允许脱敏/截断后的字段保留)。
|
||||
*
|
||||
* 语句级扫描 src/main 的全部 log.* 调用(含跨行参数):凡 format 会出现完整敏感值的高风险
|
||||
* 模式做白名单校验——body={} 或 authorization/Bearer/token 的语句必须经脱敏/截断 helper
|
||||
* (maskChatBody/maskSecretsForLog/maskForLog/toJsonForLog/abbreviate 等);Cookie/凭据/
|
||||
* API Key/二进制类字段一律不允许出现。纯扫描可重复,违规即列出 file:line。
|
||||
*/
|
||||
class ForbiddenLogAuditTest {
|
||||
|
||||
private static final List<String> MASK_HELPERS = List.of(
|
||||
"maskChatBody", "maskSecretsForLog", "maskForLog", "toJsonForLog",
|
||||
"abbreviate", "mask(", "redact");
|
||||
|
||||
private static final Pattern LOG_CALL = Pattern.compile("log\\.(info|warn|error|debug|trace)\\s*\\(");
|
||||
|
||||
private static final Pattern TOKEN_AUTH = Pattern.compile(
|
||||
"\"[^\"]*(authorization|Bearer|token=)[^\"]*\"");
|
||||
private static final Pattern COOKIE = Pattern.compile("cookie=");
|
||||
private static final Pattern FULL_BODY = Pattern.compile("body=\\{\\}");
|
||||
private static final Pattern BINARY = Pattern.compile(
|
||||
"(fileBinary|binary=|base64|fileContent=|contentBytes=)");
|
||||
private static final Pattern CREDENTIAL = Pattern.compile("(password=|secret=|credential=)");
|
||||
private static final Pattern API_KEY = Pattern.compile("(apiKey=|api_secret=|clientSecret=)");
|
||||
|
||||
@Test
|
||||
void noTokenLogged() throws IOException {
|
||||
assertNoViolations("Token/鉴权头", TOKEN_AUTH, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noCookieLogged() throws IOException {
|
||||
assertNoViolations("Cookie", COOKIE, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noFullRequestBodyLogged() throws IOException {
|
||||
assertNoViolations("完整请求体 body={}", FULL_BODY, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noBinaryLogged() throws IOException {
|
||||
assertNoViolations("文件二进制", BINARY, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noCredentialLogged() throws IOException {
|
||||
assertNoViolations("用户凭据", CREDENTIAL, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noApiKeyLogged() throws IOException {
|
||||
assertNoViolations("敏感 API Key", API_KEY, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowedMaskedFieldsKept() throws IOException {
|
||||
// 允许项保留:LLM 请求/响应的 body 经 maskChatBody/toJsonForLog/abbreviate 脱敏后仍记录,
|
||||
// 审计不得把这些"已脱敏的可用诊断"当违规删掉(否则白名单空转)。
|
||||
int maskedBodyStatements = 0;
|
||||
try (Stream<Path> paths = Files.walk(Path.of("src/main/java"))) {
|
||||
for (Path path : paths.filter(p -> p.toString().endsWith(".java")).toList()) {
|
||||
List<String> lines = Files.readAllLines(path);
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (!LOG_CALL.matcher(lines.get(i)).find()) {
|
||||
continue;
|
||||
}
|
||||
String stmt = joinStatement(lines, i);
|
||||
if (FULL_BODY.matcher(stmt).find() && hasMaskHelper(stmt)) {
|
||||
maskedBodyStatements++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(maskedBodyStatements >= 4,
|
||||
"应保留至少 4 处已脱敏的 body 诊断日志,实际 " + maskedBodyStatements);
|
||||
}
|
||||
|
||||
@Test
|
||||
void auditScriptRepeatable() throws IOException {
|
||||
assertEquals(violations(TOKEN_AUTH, true), violations(TOKEN_AUTH, true),
|
||||
"审计可重复执行且结果稳定");
|
||||
assertEquals(0, violations(FULL_BODY, true).size(), "重复扫描 body 违规应为 0");
|
||||
}
|
||||
|
||||
private static void assertNoViolations(String label, Pattern pattern, boolean requireMask) throws IOException {
|
||||
List<String> found = violations(pattern, requireMask);
|
||||
assertTrue(found.isEmpty(), label + " 不应入日志(需脱敏或移除):\n " + String.join("\n ", found));
|
||||
}
|
||||
|
||||
private static List<String> violations(Pattern pattern, boolean requireMask) throws IOException {
|
||||
List<String> violations = new ArrayList<>();
|
||||
try (Stream<Path> paths = Files.walk(Path.of("src/main/java"))) {
|
||||
for (Path path : paths.filter(p -> p.toString().endsWith(".java")).toList()) {
|
||||
List<String> lines = Files.readAllLines(path);
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (!LOG_CALL.matcher(lines.get(i)).find()) {
|
||||
continue;
|
||||
}
|
||||
String statement = joinStatement(lines, i);
|
||||
if (pattern.matcher(statement).find()) {
|
||||
boolean ok = !requireMask || hasMaskHelper(statement);
|
||||
if (!ok) {
|
||||
violations.add(path + ":" + (i + 1) + " " + trim(lines.get(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
private static String joinStatement(List<String> lines, int start) {
|
||||
StringBuilder sb = new StringBuilder(lines.get(start));
|
||||
int depth = 0;
|
||||
for (int i = start; i < lines.size() && i < start + 8; i++) {
|
||||
if (i > start) {
|
||||
sb.append(' ').append(lines.get(i));
|
||||
}
|
||||
depth += count(lines.get(i), '(') - count(lines.get(i), ')');
|
||||
if (depth <= 0 && lines.get(i).contains(");")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static int count(String text, char c) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
if (text.charAt(i) == c) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private static boolean hasMaskHelper(String statement) {
|
||||
return MASK_HELPERS.stream().anyMatch(statement::contains);
|
||||
}
|
||||
|
||||
private static String trim(String line) {
|
||||
String t = line.trim();
|
||||
return t.length() > 160 ? t.substring(0, 160) : t;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user