fix(posture): NIST Govern credit for policy.json only when it is honored
checkNISTAlignment credited Govern for a .llm-security/policy.json that merely existed. Since v8.1.0 policy-loader ignores a target's policy outside the caller's own working tree, so a foreign target (a clone, an installed package) earned governance credit for a file no scanner reads. The credit now also requires isOwnWorkingTree(projectRoot). Red first: the two foreign cases (node_modules package, target outside cwd) failed; the own-tree known-positive passed before and after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a61c1c9648
commit
50f60b46e2
2 changed files with 77 additions and 1 deletions
|
|
@ -15,6 +15,7 @@ import { homedir } from 'node:os';
|
||||||
import { scanForInjection } from './lib/injection-patterns.mjs';
|
import { scanForInjection } from './lib/injection-patterns.mjs';
|
||||||
import { gradeFromPassRate, riskScore, riskBand, verdict, SEVERITY } from './lib/severity.mjs';
|
import { gradeFromPassRate, riskScore, riskBand, verdict, SEVERITY } from './lib/severity.mjs';
|
||||||
import { finding, scannerResult, resetCounter } from './lib/output.mjs';
|
import { finding, scannerResult, resetCounter } from './lib/output.mjs';
|
||||||
|
import { isOwnWorkingTree } from './lib/own-working-tree.mjs';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Constants
|
// Constants
|
||||||
|
|
@ -1331,7 +1332,10 @@ async function checkNISTAlignment(projectRoot, hooksJson, projectSettings) {
|
||||||
// Govern: deny-first configuration or policy documentation
|
// Govern: deny-first configuration or policy documentation
|
||||||
const settingsJson = projectSettings || await readJson(join(projectRoot, '.claude', 'settings.json'));
|
const settingsJson = projectSettings || await readJson(join(projectRoot, '.claude', 'settings.json'));
|
||||||
const hasDenyFirst = settingsJson?.permissions?.defaultPermissionLevel === 'deny';
|
const hasDenyFirst = settingsJson?.permissions?.defaultPermissionLevel === 'deny';
|
||||||
const hasPolicyFile = await fileExists(join(projectRoot, '.llm-security', 'policy.json'));
|
// v8.1.2: a policy.json counts only when it is honored for this target —
|
||||||
|
// policy-loader.mjs ignores it outside the caller's own working tree.
|
||||||
|
const hasPolicyFile = await fileExists(join(projectRoot, '.llm-security', 'policy.json'))
|
||||||
|
&& isOwnWorkingTree(projectRoot);
|
||||||
if (hasDenyFirst || hasPolicyFile) {
|
if (hasDenyFirst || hasPolicyFile) {
|
||||||
functionsPresent++;
|
functionsPresent++;
|
||||||
evidence.push('Govern: deny-first config or policy file present');
|
evidence.push('Govern: deny-first config or policy file present');
|
||||||
|
|
|
||||||
72
tests/scanners/posture-policy-scope.test.mjs
Normal file
72
tests/scanners/posture-policy-scope.test.mjs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
// posture-policy-scope.test.mjs — NIST "Govern" credit for .llm-security/
|
||||||
|
// policy.json only when the policy is actually honored for the target (v8.1.2).
|
||||||
|
//
|
||||||
|
// Since v8.1.0 a target's policy.json is read only for the caller's own
|
||||||
|
// working tree (scanners/lib/own-working-tree.mjs). posture-scanner still
|
||||||
|
// credited Govern for a policy.json that merely EXISTED, so a foreign target
|
||||||
|
// (a clone, an installed package) earned governance credit for a file no
|
||||||
|
// scanner would read. Fixtures under $HOME, outside os.tmpdir() (always
|
||||||
|
// foreign) and this repo's git tree; no deny-first settings, so policy.json
|
||||||
|
// is the only possible source of the Govern credit.
|
||||||
|
// (own) cwd = a repo, target = a subdir of it with policy.json → credited
|
||||||
|
// (foreign) cwd = a repo, target = node_modules/pkg with policy.json → not
|
||||||
|
// (foreign) target outside cwd with policy.json → not
|
||||||
|
|
||||||
|
import { describe, it, before, after, beforeEach } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { resetCounter } from '../../scanners/lib/output.mjs';
|
||||||
|
import { scan } from '../../scanners/posture-scanner.mjs';
|
||||||
|
import { mkOwnTreeDir, inOwnTree } from '../helpers/own-tree.mjs';
|
||||||
|
|
||||||
|
const GOVERN = /^Govern:/;
|
||||||
|
|
||||||
|
function writeProject(dir) {
|
||||||
|
mkdirSync(join(dir, '.llm-security'), { recursive: true });
|
||||||
|
writeFileSync(join(dir, '.llm-security', 'policy.json'), '{}\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function nistEvidence(cwd, target) {
|
||||||
|
resetCounter();
|
||||||
|
const result = await inOwnTree(cwd, () => scan(target));
|
||||||
|
const cat = result.categories.find((c) => c.id === 15);
|
||||||
|
assert.equal(cat.name, 'NIST AI RMF Alignment');
|
||||||
|
return cat.evidence || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('posture NIST Govern: policy.json counts only when honored (v8.1.2)', () => {
|
||||||
|
let root;
|
||||||
|
let repo;
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
root = mkOwnTreeDir('posture-policy-scope-');
|
||||||
|
repo = join(root, 'repo');
|
||||||
|
mkdirSync(repo, { recursive: true });
|
||||||
|
const r = spawnSync('git', ['init', '-q', repo], { encoding: 'utf8' });
|
||||||
|
assert.equal(r.status, 0, `git init failed: ${r.stderr}`);
|
||||||
|
writeProject(join(repo, 'app'));
|
||||||
|
writeProject(join(repo, 'node_modules', 'pkg'));
|
||||||
|
writeProject(join(root, 'elsewhere'));
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => { rmSync(root, { recursive: true, force: true }); });
|
||||||
|
|
||||||
|
beforeEach(() => resetCounter());
|
||||||
|
|
||||||
|
it('own tree: a honored policy.json earns the Govern credit (known-positive)', async () => {
|
||||||
|
const evidence = await nistEvidence(repo, join(repo, 'app'));
|
||||||
|
assert.ok(evidence.some((e) => GOVERN.test(e)), `expected Govern evidence, got ${JSON.stringify(evidence)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('foreign (installed package): policy.json present but not honored earns no credit', async () => {
|
||||||
|
const evidence = await nistEvidence(repo, join(repo, 'node_modules', 'pkg'));
|
||||||
|
assert.ok(!evidence.some((e) => GOVERN.test(e)), `unexpected Govern evidence: ${JSON.stringify(evidence)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('foreign (outside cwd): policy.json present but not honored earns no credit', async () => {
|
||||||
|
const evidence = await nistEvidence(repo, join(root, 'elsewhere'));
|
||||||
|
assert.ok(!evidence.some((e) => GOVERN.test(e)), `unexpected Govern evidence: ${JSON.stringify(evidence)}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue