From 0e81dd3155c3de97b40457672ceef920075c8711 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 07:50:57 +0000 Subject: [PATCH] [oblt-aw][security] Fix SEC-010 semgrep rule mapping Align semgrep finding-to-SEC mapping in scripts/obs/security-scan.sh so secret/token/credential findings are classified under secret-management rules instead of SEC-010 injection by default. Add a regression test for rule mapping and update detector documentation accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/workflows/gh-aw-security-detector.md | 2 +- docs/workflows/security-scanning-ruleset.md | 6 +- scripts/obs/security-scan.sh | 5 +- tests/test_security_scan_semgrep_mapping.py | 78 +++++++++++++++++++++ 4 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 tests/test_security_scan_semgrep_mapping.py diff --git a/docs/workflows/gh-aw-security-detector.md b/docs/workflows/gh-aw-security-detector.md index 54e086f..577451c 100644 --- a/docs/workflows/gh-aw-security-detector.md +++ b/docs/workflows/gh-aw-security-detector.md @@ -42,7 +42,7 @@ jobs: |-------|-----------| | SEC-002, SEC-021, SEC-030, SEC-040, SEC-043, and other workflow rules | **zizmor** (offline audits; `ident` mapped to SEC IDs in [scripts/obs/security-scan.sh](../../scripts/obs/security-scan.sh)) | | SEC-010, SEC-002 (expression), SEC-020 (credentials) | **actionlint** JSON output (security-related kinds / messages only) | -| SEC-010, SEC-012 | **semgrep** `p/github-actions` on `.github/workflows` | +| SEC-002, SEC-010, SEC-012, SEC-020 | **semgrep** `p/github-actions` on `.github/workflows` (secret/token/credential IDs map to secret-management rules; injection IDs map to injection rules) | | SEC-011 | shellcheck on `*.sh` / `*.bash`; actionlint also runs shellcheck on embedded `run:` scripts | | SEC-032 | curl/wget in scripts without checksum/signature helpers in-file (custom heuristic) | | SEC-033 | `npm audit` when lockfile + npm available | diff --git a/docs/workflows/security-scanning-ruleset.md b/docs/workflows/security-scanning-ruleset.md index 5f9e171..00e3469 100644 --- a/docs/workflows/security-scanning-ruleset.md +++ b/docs/workflows/security-scanning-ruleset.md @@ -40,12 +40,12 @@ The table below documents how each rule ID is currently represented in the detec | Rule ID | Implemented in detector | Primary implementation path | |---------|-------------------------|-----------------------------| | SEC-001 | No | Not currently emitted by [`scripts/obs/security-scan.sh`](../../scripts/obs/security-scan.sh) | -| SEC-002 | Yes | `actionlint` secret message mapping and `zizmor` `secrets-outside-env` mapping in [`scripts/obs/security-scan.sh`](../../scripts/obs/security-scan.sh) | +| SEC-002 | Yes | `actionlint` secret message mapping, `zizmor` `secrets-outside-env` mapping, and semgrep secret/token check-id mapping in [`scripts/obs/security-scan.sh`](../../scripts/obs/security-scan.sh) | | SEC-003 | No | Not currently emitted by [`scripts/obs/security-scan.sh`](../../scripts/obs/security-scan.sh) | -| SEC-010 | Yes | `actionlint` expression mapping, `zizmor` template/github-env mappings, and `semgrep` injection mapping in [`scripts/obs/security-scan.sh`](../../scripts/obs/security-scan.sh) | +| SEC-010 | Yes | `actionlint` expression mapping, `zizmor` template/github-env mappings, and `semgrep` injection check-id mapping in [`scripts/obs/security-scan.sh`](../../scripts/obs/security-scan.sh) | | SEC-011 | Yes | `shellcheck` and `actionlint` shellcheck mappings in [`scripts/obs/security-scan.sh`](../../scripts/obs/security-scan.sh) | | SEC-012 | Yes | `zizmor` default and targeted mappings plus `semgrep` non-injection workflow mappings in [`scripts/obs/security-scan.sh`](../../scripts/obs/security-scan.sh) | -| SEC-020 | Yes | `actionlint` credentials mapping and `zizmor` hardcoded credentials mapping in [`scripts/obs/security-scan.sh`](../../scripts/obs/security-scan.sh) | +| SEC-020 | Yes | `actionlint` credentials mapping, `zizmor` hardcoded credentials mapping, and semgrep hardcoded-credential check-id mapping in [`scripts/obs/security-scan.sh`](../../scripts/obs/security-scan.sh) | | SEC-021 | Yes | `zizmor` `unredacted-secrets` mapping in [`scripts/obs/security-scan.sh`](../../scripts/obs/security-scan.sh) | | SEC-022 | Yes | `zizmor` `overprovisioned-secrets` and `secrets-inherit` mappings in [`scripts/obs/security-scan.sh`](../../scripts/obs/security-scan.sh) | | SEC-030 | Yes | `zizmor` unpinned/ref-integrity mappings in [`scripts/obs/security-scan.sh`](../../scripts/obs/security-scan.sh) | diff --git a/scripts/obs/security-scan.sh b/scripts/obs/security-scan.sh index b59354b..d433772 100755 --- a/scripts/obs/security-scan.sh +++ b/scripts/obs/security-scan.sh @@ -184,7 +184,10 @@ if [ -d "$REPO_ROOT/.github/workflows" ] && command -v semgrep >/dev/null 2>&1; (if ($sv == "ERROR" or $sv == "error") then "high" elif ($sv == "WARNING" or $sv == "warning") then "medium" else "low" end) as $sev | - (if ($cid | test("injection|insecure|secret|credential"; "i")) then "SEC-010" + (($cid + " " + $msg) | ascii_downcase) as $text | + (if ($text | test("hardcoded[[:space:]_-]*(secret|token|credential)|(secret|token|credential).*(hardcoded|literal)"; "i")) then "SEC-020" + elif ($text | test("secret|token|credential"; "i")) then "SEC-002" + elif ($text | test("inject|insecure|template"; "i")) then "SEC-010" else "SEC-012" end) as $rule | "\($p)|\($ln)|\($rule)|\($sev)|semgrep [\($cid)]: \($msg)" ' >>"$FINDINGS_TMP" 2>/dev/null || true diff --git a/tests/test_security_scan_semgrep_mapping.py b/tests/test_security_scan_semgrep_mapping.py new file mode 100644 index 0000000..be48fc7 --- /dev/null +++ b/tests/test_security_scan_semgrep_mapping.py @@ -0,0 +1,78 @@ +import json +import os +import stat +import subprocess +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SECURITY_SCAN_SCRIPT = REPO_ROOT / "scripts/obs/security-scan.sh" + + +def test_semgrep_findings_map_to_correct_sec_rules(tmp_path: Path) -> None: + repo_dir = tmp_path / "repo" + workflow_dir = repo_dir / ".github/workflows" + workflow_dir.mkdir(parents=True) + (workflow_dir / "sample.yml").write_text("name: sample\non: push\njobs: {}\n", encoding="utf-8") + + semgrep_results = { + "results": [ + { + "path": str(workflow_dir / "sample.yml"), + "start": {"line": 3}, + "check_id": "p/github-actions/security/secrets-in-workflow", + "extra": {"message": "secret interpolation in run command", "severity": "ERROR"}, + }, + { + "path": str(workflow_dir / "sample.yml"), + "start": {"line": 4}, + "check_id": "p/github-actions/security/hardcoded-credential", + "extra": {"message": "hardcoded credential value found", "severity": "ERROR"}, + }, + { + "path": str(workflow_dir / "sample.yml"), + "start": {"line": 5}, + "check_id": "p/github-actions/security/expression-injection", + "extra": {"message": "expression injection risk", "severity": "ERROR"}, + }, + { + "path": str(workflow_dir / "sample.yml"), + "start": {"line": 6}, + "check_id": "p/github-actions/security/matrix-user-input", + "extra": {"message": "user-controlled matrix value", "severity": "WARNING"}, + }, + ] + } + + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + fake_semgrep = fake_bin / "semgrep" + fake_semgrep.write_text( + "#!/usr/bin/env bash\n" + "cat <<'JSON'\n" + f"{json.dumps(semgrep_results)}\n" + "JSON\n", + encoding="utf-8", + ) + fake_semgrep.chmod(fake_semgrep.stat().st_mode | stat.S_IEXEC) + + env = os.environ.copy() + env["PATH"] = f"{fake_bin}:{env['PATH']}" + + result = subprocess.run( + ["bash", str(SECURITY_SCAN_SCRIPT), str(repo_dir)], + capture_output=True, + text=True, + check=True, + env=env, + ) + + findings_by_line = {} + for line in result.stdout.splitlines(): + _file_path, line_no, sec_rule, _severity, _message = line.split("|", 4) + findings_by_line[line_no] = sec_rule + + assert findings_by_line["3"] == "SEC-002" + assert findings_by_line["4"] == "SEC-020" + assert findings_by_line["5"] == "SEC-010" + assert findings_by_line["6"] == "SEC-012"