fix(llm-security): YAML/workflow parser divergence — block scalars + bare if: (#32,#33,#43)

#33 the frontmatter parser collected a block-scalar body (description: |) but did not skip it, so an indented name:/allowed_tools: inside the body re-matched as a top-level key and overrode the real values TRG-shadow and permission checks depend on; the parser now consumes block-scalar bodies as opaque content. #32 block-scalar headers carrying indentation/chomping indicators (|2, >-, |-2) were not recognized, so their bodies never reached the run: injection sink; now matched via a proper indicator/chomping regex.

#43 the B4 actor auth-bypass detector inspected only braced ${{ }} expressions, missing the canonical bare 'if: github.actor == ...' form (Synacktiv Dependabot-spoof false negative); bare if: expressions now emit a synthetic event the detector reads. Suite 2004/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
This commit is contained in:
Kjell Tore Guttormsen 2026-07-18 10:35:56 +02:00
commit b224e18b42
8 changed files with 244 additions and 7 deletions

View file

@ -63,6 +63,13 @@ describe('workflow-scanner — true-positive cases', () => {
assert.equal(fs[0].severity, 'high');
assert.match(fs[0].evidence, /issue\.body/);
});
it('flags issue.title inside `run: |2` (indentation indicator) as HIGH (#32)', () => {
const fs = findingsByFile(result.findings, 'tp-block-scalar-indent.yml');
assert.equal(fs.length, 1);
assert.equal(fs[0].severity, 'high');
assert.match(fs[0].evidence, /issue\.title/);
});
});
describe('workflow-scanner — false-positive suppression', () => {
@ -184,6 +191,18 @@ describe('workflow-scanner — B4 auth-bypass', () => {
assert.match(auth.recommendation, /pull_request\.user\.login/);
});
it('flags bare `if: github.actor == \'dependabot[bot]\'` (no ${{ }}) as MEDIUM auth-bypass (#43)', async () => {
resetCounter();
const r = await scan(FIXTURE_DIR);
const fs = findingsByFile(r.findings, 'auth-bypass-bare-if.yml');
const auth = fs.find(f => /Actor auth-bypass/i.test(f.title));
assert.ok(auth, `expected an Actor auth-bypass finding in ${JSON.stringify(fs)}`);
assert.equal(auth.severity, 'medium');
assert.equal(auth.owasp, 'LLM06');
assert.match(auth.evidence, /github\.actor == 'dependabot\[bot\]'/);
assert.match(auth.recommendation, /pull_request\.user\.login/);
});
it('does NOT flag plain `if: ${{ startsWith(github.head_ref, …) }}` as auth-bypass', async () => {
resetCounter();
const r = await scan(FIXTURE_DIR);