From b638d195f3e0f2b7f6bb7032754ed6e1b12a94d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 15:07:48 +0900 Subject: [PATCH 01/40] feat(security): detect unlocked runtime npm execution --- .github/workflows/security-process.yml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/security-process.yml b/.github/workflows/security-process.yml index 46716c39f..10e702fce 100644 --- a/.github/workflows/security-process.yml +++ b/.github/workflows/security-process.yml @@ -19,6 +19,7 @@ jobs: permissions: contents: read env: + CODEGRAPH_NO_DOWNLOAD: "1" FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - name: Checkout repository @@ -26,17 +27,20 @@ jobs: - name: Prepare CodeGraph CLI env: - CODEGRAPH_PACKAGE: "@colbymchenry/codegraph@0.9.9" + CODEGRAPH_TRUSTED_ROOT: ${{ runner.temp }}/trusted-codegraph run: | set -euo pipefail - codegraph_bin="${RUNNER_TEMP}/codegraph-bin" - mkdir -p "$codegraph_bin" - cat >"${codegraph_bin}/codegraph" <>"$GITHUB_PATH" + mkdir -p "$CODEGRAPH_TRUSTED_ROOT" + cp scripts/ci/codegraph-package/package.json \ + scripts/ci/codegraph-package/package-lock.json \ + "$CODEGRAPH_TRUSTED_ROOT"/ + ( + cd "$CODEGRAPH_TRUSTED_ROOT" + npm ci --ignore-scripts --omit=dev --no-audit --no-fund + ) + CODEGRAPH_BIN="${CODEGRAPH_TRUSTED_ROOT}/node_modules/.bin/codegraph" + test -x "$CODEGRAPH_BIN" + echo "${CODEGRAPH_TRUSTED_ROOT}/node_modules/.bin" >>"$GITHUB_PATH" - name: Run AppGuardrail deploy gate run: | From a754376e817b2c0c81de42f4c4ae149000cf85c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 15:07:49 +0900 Subject: [PATCH 02/40] feat(security): detect unlocked runtime npm execution --- scanner/rules/github_actions.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/scanner/rules/github_actions.yml b/scanner/rules/github_actions.yml index c405808d3..27d58b67f 100644 --- a/scanner/rules/github_actions.yml +++ b/scanner/rules/github_actions.yml @@ -1,4 +1,27 @@ rules: + # `npx -y` and `npm exec --yes` resolve and execute registry content during + # the trusted workflow run. A package version constrains resolution but is + # not a committed byte-integrity proof for the package and its dependency + # graph. Install a committed lock with npm ci, then execute the local binary. + - id: github-actions-runtime-package-without-integrity + patterns: + - pattern-regex: '^[ \t]*(?!#)(?:(?:-[ \t]+)?run[ \t]*:[ \t]*)?(?:exec[ \t]+)?(?:npx[ \t]+(?:-y|--yes)\b|npm[ \t]+exec[ \t]+(?:-y|--yes)\b)' + message: | + This GitHub Actions workflow downloads and executes an npm registry + package at runtime without committed lockfile integrity verification. + A package version is not a byte-integrity boundary for its tarball and + transitive dependencies. Commit the package and lockfile, install it + with `npm ci --ignore-scripts`, and execute the verified local binary; + keep tool self-download disabled where supported. [CWE-829 - Inclusion + of Functionality from Untrusted Control Sphere; OWASP A08:2021 - + Software and Data Integrity Failures] + severity: HIGH + languages: [generic] + paths: + include: + - .github/workflows/*.yml + - .github/workflows/*.yaml + # A write token plus an explicit push back to contributor-controlled branch # state is materially different from a reviewed release publisher. Match the # structure, not temporary-looking names such as `once` or `repair`. From 53214f743c7188bfdf46377a53e8f69acf02a90e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 15:07:50 +0900 Subject: [PATCH 03/40] feat(security): detect unlocked runtime npm execution --- ...ithub_actions_runtime_package_integrity.py | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 tests/test_github_actions_runtime_package_integrity.py diff --git a/tests/test_github_actions_runtime_package_integrity.py b/tests/test_github_actions_runtime_package_integrity.py new file mode 100644 index 000000000..457dacafc --- /dev/null +++ b/tests/test_github_actions_runtime_package_integrity.py @@ -0,0 +1,142 @@ +"""Regression tests for runtime npm package execution in GitHub Actions.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from scanner.cli.appguardrail import SCAN_RULES, _scan_file + + +_RULE_ID = "github-actions-runtime-package-without-integrity" + + +def _scan_workflow(tmp_path: Path, workflow: str) -> list[dict]: + """Scan one repository-local workflow with production path filtering.""" + workflow_path = tmp_path / ".github" / "workflows" / "security.yml" + workflow_path.parent.mkdir(parents=True) + workflow_path.write_text(workflow, encoding="utf-8") + return _scan_file(workflow_path, tmp_path) + + +def test_runtime_package_rule_is_packaged_and_path_scoped() -> None: + """The detector must load once and apply only to workflow YAML files.""" + matches = [rule for rule in SCAN_RULES if rule["id"] == _RULE_ID] + + assert len(matches) == 1 + assert matches[0]["severity"] == "HIGH" + assert matches[0]["include_paths"] == [ + ".github/workflows/*.yml", + ".github/workflows/*.yaml", + ] + assert "CWE-829" in matches[0]["message"] + + +@pytest.mark.parametrize( + "command", + [ + 'exec npx -y "$CODEGRAPH_PACKAGE" "$@"', + "npx --yes @example/scanner@1.2.3 scan .", + "run: npx -y @example/scanner@1.2.3 scan .", + "npm exec --yes -- @example/scanner@1.2.3 scan .", + "npm exec -y -- @example/scanner@1.2.3 scan .", + ], +) +def test_runtime_registry_package_execution_is_reported( + tmp_path: Path, command: str +) -> None: + """Versioned registry packages still lack committed byte integrity.""" + workflow = f""" +name: Security process +on: pull_request +jobs: + scan: + steps: + - name: Prepare CodeGraph CLI + env: + CODEGRAPH_PACKAGE: "@colbymchenry/codegraph@0.9.9" + run: | + {command} +""" + + findings = _scan_workflow(tmp_path, workflow) + + assert [finding["rule_id"] for finding in findings].count(_RULE_ID) == 1 + + +def test_lock_verified_local_binary_is_not_reported(tmp_path: Path) -> None: + """A committed lock plus npm ci and local binary execution is safe here.""" + workflow = """ +name: Security process +on: pull_request +jobs: + scan: + steps: + - name: Install CodeGraph CLI + env: + CODEGRAPH_NO_DOWNLOAD: "1" + run: | + npm ci --ignore-scripts --omit=dev --no-audit --no-fund + node_modules/.bin/codegraph scan . +""" + + findings = _scan_workflow(tmp_path, workflow) + + assert _RULE_ID not in {finding["rule_id"] for finding in findings} + + +def test_runtime_package_text_outside_workflow_is_not_reported(tmp_path: Path) -> None: + """Documentation and non-workflow YAML must stay outside the rule boundary.""" + example = tmp_path / "docs" / "example.yaml" + example.parent.mkdir() + example.write_text("command: npx -y example@1.0.0\n", encoding="utf-8") + + findings = _scan_file(example, tmp_path) + + assert _RULE_ID not in {finding["rule_id"] for finding in findings} + + +def test_commented_or_echoed_examples_are_not_reported(tmp_path: Path) -> None: + """Disabled or printed examples must not masquerade as executed commands.""" + workflow = """ +name: Documentation example +on: pull_request +jobs: + docs: + steps: + - run: | + # npx -y example@1.0.0 + echo "npx -y example@1.0.0" + printf '%s\\n' 'npm exec --yes example@1.0.0' +""" + + findings = _scan_workflow(tmp_path, workflow) + + assert _RULE_ID not in {finding["rule_id"] for finding in findings} + + +def test_repository_workflow_uses_integrity_locked_codegraph() -> None: + """The retained incident must stay fixed while the detector stays live.""" + repository_root = Path(__file__).parents[1] + workflow_path = repository_root / ".github/workflows/security-process.yml" + workflow = workflow_path.read_text(encoding="utf-8") + package_lock = json.loads( + ( + repository_root + / "scripts/ci/codegraph-package/package-lock.json" + ).read_text(encoding="utf-8") + ) + codegraph_package = package_lock["packages"][ + "node_modules/@colbymchenry/codegraph" + ] + + findings = _scan_file(workflow_path, repository_root) + + assert _RULE_ID not in {finding["rule_id"] for finding in findings} + assert "npm ci --ignore-scripts --omit=dev --no-audit --no-fund" in workflow + assert 'CODEGRAPH_NO_DOWNLOAD: "1"' in workflow + assert "node_modules/.bin/codegraph" in workflow + assert codegraph_package["version"] == "0.9.9" + assert codegraph_package["integrity"].startswith("sha512-") From d4594e3f3276f63c821a092e5de5646e94418fe5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 15:07:51 +0900 Subject: [PATCH 04/40] feat(security): detect unlocked runtime npm execution --- scripts/ci/codegraph-package/package.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 scripts/ci/codegraph-package/package.json diff --git a/scripts/ci/codegraph-package/package.json b/scripts/ci/codegraph-package/package.json new file mode 100644 index 000000000..13f086b6b --- /dev/null +++ b/scripts/ci/codegraph-package/package.json @@ -0,0 +1,8 @@ +{ + "name": "appguardrail-codegraph-runtime", + "version": "0.0.0", + "private": true, + "dependencies": { + "@colbymchenry/codegraph": "0.9.9" + } +} From 5700fa70bc87c2b721c52a45fe9ff41b94cc9c17 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 15:07:52 +0900 Subject: [PATCH 05/40] feat(security): detect unlocked runtime npm execution --- .../ci/codegraph-package/package-lock.json | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 scripts/ci/codegraph-package/package-lock.json diff --git a/scripts/ci/codegraph-package/package-lock.json b/scripts/ci/codegraph-package/package-lock.json new file mode 100644 index 000000000..032591712 --- /dev/null +++ b/scripts/ci/codegraph-package/package-lock.json @@ -0,0 +1,110 @@ +{ + "name": "appguardrail-codegraph-runtime", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "appguardrail-codegraph-runtime", + "version": "0.0.0", + "dependencies": { + "@colbymchenry/codegraph": "0.9.9" + } + }, + "node_modules/@colbymchenry/codegraph": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@colbymchenry/codegraph/-/codegraph-0.9.9.tgz", + "integrity": "sha512-23Dl9q0RHKVhJRp+Y823GfmQJjcPiRycoKN7TkaI8eJitluRvieoVlclPILqua8znvkAw9KevYziFfK3k74CBQ==", + "license": "MIT", + "bin": { + "codegraph": "npm-shim.js" + }, + "optionalDependencies": { + "@colbymchenry/codegraph-darwin-arm64": "0.9.9", + "@colbymchenry/codegraph-darwin-x64": "0.9.9", + "@colbymchenry/codegraph-linux-arm64": "0.9.9", + "@colbymchenry/codegraph-linux-x64": "0.9.9", + "@colbymchenry/codegraph-win32-arm64": "0.9.9", + "@colbymchenry/codegraph-win32-x64": "0.9.9" + } + }, + "node_modules/@colbymchenry/codegraph-darwin-arm64": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@colbymchenry/codegraph-darwin-arm64/-/codegraph-darwin-arm64-0.9.9.tgz", + "integrity": "sha512-Mu6xhG4bF1OLeTqN2eSI5U8z2esymK/RjxP3T8cCigA4YprDIvd3XSGmTZREbDNRCuqrCi0R1LCJ7fsdFfpAPA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@colbymchenry/codegraph-darwin-x64": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@colbymchenry/codegraph-darwin-x64/-/codegraph-darwin-x64-0.9.9.tgz", + "integrity": "sha512-xShaChiPJsajmBxvr1yP2XWsYstq0kJzHwQmiUG1huWrE14tP9NJnJ36hsbzHfHiqwBOJo2+5MZHuXmmzJa/vQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@colbymchenry/codegraph-linux-arm64": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@colbymchenry/codegraph-linux-arm64/-/codegraph-linux-arm64-0.9.9.tgz", + "integrity": "sha512-zccx1m3gGB2jAGfz41U+1GQNK12mtLGuX48XyZXiyQIGFsJQCFS8v/OvShAfPygSGhUr3zKMetbkMnztp7ULtg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@colbymchenry/codegraph-linux-x64": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@colbymchenry/codegraph-linux-x64/-/codegraph-linux-x64-0.9.9.tgz", + "integrity": "sha512-vQfe5VdSQb/cVo1pwO12FyzTNaDIXtN3dLuAS/LIPsg2tQVyT9fwr10QmL10qWUNBShjgnRsomHdMG7hCjW3Yg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@colbymchenry/codegraph-win32-arm64": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@colbymchenry/codegraph-win32-arm64/-/codegraph-win32-arm64-0.9.9.tgz", + "integrity": "sha512-U2QlTT434ulbEw6sTJVS+VNhjJUbZqCnw8tXuXSKkcuzGPZq/Hxoofe9UpY77+J9hAJu9PwwiWjEEaiDX9yb1w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@colbymchenry/codegraph-win32-x64": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@colbymchenry/codegraph-win32-x64/-/codegraph-win32-x64-0.9.9.tgz", + "integrity": "sha512-N7Xgt80BeEy78nNeqHaUgcsngxmy1Hty570QSa4hOh4zl6z6v6n7i9ZSUfraktESVUkfmMbUwoqfEzVwUbB0/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + } + } +} From 255d1bddc433ea8cc26459c903d97de224587293 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 15:07:54 +0900 Subject: [PATCH 06/40] feat(security): detect unlocked runtime npm execution --- ARCHITECTURE.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bb3a2105d..db8f07151 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -43,6 +43,13 @@ The detector that observes evidence is authoritative for its finding. `scanner/r External engines retain their own engine/rule/version provenance. AppGuardrail normalizes their output but does not claim their analysis was performed internally. +The proposed `github-actions-runtime-package-without-integrity` built-in rule owns +the narrow workflow-source signal for runtime `npx`/`npm exec` downloads. It does +not claim registry compromise. It reports the absent repository-committed byte +integrity boundary and distinguishes the fixed shape: lock-verified `npm ci` +installation followed by local binary execution. Central workflow prevention and +this repository-local detector remain separate controls. + ## Issue-to-detection boundary ```mermaid @@ -120,4 +127,4 @@ These modes share normalized contracts but can operate separately. ## Change control -A new detector engine, persistent schema, tenant authority, arbitrary autofix class, outbound target policy, issue-audit semantics, or automation credential boundary requires ADR and synchronized technical/security/test documentation. \ No newline at end of file +A new detector engine, persistent schema, tenant authority, arbitrary autofix class, outbound target policy, issue-audit semantics, or automation credential boundary requires ADR and synchronized technical/security/test documentation. From e6c564cf89592f4e4d64a148931b4288bcfdc41b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 15:07:55 +0900 Subject: [PATCH 07/40] feat(security): detect unlocked runtime npm execution --- docs/TRACEABILITY.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index b9757a957..497022870 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -19,6 +19,7 @@ | every retained issue claim mapped to executable detector obligation | issue-detection audit | PR #911 active-PR | | authenticated workflow-result detector evidence | issue-detection audit workflow evidence | PR #911 active-PR | | automatic scanner detection of unsafe stored-webhook SSRF pattern | built-in `python-stored-ssrf-webhook-url` rule | implemented-main through PR #910 for tested Python `set_webhook` direct and one-hop persistence flows; bounded scope | +| GitHub Actions runtime npm execution without committed integrity | built-in `github-actions-runtime-package-without-integrity` rule | Proposed: positive `npx`/`npm exec` and lock-verified local-binary regression corpus; promote only after protected merge | | structural Semgrep-style `pattern:` execution by lightweight engine | built-in scanner | not implemented unless a real structural matcher is added; fixtures are not execution | ## Promotion rules @@ -47,10 +48,22 @@ For stored webhook/callback SSRF, trace separately: Current protected-branch evidence keeps those controls distinct: PR #924 supplies the fail-closed webhook storage boundary, and PR #910 supplies the packaged `python-stored-ssrf-webhook-url` detector plus focused regression corpus. Neither control expands the detector beyond its declared source/sink and flow contract. +## Runtime package integrity boundary + +The GitHub Actions detector is intentionally bounded to executable workflow lines +that invoke `npx -y`, `npx --yes`, `npm exec -y`, or `npm exec --yes`. It treats an +exact package version as insufficient because registry content and the transitive +dependency graph are not verified against repository-committed integrity metadata. +The fixed boundary is a committed lockfile installed with `npm ci` followed by a +local binary invocation, with tool self-download disabled when the tool supports it. +Comments, printed examples, non-workflow YAML, other package managers, composite +actions, chained commands, and indirect shell expansion are outside this first +detector version. + ## Standards/research Existing repository docs/doctoring/security evidence remain the bibliography/source-of-truth for standards such as SARIF, CycloneDX, GitHub security interfaces, and applicable OWASP/CWE classes. Material new detector classes should add authoritative standard/CWE/OWASP references and APA 7 citations in doctoring where research/standards materially drive implementation. ## Change rule -Every new issue-class detector or product security boundary should add/update a row and its concrete test/evidence path. Stale/queued/cancelled/rate-limited/predecessor checks cannot promote evidence maturity. \ No newline at end of file +Every new issue-class detector or product security boundary should add/update a row and its concrete test/evidence path. Stale/queued/cancelled/rate-limited/predecessor checks cannot promote evidence maturity. From 3a79d830a495bb87a7affedb9c09eb5506425786 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 9 Oct 2026 15:07:56 +0900 Subject: [PATCH 08/40] feat(security): detect unlocked runtime npm execution --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 110a549d8..10a54d0cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - 대시보드 검색창 커서 유지 — 검색어 중간에서 텍스트를 수정할 때마다 커서가 검색어의 맨 끝으로 점프하는 불편함을 수정했습니다. 이제 입력창의 커서 위치(`selectionStart`/`selectionEnd`)가 동적 렌더링 이후에도 원래 위치에 정확히 유지되어 자연스러운 타이핑 경험을 제공합니다. ### 보안 +- GitHub Actions 공급망 검출 — `npx -y`와 `npm exec --yes`로 워크플로 실행 중 registry 패키지를 내려받아 즉시 실행하는 패턴을 `github-actions-runtime-package-without-integrity`(HIGH, CWE-829)로 탐지합니다. 버전 문자열만으로 tarball·전이 의존성의 바이트 무결성이 고정되지 않는 경계를 검사하며, 커밋된 lockfile을 `npm ci --ignore-scripts`로 검증한 뒤 로컬 binary를 실행하는 경로는 발화하지 않습니다. - 리포트 출력 하드닝 — 생성된 markdown 리포트가 HTML로 렌더될 때 악성 finding 내용(예: 외부 엔진이 스캔한 코드의 `