Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/ossf-scorecard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,3 @@ jobs:
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
with:
sarif_file: results.sarif
- name: Skip OSSF Scorecard on non-default branch
if: github.ref != format('refs/heads/{0}', github.event.repository.default_branch)
run: echo "OSSF Scorecard only supports the default branch; skipped for ${GITHUB_REF}."
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
name: bandscope-supply-chain-warning-remediation
description: Use when BandScope verification, CI, GitHub Actions, Dependabot, OSSF Scorecard, cargo audit, npm audit, CodeQL, security gates, or PR review emits warnings, deprecations, notices, or supply-chain failures.
---

# BandScope Supply-Chain Warning Remediation

## Overview

Treat every supply-chain warning as evidence to classify, fix, or track. The goal is clean verification without weakening BandScope security gates or hiding externally owned risk.

## Workflow

1. Capture the exact warning or failure text, command, working directory, commit SHA, tool version, and whether it appeared locally, in CI, or on a PR.
2. Classify the source: repo code, workflow shape, direct dependency, transitive dependency, scanner/platform limit, or stale issue metadata.
3. Trace the direct owner with structured tooling:
- GitHub Actions: `gh run view <run-id> --log-failed`
- Dependabot: `gh api repos/seonghobae/bandscope/dependabot/alerts/<id>`
- Rust/Tauri: `cargo tree -i <crate> --manifest-path apps/desktop/src-tauri/Cargo.toml`
- npm: `npm explain <package>`
- Python: `uv tree --project services/analysis-engine --package <package>`
4. Add a failing regression guard first when repo code can prevent recurrence.
5. Fix the root cause. Do not use broad log filtering, generic quiet flags, or gate removal.
6. If no maintained fix exists, document the owner chain and create or link a follow-up issue with acceptance criteria and Security Notes.
7. Re-run the original warning command plus the smallest relevant policy/test command.
8. For PR review warnings, push the fix and re-check robot review/check evidence instead of dismissing the review.

## BandScope Rules

- Preserve required checks from `docs/security/github-required-checks.md`.
- Do not disable or downgrade SBOM, dependency review, CodeQL, Trivy, Bandit, secret scanning, OSSF Scorecard, Windows build, or macOS build gates.
- OSSF Scorecard publishing jobs must contain only `uses:` steps. Remove shell diagnostics from the publishing job or move them to a separate non-publishing job.
- Direct dependency changes require lockfile updates and the dependency admission rationale defined in `docs/security/dependency-policy.md`.
- For transitive Rust/Tauri vulnerabilities, prefer minimal lockfile updates. If blocked upstream, record the exact crate chain and patched-version status.
- Treat `+deprecated` Cargo version metadata as a tracked dependency signal, not automatically as a compiler warning.
- Every supply-chain PR or issue update must include Security Notes.

## Verification Commands

Run the narrowest command first, then widen as needed:

- `python3 scripts/checks/verify_supply_chain.py`
- `python3 scripts/checks/security_gates.py`
- `uv run --project services/analysis-engine pytest services/analysis-engine/tests/test_supply_chain_policy.py`
- `npm audit --workspaces --audit-level=high`
- `BANDSCOPE_ENABLE_RUST_CHECK=1 ./scripts/harness/quickcheck.sh`
- `cargo test --manifest-path apps/desktop/src-tauri/Cargo.toml --locked` when Rust changes are in scope

## Issue Template

When a warning remains externally owned, update or create an issue with:

- Current state and exact evidence link
- Root cause and owner chain
- Repo-controlled actions already attempted
- Next maintained fix path or upstream dependency
- Acceptance criteria
- Security Notes covering risk, gate impact, logging/privacy, and test points

## Common Mistakes

