Skip to content
Open
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
34 changes: 29 additions & 5 deletions .github/workflows/security-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ name: security-pr
# (#313: secrets reaching history because the scan only ran weekly) at PR time.
#
# Scope is deliberately the FAST scanners only — secrets (range-scoped),
# dependencies, and workflow static-analysis — so this stays off the slow path
# (<~3 min) and can be a required status check without throttling the inner loop.
# The heavy image + SAST suite (trivy/grype/semgrep) runs in security.yml and is
# tracked separately by #235; the full-history secret sweep also lives there.
# dependencies, workflow static-analysis, and the silent-success-masking rule
# (ratcheted to newly-introduced findings only) — so this stays off the slow
# path (<~3 min) and can be a required status check without throttling the inner
# loop. The masking scan is diff-scoped via --baseline-commit: it blocks a PR
# that ADDS a finding without re-flagging the pre-existing debt on main (#756).
# The heavy image + full SAST suite (trivy/grype/semgrep auto rules) runs in
# security.yml and is tracked separately by #235; the full-repo blocking masking
# scan (security:sast:masking) also runs there and in the pre-push hook. The
# full-history secret sweep lives in security.yml too.
on:
pull_request: {}
# Must also run on merge_group so this check reports inside the merge queue;
Expand Down Expand Up @@ -58,29 +63,48 @@ jobs:
MG_HEAD_SHA: ${{ github.event.merge_group.head_sha }}
run: |
set -euo pipefail
# `range` is the gitleaks A..B commit range; `baseline` is the single
# commit semgrep diffs against (--baseline-commit reports only findings
# ADDED since it). Both derive from the same base SHA.
case "$EVENT_NAME" in
pull_request)
# Scan exactly the commits this PR introduces.
echo "range=${PR_BASE_SHA}..${PR_HEAD_SHA}" >> "$GITHUB_OUTPUT"
echo "baseline=${PR_BASE_SHA}" >> "$GITHUB_OUTPUT"
;;
merge_group)
# Scan only the queued merge commits — not full history. An empty
# range would walk every ref and fail on secrets on other branches
# (#334 merge-queue ejection).
echo "range=${MG_BASE_SHA}..${MG_HEAD_SHA}" >> "$GITHUB_OUTPUT"
echo "baseline=${MG_BASE_SHA}" >> "$GITHUB_OUTPUT"
;;
workflow_dispatch|*)
# Manual dispatch: scan HEAD only (safe default, not full history).
# baseline=HEAD reports only uncommitted findings (none in CI), so
# the ratchet is a no-op here — the full scan lives in security.yml.
echo "range=HEAD" >> "$GITHUB_OUTPUT"
echo "baseline=HEAD" >> "$GITHUB_OUTPUT"
;;
esac
echo "Resolved GITLEAKS_RANGE='$(tail -n1 "$GITHUB_OUTPUT" | cut -d= -f2-)'"
echo "Resolved GITLEAKS_RANGE='$(grep '^range=' "$GITHUB_OUTPUT" | tail -n1 | cut -d= -f2-)'"
echo "Resolved SEMGREP_MASKING_BASELINE='$(grep '^baseline=' "$GITHUB_OUTPUT" | tail -n1 | cut -d= -f2-)'"

- name: Secret scan (gitleaks, range-scoped)
env:
GITLEAKS_RANGE: ${{ steps.range.outputs.range }}
run: mise run security:secrets:range

# This step is part of the "Secrets, deps, and workflow scan" job, which is
# a required status check on main (repo ruleset) and re-runs in the merge
# queue using trunk's copy of this workflow — so a fork PR cannot gut this
# step to slip a masking regression past merge. Do not remove this step or
# rename the job: the required-check binding is by job name.
- name: Silent-success masking scan (semgrep, ratcheted to new findings)
env:
SEMGREP_MASKING_BASELINE: ${{ steps.range.outputs.baseline }}
run: mise run security:sast:masking:range

- name: Dependency scan (osv-scanner)
run: mise run security:deps

Expand Down
2 changes: 1 addition & 1 deletion agent/src/clarification_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def build_clarification_server() -> Any:
try:
from claude_agent_sdk import create_sdk_mcp_server, tool
except ImportError: # pragma: no cover - SDK always present in the container
return None
return None # nosemgrep: py-silent-success-masking -- optional-dependency feature detect; None is the documented "server not registered" signal and the runner's marker-based fallback covers it (#756 Category 3) # noqa: E501

