Skip to content
Draft
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
2 changes: 1 addition & 1 deletion docs/workflows/gh-aw-security-detector.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/security-scan.sh](../../scripts/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 |
Expand Down
6 changes: 3 additions & 3 deletions docs/workflows/security-scanning-ruleset.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/security-scan.sh`](../../scripts/security-scan.sh) |
| SEC-002 | Yes | `actionlint` secret message mapping and `zizmor` `secrets-outside-env` mapping in [`scripts/security-scan.sh`](../../scripts/security-scan.sh) |
| SEC-002 | Yes | `actionlint` secret message mapping, `zizmor` `secrets-outside-env` mapping, and semgrep secret/token check-id mapping in [`scripts/security-scan.sh`](../../scripts/security-scan.sh) |
| SEC-003 | No | Not currently emitted by [`scripts/security-scan.sh`](../../scripts/security-scan.sh) |
| SEC-010 | Yes | `actionlint` expression mapping, `zizmor` template/github-env mappings, and `semgrep` injection mapping in [`scripts/security-scan.sh`](../../scripts/security-scan.sh) |
| SEC-010 | Yes | `actionlint` expression mapping, `zizmor` template/github-env mappings, and `semgrep` injection check-id mapping in [`scripts/security-scan.sh`](../../scripts/security-scan.sh) |
| SEC-011 | Yes | `shellcheck` and `actionlint` shellcheck mappings in [`scripts/security-scan.sh`](../../scripts/security-scan.sh) |
| SEC-012 | Yes | `zizmor` default and targeted mappings plus `semgrep` non-injection workflow mappings in [`scripts/security-scan.sh`](../../scripts/security-scan.sh) |
| SEC-020 | Yes | `actionlint` credentials mapping and `zizmor` hardcoded credentials mapping in [`scripts/security-scan.sh`](../../scripts/security-scan.sh) |
| SEC-020 | Yes | `actionlint` credentials mapping, `zizmor` hardcoded credentials mapping, and semgrep hardcoded-credential check-id mapping in [`scripts/security-scan.sh`](../../scripts/security-scan.sh) |
| SEC-021 | Yes | `zizmor` `unredacted-secrets` mapping in [`scripts/security-scan.sh`](../../scripts/security-scan.sh) |
| SEC-022 | Yes | `zizmor` `overprovisioned-secrets` and `secrets-inherit` mappings in [`scripts/security-scan.sh`](../../scripts/security-scan.sh) |
| SEC-030 | Yes | `zizmor` unpinned/ref-integrity mappings in [`scripts/security-scan.sh`](../../scripts/security-scan.sh) |
Expand Down
5 changes: 4 additions & 1 deletion scripts/security-scan.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 78 additions & 0 deletions tests/test_security_scan_semgrep_mapping.py
Original file line number Diff line number Diff line change
@@ -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/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"
Loading