| Mistake | Fix |
| --- | --- |
| Adding `grep -v`, `2>/dev/null`, or global quiet flags | Fix the source or add a narrow documented allowance. |
| Treating default-branch CI failure as blocked | Fix repo-controlled workflow failures unless auth, network, permission, or platform capability blocks it. |
| Removing security workflows to unblock a PR | Preserve gates and fix their inputs or job shape. |
| Closing stale warning issues without evidence | Link the passing command, PR, commit, or successor issue. |

## Done Criteria

- Original warning command re-run.
- Output is clean, or residual risk has an owner and linked follow-up issue.
- Regression guard covers repo-controlled recurrence.
- Required checks remain intact.
- PR/issue comments include durable evidence and Security Notes.
67 changes: 67 additions & 0 deletions scripts/checks/verify_supply_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
"publish_results: ${{ github.ref == format('refs/heads/{0}', "
"github.event.repository.default_branch) }}"
)
OSSF_PUBLISH_USES_ONLY_VIOLATION = (
"ossf scorecard publishing job must only contain uses steps; split run steps "
"into a separate non-publishing job"
)
RELEASE_ARTIFACT_GLOB = re.compile(r"(?:^|\s)artifacts/\*")
RELEASE_ASSET_VALIDATOR = (
"scripts/release/select_release_assets.py --output release-assets.txt"
Expand Down Expand Up @@ -248,6 +252,60 @@ def read_workflow(path: Path, label: str, missing: list[str]) -> str:
return path.read_text(encoding="utf-8")


def ossf_scorecard_publish_restriction_violations(
content: str, path: Path | None = None
) -> list[str]:
"""Return OSSF publishing job violations that GitHub cannot publish."""
violations: list[str] = []
current_job_lines: list[str] = []
current_job_start_line = 0
in_jobs = False

def evaluate_job(job_lines: list[str], start_line: int) -> None:
if not job_lines:
return
job_content = "\n".join(job_lines)
if "ossf/scorecard-action" not in job_content:
return
if "publish_results:" not in job_content:
return
has_run_step = any(
stripped.startswith("run:") or re.match(r"^-\s+run:", stripped)
for stripped in (line.strip() for line in job_lines)
)
if has_run_step:
if path is None:
violations.append(OSSF_PUBLISH_USES_ONLY_VIOLATION)
else:
violations.append(
f"{path}:{start_line or 1} -> {OSSF_PUBLISH_USES_ONLY_VIOLATION}"
)

for idx, line in enumerate(content.splitlines(), start=1):
indent = len(line) - len(line.lstrip(" "))
stripped = line.strip()
if indent == 0 and stripped == "jobs:":
in_jobs = True
continue
if not in_jobs:
continue
if indent == 0 and stripped:
evaluate_job(current_job_lines, current_job_start_line)
current_job_lines = []
current_job_start_line = 0
in_jobs = False
continue
if indent == 2 and stripped.endswith(":") and not stripped.startswith("-"):
evaluate_job(current_job_lines, current_job_start_line)
current_job_lines = [line]
current_job_start_line = idx
continue
current_job_lines.append(line)

evaluate_job(current_job_lines, current_job_start_line)
return violations


def verify_workflow_coverage() -> list[str]:
"""Return workflow trigger and artifact coverage violations."""
missing: list[str] = []
Expand Down Expand Up @@ -351,6 +409,15 @@ def verify_workflow_coverage() -> list[str]:
missing.append(
"ossf scorecard publish_results must use the repository default branch guard"
)
workflow_paths = sorted(Path(".github/workflows").glob("*.yml")) + sorted(
Path(".github/workflows").glob("*.yaml")
)
for workflow_path in workflow_paths:
missing.extend(
ossf_scorecard_publish_restriction_violations(
workflow_path.read_text(encoding="utf-8"), workflow_path
)
)
return missing


Expand Down
133 changes: 133 additions & 0 deletions services/analysis-engine/tests/test_supply_chain_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,139 @@ def test_supply_chain_check_rejects_hardcoded_ossf_publish_results_branch(
)


def test_supply_chain_check_rejects_ossf_publish_job_run_steps(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Ensure Scorecard publishing jobs satisfy OSSF uses-only restrictions."""
supply_chain = load_module(
"scripts/checks/verify_supply_chain.py", "verify_supply_chain_ossf_uses_only"
)
publish_guard = supply_chain.OSSF_DEFAULT_BRANCH_PUBLISH_GUARD.partition(": ")[2]
default_branch_ref = "format('refs/heads/{0}', github.event.repository.default_branch)"
scorecard_action = (
" - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3"
)

workflow_dir = tmp_path / ".github" / "workflows"
workflow_dir.mkdir(parents=True)
(workflow_dir / "ossf-scorecard.yml").write_text(
"\n".join(
[
"name: ossf-scorecard",
"on:",
" push:",
" branches:",
" - develop",
" - main",
" schedule:",
" - cron: '30 1 * * 1'",
"jobs:",
" analysis:",
" name: ossf-scorecard",
" steps:",
" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2",
scorecard_action,
f" if: github.ref == {default_branch_ref}",
" with:",
f" publish_results: {publish_guard}",
" - name: Skip OSSF Scorecard on non-default branch",
f" if: github.ref != {default_branch_ref}",
' run: echo "skip"',
]
),
encoding="utf-8",
)

monkeypatch.chdir(tmp_path)

violations = supply_chain.verify_workflow_coverage()

assert any(
"ossf scorecard publishing job must only contain uses steps; split run steps "
"into a separate non-publishing job" in violation
for violation in violations
)


def test_supply_chain_check_rejects_ossf_publish_run_steps_in_any_workflow(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Ensure OSSF publishing restrictions follow Scorecard if it moves workflows."""
supply_chain = load_module(
"scripts/checks/verify_supply_chain.py", "verify_supply_chain_ossf_any_workflow"
)
publish_guard = supply_chain.OSSF_DEFAULT_BRANCH_PUBLISH_GUARD.partition(": ")[2]
default_branch_ref = "format('refs/heads/{0}', github.event.repository.default_branch)"
scorecard_action = (
" - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3"
)

workflow_dir = tmp_path / ".github" / "workflows"
workflow_dir.mkdir(parents=True)
(workflow_dir / "ossf-scorecard.yml").write_text(
"\n".join(
[
"name: ossf-scorecard",
"on: push",
"jobs:",
" analysis:",
" name: ossf-scorecard",
" steps:",
" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2",
scorecard_action,
f" if: github.ref == {default_branch_ref}",
" with:",
f" publish_results: {publish_guard}",
]
),
encoding="utf-8",
)
(workflow_dir / "scorecard-security-gate.yml").write_text(
"\n".join(
[
"name: scorecard-security-gate",
"on: push",
"jobs:",
" moved-scorecard:",
" steps:",
scorecard_action,
f" if: github.ref == {default_branch_ref}",
" with:",
f" publish_results: {publish_guard}",
" - name: extra diagnostics",
' run: echo "this breaks OSSF publishing"',
]
),
encoding="utf-8",
)

monkeypatch.chdir(tmp_path)

violations = supply_chain.verify_workflow_coverage()

assert any(
violation.startswith(".github/workflows/scorecard-security-gate.yml:")
and "ossf scorecard publishing job must only contain uses steps" in violation
for violation in violations
)


def test_supply_chain_check_accepts_repo_ossf_publish_restrictions(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Ensure checked-in OSSF Scorecard workflow follows publish restrictions."""
supply_chain = load_module(
"scripts/checks/verify_supply_chain.py", "verify_supply_chain_ossf_repo"
)
repo_root = Path(__file__).resolve().parents[3]

monkeypatch.chdir(repo_root)

violations = supply_chain.verify_workflow_coverage()

assert not any("ossf scorecard" in violation for violation in violations)


def test_supply_chain_check_rejects_release_published_asset_upload(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
Expand Down
Loading