feat: source-side Strix → appguardrail per-finding issue emitter#358
feat: source-side Strix → appguardrail per-finding issue emitter#358seonghobae wants to merge 5 commits into
Conversation
Add a source-side emitter that turns Strix vulnerability reports (strix_runs/<run>/vulnerabilities/*.md) into deduplicated, lifecycle-managed issues in ContextualWisdomLab/appguardrail, replacing the coarse per-failed-run polling collector. - scripts/ci/strix_emit_appguardrail_issues.py: parse each finding into a normalized record; dedup by sha256(source_repo, title, normalized_location) stored in a hidden body marker + strix-finding:<shorthash> label; create one issue per finding, update the body on refresh, comment on severity change, reopen returning findings; close-on-fix via set difference guarded to run only on a clean/complete scan. App token from actions/create-github-app-token; when absent it runs DRY-RUN (logs intended ops, never fails the Strix job). Token shapes are scrubbed from logs. - .github/workflows/strix.yml: final best-effort steps (if: always()) mint the app token (owner ContextualWisdomLab, repositories: appguardrail) and run the emitter; continue-on-error so the pass/fail gate is unchanged. scan-complete is gated on the Strix step succeeding. - tests/test_strix_emit_appguardrail_issues.py: synthetic fixtures cover parsing (plain/bold, no-location), stable dedup hashing, create/update/close planning, close-on-fix set difference, the incomplete-scan guard, dry-run and live execution. 100% coverage of the new module; interrogate clean. - docs/strix-appguardrail-issues.md: mechanism, dedup/lifecycle, the App + secrets to provision, and retirement of appguardrail's old collector. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTAMs4bpSZS77Xe3RQjv9P
Close-on-fix did a repo-wide set difference: it closed every open Strix
issue for repo:<name> whose finding-hash was absent from the current run,
guarded only by scan completion (run_strix.outcome == 'success'). But the
normal Strix trigger (pull_request_target, and workflow_dispatch+pr_number)
is PR-scoped (strix.yml __PR_SCOPE__ / STRIX_DISABLE_PR_SCOPING=0 /
IS_PR_EVIDENCE_RUN=true) and only scans the PR's changed files. A cleanly
completed PR-scoped scan therefore closed still-valid open findings in files
the PR never touched -- a clean PR with zero findings closed ALL open Strix
issues for that repo. Data loss.
Fix: plumb a scan-scope flag from strix.yml into the emitter and require BOTH
guards to close. Close-on-fix now runs only when the scan completed cleanly
AND covered the whole repo (scope=full).
- emitter: add SCOPE_FULL/SCOPE_PR, EmitContext.scan_scope (default pr =
safe) and EmitContext.close_on_fix_enabled (scan_complete AND scope==full);
gate the close-on-fix block on it; add --scope {full,pr} (default pr).
PR/unknown scope still creates/updates/reopens findings, never closes.
- strix.yml: emit STRIX_SCAN_SCOPE mirroring the PR-scoping decision
(pull_request_target || inputs.pr_number => pr, else full) and pass
--scope; unknown/unset defaults to the safe pr.
- tests: rename the test that blessed the unsafe semantic to assert
close-on-fix happens ONLY for full scope; add PR-scoped (zero + subset) and
unknown-scope tests asserting nothing closes; add CLI full/pr close tests.
100% line + docstring coverage retained.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTAMs4bpSZS77Xe3RQjv9P
CRITICAL data-loss fix pushed (8987a3d)Bug (confirmed): close-on-fix in Fix: close-on-fix now requires both guards — the scan completed cleanly and it covered the whole repo.
Verification: 43/43 emitter tests pass, 100% line coverage, 100% interrogate docstring coverage, dry-run smoke confirms full scope plans a close-eligible reconciliation while PR scope prints "close-on-fix is disabled" and closes nothing. (The 3 failing CodeGraph (code-review-graph, full build: 38 files / 866 nodes) confirmed the blast radius — |
Emit per-finding Strix issues to appguardrail using the OpenCode GitHub App token the job already OIDC-exchanges (steps.target_app_token), instead of minting a token via actions/create-github-app-token from to-be-provisioned STRIX_ISSUE_APP_ID / STRIX_ISSUE_APP_PRIVATE_KEY org secrets. - Remove the "Mint appguardrail issue app token" step and the new-secret dependency entirely -- no new GitHub App or org secret to provision. - Feed STRIX_ISSUE_APP_TOKEN from the existing OpenCode-app OIDC exchange the workflow already runs for cross-repo reads/status. - Emitter is unchanged and stays best-effort: if the OpenCode token is absent (OIDC unavailable) or lacks Issues: write on appguardrail, it degrades to DRY-RUN / swallows the write and never fails the gate. - Per-finding parse, dedup hash, and close-on-fix full-repo scope guard are preserved unchanged. - docs/strix-appguardrail-issues.md documents the one-time OpenCode App enablement (grant Issues: Read and write, install on appguardrail) with exact click-paths; no new App/secret creation. Emitter tests: 43 passed, 100% coverage of the module. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTAMs4bpSZS77Xe3RQjv9P
Reworked: no new GitHub App / secrets — reuse the OpenCode app token (
|
…heme SSRF reject The coverage-evidence gate runs the full pytest suite before measuring coverage. Three assertions had drifted from the intentional source: - opencode-review.yml grew the `if:` block between `opencode-review-target:` and `timeout-minutes: 360`, exceeding the 240-char regex window (widen to 400). - pr-review-merge-scheduler.yml defaults `review_dispatch_limit` to "1", not "-1". - noema rejects non-http(s) schemes with "URL scheme must be http or https". Also fixes a real bug: uppercase-scheme URLs (HTTPS://) were wrongly rejected by the case-sensitive startswith guard; lowercase before comparing so valid HTTPS:// URLs pass while SSRF protection is preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTAMs4bpSZS77Xe3RQjv9P
Summary
Replaces the coarse, never-provisioned per-failed-run collector with a
source-side, per-finding emitter. The central Strix workflow now turns each
strix_runs/<run>/vulnerabilities/*.mdreport into a deduplicated,lifecycle-managed issue in
ContextualWisdomLab/appguardrail.CVSS + vector, Target, Endpoint, Method, Description, Impact, Code Locations
path:line[-range], Remediation); tolerant of plain and**bold**fieldstyles;
/workspace/<repo>/and PR-scope prefixes normalized.sha256(source_repo + "\n" + title + "\n" + normalized_location),stored in a hidden
<!-- strix-finding: <hash> -->body marker and astrix-finding:<shorthash>label. Existing open/closed issues are looked up byhash before creating.
comment on severity change; reopen returning findings; close-on-fix by set
difference between the current run's finding hashes and open scoped issues.
(
steps.run_strix.outcome == 'success'). A failed/skipped/infra-broken scannever closes issues.
actions/create-github-app-token(Issues: write on
appguardrail). When the App/secrets are absent, the emitterruns DRY-RUN — logs every intended create/update/close, mutates nothing, exits
0 — so the Strix pass/fail gate is unchanged.
--dry-runforces this. Tokenshapes are scrubbed from logs.
if: always()+continue-on-error; least-privilege tokenscoped to
appguardrailonly.Tests
tests/test_strix_emit_appguardrail_issues.py: synthetic multi-findingfixtures (varying severity, a no-location finding), stable dedup hash,
create/update/close op planning in dry-run (mocked GitHub API), close-on-fix
set-difference logic, and the incomplete-scan guard (no closes). 39 tests
pass, 100% coverage of the new module; interrogate clean. Workflow YAML and
the embedded emit step pass
bash -n.The emitter stays in DRY-RUN until a human provisions:
ContextualWisdomLab(suggestedstrix-issue-emitter) withRepository → Issues: Read and write, installed on (at least) the
appguardrailrepo..github) level:STRIX_ISSUE_APP_ID— the App's numeric App IDSTRIX_ISSUE_APP_PRIVATE_KEY— the App's private-key.pemcontentsFull steps in
docs/strix-appguardrail-issues.md.Follow-up in another repo
ContextualWisdomLab/appguardrail's.github/workflows/org-security-failure-collector.ymlshould be disabled/removed once this lands to avoid duplicate issues (cannot
be changed from this repo). Its
ORG_SECURITY_FAILURE_APP_ID/_PRIVATE_KEYsecrets can be retired.
🤖 Generated with Claude Code