@tool(
"request_clarification",
Expand Down
2 changes: 1 addition & 1 deletion agent/src/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1599,7 +1599,7 @@ def _stuck_guard_between_turns_hook(ctx: dict) -> list[str]:
_LAST_STUCK_SUMMARY = guard.recent_failure_summary()
except Exception as exc:
log("WARN", f"stuck-guard evaluate raised (ignored): {type(exc).__name__}: {exc}")
return []
return [] # nosemgrep: py-silent-success-masking -- stuck guard is ADVISORY ONLY (see docstring); a guard bug must never wedge a healthy agent, max_turns is the real backstop. [] means "no steer this turn", not a masked failure (#756 Category 3) # noqa: E501

if action.kind == "steer":
_emit_nudge_milestone(ctx, "stuck_steer", action.message[:_NUDGE_PREVIEW_LEN])
Expand Down
2 changes: 1 addition & 1 deletion cdk/src/handlers/jira-webhook-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,7 +1527,7 @@ async function screenCommentsOrDrop(
jira_cloud_id: cloudId,
error: err instanceof Error ? err.message : String(err),
});
return [];
return []; // nosemgrep: ts-silent-success-masking -- comments are advisory enrichment; a screening outage drops them (logged) and the reporter's task still proceeds. Fail-open is the deliberate posture, not a masked bug (#756 Category 3)
}
}

Expand Down
4 changes: 2 additions & 2 deletions cdk/src/handlers/linear-webhook-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3054,7 +3054,7 @@ async function screenProjectDocsOrDrop(
linear_workspace_id: workspaceId,
error: err instanceof Error ? err.message : String(err),
});
return [];
return []; // nosemgrep: ts-silent-success-masking -- project docs are advisory enrichment; a screening outage drops them (logged) and the reporter's task still proceeds. Fail-open is the deliberate posture, not a masked bug (#756 Category 3)
}
}

Expand Down Expand Up @@ -3105,7 +3105,7 @@ async function screenCommentsOrDrop(
linear_workspace_id: workspaceId,
error: err instanceof Error ? err.message : String(err),
});
return [];
return []; // nosemgrep: ts-silent-success-masking -- comments are advisory enrichment; a screening outage drops them (logged) and the reporter's task still proceeds. Fail-open is the deliberate posture, not a masked bug (#756 Category 3)
}
}

Expand Down
18 changes: 18 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,24 @@ run = [
"semgrep scan --config .semgrep/silent-success-masking.yaml --exclude '.semgrep/*' --sarif-output=test-reports/semgrep-silent-success-masking.sarif --error --quiet .",
]

[tasks."security:sast:masking:range"]
description = "Masking scan ratcheted to newly-introduced findings only (per-PR gate). Set SEMGREP_MASKING_BASELINE, e.g. origin/main."
# Ratchet, not a full scan: --baseline-commit reports only findings a branch
# ADDS relative to the baseline, so the pre-existing findings on main (tracked
# in #756) don't block PRs that don't touch them — while any NEW masking
# regression fails here at PR time. Mirrors security:secrets:range. The full
# blocking scan (security:sast:masking) still runs in security.yml and the
# pre-push hook.
# `semgrep test .semgrep/` runs first (as in the full task) so a PR that WEAKENS
# the rule or breaks a fixture fails at PR time too — otherwise a rule-weakening
# change would only be caught weekly / at pre-push (which is currently bypassable
# with --no-verify). Defaults the baseline to origin/main so the task is safe to
# run anywhere; the per-PR CI job sets SEMGREP_MASKING_BASELINE to the PR base SHA.
run = [
"semgrep test .semgrep/",
"semgrep scan --config .semgrep/silent-success-masking.yaml --exclude '.semgrep/*' --baseline-commit \"${SEMGREP_MASKING_BASELINE:-origin/main}\" --error --quiet .",
]

[tasks."security:deps"]
description = "Audit dependencies for known vulnerabilities (osv-scanner)"
# Yarn workspaces use the repo-root lockfile only; do not scan stale cli/docs yarn.lock copies.
Expand Down
Loading