feat(ms-ai-architect): Enhet A2 — Layer B baseline-adjudikering: innholds-basert allowlist (75 funn adjudikert, TDD) + 4 korpus-defekter fikset (2 homoglyph-ord, 2 U+00AD-filer); korpus 389/389 exit 0 [skip-docs]
This commit is contained in:
parent
135f34eb9b
commit
af6c31c4c1
11 changed files with 1048 additions and 15 deletions
|
|
@ -23,9 +23,30 @@
|
|||
|
||||
import { readFileSync, realpathSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { classifyFindings } from './lib/adversarial-scan.mjs';
|
||||
import { detectAdversarial } from './lib/adversarial-detect.mjs';
|
||||
|
||||
/** Adjudicated Layer B allowlist (Enhet A2) — repo-relative, committed, human-reviewed. */
|
||||
const ALLOWLIST_PATH = resolve(dirname(fileURLToPath(import.meta.url)), 'data/layerb-allowlist.json');
|
||||
|
||||
/**
|
||||
* Load the adjudicated allowlist. FAIL-SAFE in the strict direction: a missing,
|
||||
* unreadable, malformed, or mis-shaped file yields an EMPTY allowlist — the gate can
|
||||
* only be relaxed by a well-formed, committed file, never by an error.
|
||||
* @param {{readFile?: (p: string) => string, path?: string}} [deps]
|
||||
* @returns {Array<object>} allowlist entries
|
||||
*/
|
||||
export function loadAllowlist(deps = {}) {
|
||||
const readFile = deps.readFile ?? ((p) => readFileSync(p, 'utf8'));
|
||||
try {
|
||||
const parsed = JSON.parse(readFile(deps.path ?? ALLOWLIST_PATH));
|
||||
return Array.isArray(parsed?.entries) ? parsed.entries : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a batch of candidate KB files. Pure + dependency-injected (readFile + detect) so it is
|
||||
* unit-testable without disk or llm-security. FAIL-CLOSED: a path that cannot be read, or a
|
||||
|
|
@ -36,11 +57,14 @@ import { detectAdversarial } from './lib/adversarial-detect.mjs';
|
|||
* @param {object} [deps]
|
||||
* @param {(p: string) => string} [deps.readFile] — reader (defaults to readFileSync utf8)
|
||||
* @param {(content: string, opts: {path: string}) => Promise<object[]>} [deps.detect] — raw detector
|
||||
* @param {Array<object>} [deps.allowlist] — adjudicated allowlist entries (defaults to the
|
||||
* committed data/layerb-allowlist.json; missing/malformed → empty = fully strict)
|
||||
* @returns {Promise<{ok: boolean, blocked: boolean, warned: boolean, results: Array<{path: string, disposition: string, findings: object[]}>}>}
|
||||
*/
|
||||
export async function scanPaths(paths, deps = {}) {
|
||||
const readFile = deps.readFile ?? ((p) => readFileSync(p, 'utf8'));
|
||||
const detect = deps.detect ?? detectAdversarial;
|
||||
const allowlist = deps.allowlist ?? loadAllowlist();
|
||||
const results = [];
|
||||
for (const path of paths ?? []) {
|
||||
let content;
|
||||
|
|
@ -65,7 +89,8 @@ export async function scanPaths(paths, deps = {}) {
|
|||
});
|
||||
continue;
|
||||
}
|
||||
const { disposition, findings } = classifyFindings(content, raw, {});
|
||||
const allow = allowlist.filter((e) => e.path === path);
|
||||
const { disposition, findings } = classifyFindings(content, raw, { allow });
|
||||
results.push({ path, disposition, findings });
|
||||
}
|
||||
const blocked = results.some((r) => r.disposition === 'block');
|
||||
|
|
@ -76,7 +101,9 @@ export async function scanPaths(paths, deps = {}) {
|
|||
function report(results) {
|
||||
for (const r of results) {
|
||||
if (r.disposition === 'clean') {
|
||||
process.stdout.write(`OK ${r.path}\n`);
|
||||
const allowed = (r.findings ?? []).filter((f) => f.disposition === 'allow').length;
|
||||
const note = allowed > 0 ? ` (${allowed} allowlisted)` : '';
|
||||
process.stdout.write(`OK ${r.path}${note}\n`);
|
||||
continue;
|
||||
}
|
||||
const marker = r.disposition === 'block' ? 'BLOCK' : 'WARN ';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue