Skip to content

feat: bind workflow findings to source evidence - #1000

Open
seonghobae wants to merge 5 commits into
developfrom
feat/issue-938-source-bound-workflow-evidence
Open

feat: bind workflow findings to source evidence#1000
seonghobae wants to merge 5 commits into
developfrom
feat/issue-938-source-bound-workflow-evidence

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Closes #938

Summary

  • derive canonical source-bound evidence from acquired GitHub Actions run/job metadata
  • preserve explicit probe_ref, acquirer_ref, repository/revision, generated artifact identity, SHA-256, freshness, typed assessment, and evidence digest
  • fail closed for unavailable, malformed, stale, duplicate, ambiguous, unsupported, or unknown source evidence
  • acquire and validate source evidence before legacy finding conversion; publish only detected assessments to the security-failure issue path
  • honor the configured --lookback-hours as the source freshness bound, including explicit --run-url historical replays
  • carry canonical evidence through the existing collector CLI, IssueOps, and control-plane scan detail without copying raw logs
  • replay a public historical Issue [security-failure] ContextualWisdomLab/.github: OpenCode Review Dispatch ContextualWisdomLab/.github#701@3a15867168d39a248b92c14f6db0e63584e8dc22 #815 OpenCode failure and its successful sibling job as source fixtures with independent test oracles
  • add mutation/adversarial regressions, exact coverage, ADR, architecture/threat/test/operability/traceability updates, and changelog entry

Semantics

A detected result means the security workflow control failed; it is not a confirmed application vulnerability. Caller-provided assessment and digest fields are ignored. Unknown or inconclusive source evidence is not published as a security finding. Freshness remains fail-closed against the explicitly configured lookback window.

Verification

  • uv run pytest -q: 1034 passed
  • source_evidence.py exact statement coverage: 100% (256/256)
  • source_evidence.py branch coverage: 100% (52/52)
  • appguardrail scan --codegraph .: 0 deploy blockers
  • current-head live replay: fetched public run 30769144488 and failed job 91553355284 via the GitHub connector, then ran the production acquire_workflow_evidence path at b1ec29b; result was detected/security-workflow-job-failure with artifact SHA-256 d5f63667ab6254d8c6be26bf6998cb22e7465be5289f53fd1d10413f7d5fb6a9; no raw logs were used

Open in Devin Review

Closes #938

Derive typed clean, detected, or unknown assessments from acquired GitHub Actions run/job metadata, preserve canonical provenance through IssueOps and control-plane scan detail, and add fail-closed fixtures, coverage, and product traceability docs.
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

GitHub Actions의 보안 워크플로 run/job 결과를 검증하는 source-bound evidence 수집기를 추가했습니다. 수집기는 SHA-256, freshness, typed assessment를 생성하고, 정상화된 finding과 IssueOps 및 control plane 상세 조회에 증거를 보존합니다.

Changes

소스 바운드 워크플로 증거

Layer / File(s) Summary
증거 계약 및 획득
appguardrail_core/source_evidence.py, docs/adr/0007-source-bound-workflow-evidence.md, ARCHITECTURE.md
GitHub Actions run/job의 저장소, revision, artifact identity, 시점, 결과를 검증합니다. 제한된 payload만 해시합니다. 성공은 clean, 보안 작업 실패는 detected, 검증 불가 입력은 unknown으로 처리합니다.
Finding 연계 및 보존
scripts/ci/collect_org_security_failures.py, appguardrail_core/issueops.py, docs/TRACEABILITY.md, docs/adr/README.md
수집기가 공유 timestamp와 artifact 참조 집합을 사용해 source evidence를 finding에 연결합니다. IssueOps와 scan detail은 canonical evidence를 보존합니다.
검증, 운영 및 위협 통제
tests/fixtures/source_evidence/*, tests/test_source_bound_workflow_evidence.py, docs/OPERABILITY.md, docs/TEST_STRATEGY.md, docs/THREAT_MODEL.md
성공·실패 fixture와 독립 검증으로 누락, 변조, 불일치, 만료, 중복, 모호성, 미지원 결과를 검증합니다. 운영 상태와 원시 로그 제외 규칙을 문서화합니다.
아키텍처 및 변경 기록
CHANGELOG.md, docs/TEST_STRATEGY.md, ARCHITECTURE.md
표준 source-bound evidence envelope, 레거시 호환 경계, 변경 추적 규칙을 기록합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟠 High · up to 782c7

This PR can incorrectly treat stale or unbound workflow jobs as valid evidence, crash on malformed metadata, and publish inconclusive results as confirmed workflow failures. These paths can create false security findings or bypass safe evidence handling, so the current head is not ready to merge until the validation and publication issues are fixed.

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions
  participant collect_findings
  participant acquire_workflow_evidence
  participant IssueOps
  participant ControlPlane
  GitHubActions->>collect_findings: workflow run/job 결과 제공
  collect_findings->>acquire_workflow_evidence: repository, run, job 전달
  acquire_workflow_evidence->>acquire_workflow_evidence: 필드, freshness, 중복, 결과 검증
  acquire_workflow_evidence-->>collect_findings: source evidence와 typed assessment 반환
  collect_findings->>IssueOps: 증거가 포함된 finding 게시
  collect_findings->>ControlPlane: canonical evidence를 scan detail에 저장
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive 핵심 계약과 운영 경로는 구현되었지만 RED 테스트, 전체 프로덕션 커버리지, docstring, exact-head 감사와 필수 검사 통과 증거가 없습니다. 누락된 acceptance criteria의 실행 결과와 증거를 추가하고, 충족하지 못한 항목을 보완하십시오.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed 코드, 테스트, fixture, 문서와 changelog 변경은 모두 source-bound workflow evidence vertical slice의 범위에 포함됩니다.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 GitHub Actions 워크플로 결과를 소스 증거에 연결하는 변경의 핵심을 정확하고 간결하게 설명합니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-938-source-bound-workflow-evidence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@seonghobae

Copy link
Copy Markdown
Contributor Author

@opencode-agent @cwl-noema-review please review the exact current head 782c7ba for Issue #938. This is a protected review request only; no approval or merge authorization is implied. Please report findings and required changes on this exact head.

coderabbitai[bot]

This comment was marked as resolved.

Add the public Issue #815 OpenCode run and sibling success job as source fixtures with independent outcome oracles and traceability.

Copy link
Copy Markdown
Contributor Author

@opencode-agent @cwl-noema-review please review the exact current head 5a217b5 for Issue #938. This is a protected review request only; no approval or merge authorization is implied. Please report findings and required changes on this exact head.

Copy link
Copy Markdown
Contributor Author

@opencode-agent @cwl-noema-review please review the exact current head 2d86881 for Issue #938, including the source-evidence acquisition ordering, strict run/job identity validation, job-completion freshness precedence, fail-closed publication path, and regression fixtures.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

@opencode-agent @cwl-noema-review please review the exact current head b1ec29b for Issue #938, including the configured lookback-to-freshness propagation and historical --run-url replay regression.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Correction to the preceding bot-review request: review exact current PR #1000 head b1ec29b against develop. Prior review evidence is predecessor-bound; re-run current-head coverage/security/review evidence and report only actionable findings. Do not merge or update the branch.

@opencode-agent opencode-agent Bot added area: ci-cd CI, GitHub Actions, checks, release, or supply chain priority: medium Normal-priority or P2 work status: needs-review Open pull request requiring current-head review or checks type: feature New or expanded product capability labels Aug 22, 2026
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8cbfc395-238b-47de-b2a0-225a5d289e60


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 6 new potential issues.

Open in Devin Review

Comment on lines +211 to +216
workflow = _text(run.get("name")) or _text(job.get("workflow_name"))
job_name = _text(job.get("name"))
if not is_security_name(workflow, job_name):
return _unknown(
repository, acquired_at, "unknown-detector-family", revision=revision
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: acquire uses a two-argument is_security_name while collector uses three; effectively equivalent

collect_findings gates jobs with is_security_name(run.get("name"), job.get("workflow_name"), job.get("name")), while acquire_workflow_evidence recomputes with is_security_name(workflow, job_name) where workflow = _text(run.get("name")) or _text(job.get("workflow_name")) (source_evidence.py). Since is_security_name is variadic and joins all names, dropping job.workflow_name when run.name is truthy could theoretically diverge. In practice GitHub sets run name and job workflow_name to the same workflow name, so the security-term set is unchanged and the divergence only ever fails closed (excludes, never falsely includes). Not a bug, but the duplicated classification logic is a maintenance hazard if the two call sites drift.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +249 to +256
finding = build_source_bound_finding(
repo,
run,
job,
now=collection_now,
seen_artifact_refs=seen_artifact_refs,
max_age_hours=args.lookback_hours,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: run-url replay now bounded by lookback freshness

The --run-url replay path now enforces source freshness against --lookback-hours (default 48h), so replaying an older run yields stale-source-evidence and publishes nothing. Operators validating historical runs must raise --lookback-hours to cover the run's age.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +235 to +243
age = (acquired - observed).total_seconds()
if age < 0 or age > max_age_hours * 3600:
return _unknown(
repository,
acquired_at,
"stale-source-evidence",
revision=revision,
observed_at=observed_at,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Source freshness now uses job completed_at, diverging from run-selection cutoff

collect_findings selects candidate runs using the run's updated_at/created_at against cutoff (collect_org_security_failures.py), but acquire_workflow_evidence computes freshness from job.completed_at or run.updated_at or run.created_at (source_evidence.py) with the same max_age_hours. A run whose overall updated_at is recent (e.g. because other jobs re-ran) but whose relevant security job completed longer ago than the lookback window will now be dropped as stale-source-evidence, whereas the old collector would have published it. This is a deliberate tightening consistent with the ADR, but worth confirming it does not silently drop still-relevant failures.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +244 to +252
causes = job.get("failure_causes")
if causes is not None and (not isinstance(causes, list) or len(causes) != 1):
return _unknown(
repository,
acquired_at,
"ambiguous-cause-order",
revision=revision,
observed_at=observed_at,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Empty failure_causes list is classified as ambiguous-cause-order

The cause-order guard rejects any failure_causes value that is not a single-element list: causes is not None and (not isinstance(causes, list) or len(causes) != 1) (source_evidence.py). An empty list ([]) therefore yields unknown/ambiguous-cause-order rather than being treated as "no cause reported". Since GitHub REST job payloads do not natively include failure_causes, this only matters if a caller supplies it; behavior is fail-closed so it is not a correctness bug, but the empty-list case may be surprising.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +249 to +261
finding = build_source_bound_finding(
repo,
run,
job,
now=collection_now,
seen_artifact_refs=seen_artifact_refs,
max_age_hours=args.lookback_hours,
)
assessment = finding.get("source_evidence", {}).get(
"assessment", {}
)
if assessment.get("status") != "detected":
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Collector now files issues only for job conclusion 'failure', dropping cancelled/timed_out/action_required security jobs

Previously collect_findings appended a finding for every failure-class job (is_failure matches failure/cancelled/timed_out/action_required). Now build_source_bound_finding only returns a full finding when the derived assessment is detected, and acquire_workflow_evidence maps only job conclusion == "failure" to detected (source_evidence.py); cancelled/timed_out/action_required become unknown-detector-result and are skipped at collect_org_security_failures.py. This means cancelled/timed-out/action-required security workflow jobs no longer generate any issue at all. Note the diagnosis() branches in issueops.py that special-case those conclusions are now effectively dead for the collector path. This appears intended per ADR-0007 ("The collector publishes only detected assessments"), but it is a meaningful coverage narrowing worth confirming against the docs that insist non-completion must not be collapsed into a passing state.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +82 to +104
result = {
"schema": SCHEMA,
"detector_family": DETECTOR_FAMILY,
"atomic_cause": ATOMIC_CAUSE,
"control_obligation": CONTROL_OBLIGATION,
"probe_ref": PROBE_REF,
"acquirer_ref": ACQUIRER_REF,
"source_identity": {
"repository": repository,
"revision": revision,
"artifact_ref": artifact_ref,
"artifact_sha256": artifact_sha256,
"observed_at": observed_at,
"acquired_at": acquired_at,
},
"assessment": {
"status": status,
"reason": reason,
"confirmed_vulnerability": False,
},
}
result["evidence_digest"] = _sha256(result)
return result

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: evidence_digest correctly excludes itself from the hash

_evidence computes result["evidence_digest"] = _sha256(result) (source_evidence.py) after building the envelope but before inserting the digest key, so the digest deterministically covers all fields except itself. artifact_sha256 is separately derived from _source_payload, which selects only bounded run/job/step metadata and ignores any caller-injected assessment/source_artifact_sha256 keys on the raw job dict — verified by the mutation tests. This confirms the PR's claim that caller-provided assessment/digest fields cannot influence the derived hash.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: ci-cd CI, GitHub Actions, checks, release, or supply chain enhancement New feature or request priority: medium Normal-priority or P2 work status: needs-review Open pull request requiring current-head review or checks type: feature New or expanded product capability

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

[Product Gap] Ship a source-authoritative detector vertical slice with independent efficacy evidence

1 participant