Use with caution. Address findings before production.
Scanned 11/1/2026, 1:38:47 PMยทCached resultยทDeep Scanยท91 rulesยทHow we decide โ
AIVSS Score
Medium
Severity Breakdown
0
critical
2
high
22
medium
0
low
MCP Server Information
Findings
This package carries a C grade with 2 high-severity command injection vulnerabilities and 1 ansi escape injection issue that could allow attackers to execute arbitrary code or manipulate terminal output. Additionally, 21 medium-severity server configuration problems create potential exposure vectors, though no critical issues were detected. The safety score of 64/100 suggests this package requires significant security hardening before production use.
AIPer-finding remediation generated by bedrock-claude-haiku-4-5 โ 20 of 24 findings. Click any finding to read.
No known CVEs found for this package or its dependencies.
Scan Details
Done
Sign in to save scan history and re-scan automatically on new commits.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasnโt shipped yet.
24 of 24 findings
24 findings
Command injection risk. Shell-execution sink called with interpolated / attacker-controllable input. Use list-arg subprocess with shell=False, or escape every variable via shlex.quote (Python) / shell-escape (Node).
Evidence
| 52 | } |
| 53 | |
| 54 | execSync('git fetch --depth=1 origin ' + sha, { cwd: project, stdio: 'pipe', timeout: 120_000 }); |
| 55 | execSync('git checkout --detach ' + sha, { cwd: project, stdio: 'pipe', timeout: 120_000 }); |
| 56 | } |
RemediationAI
The problem is that `sha` (a Git commit reference) is concatenated directly into shell commands via string interpolation, allowing command injection if `sha` contains shell metacharacters. Replace both `execSync()` calls with array-based argument passing: `execSync('git', ['fetch', '--depth=1', 'origin', sha], { cwd: project, stdio: 'pipe', timeout: 120_000 })` and `execSync('git', ['checkout', '--detach', sha], { cwd: project, stdio: 'pipe', timeout: 120_000 })`. This fix eliminates the vulnerability because arguments passed as an array are never interpreted as shell syntax. Verify by running the script with a `sha` value containing special characters like `$(whoami)` and confirm the command fails or treats it as a literal ref name rather than executing code.
Command injection risk. Shell-execution sink called with interpolated / attacker-controllable input. Use list-arg subprocess with shell=False, or escape every variable via shlex.quote (Python) / shell-escape (Node).
Evidence
| 51 | } |
| 52 | } |
| 53 | |
| 54 | execSync('git fetch --depth=1 origin ' + sha, { cwd: project, stdio: 'pipe', timeout: 120_000 }); |
| 55 | execSync('git checkout --detach ' + sha, { cwd: project, stdio: 'pipe', timeout: 120_000 }); |
| 56 | } |
RemediationAI
The problem is identical to finding 1: `sha` is concatenated into `execSync()` commands, creating a command injection sink. Apply the same fix: convert both `execSync()` calls to use array-based arguments instead of string concatenation. This prevents shell metacharacter interpretation. Test by passing a malicious `sha` like `; rm -rf /` and verify the git command rejects it as an invalid ref rather than executing the injected command.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 177 | recoverable: true, |
| 178 | hint: 'Repair or remove the invalid Guard artifact, then run `codapult-guard doctor`.', |
| 179 | }); |
| 180 | if (options.json || options.machine) console.log(JSON.stringify(payload, null, 2)); |
| 181 | else fail(`${terminalSafe(payload.message)} Run \`codapult-guard doctor\` for details.`); |
| 182 | process.exitCode = 1; |
| 183 | return { invalid: true }; |
RemediationAI
The problem is that `payload.message` is printed to the terminal without sanitization, allowing ANSI escape sequences to rewrite output or hide commands. The code already calls `terminalSafe()` on `payload.message`, but verify that `terminalSafe()` function strips or escapes all ANSI control sequences (CSI codes, OSC sequences, etc.); if it does not exist or is incomplete, add a robust sanitizer using a library like `strip-ansi` or implement regex-based removal of `\x1b\[[0-9;]*m` and similar patterns. This fix prevents cursor repositioning and output injection attacks. Verify by passing a message containing `\x1b[2J` (clear screen) or `\x1b[H` (cursor home) and confirm the output displays literally rather than executing the escape sequence.
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 246 | tooling: z |
| 247 | .record( |
| 248 | z.string().trim().min(1), |
| 249 | z.object({ script: z.string().trim().min(1), enabled: z.boolean() }), |
| 250 | ) |
| 251 | .default({}), |
| 252 | completionGate: z.object({ |
RemediationAI
The problem is that the `script` field in the `tooling` schema accepts any string without length or pattern constraints, allowing callers to inject arbitrarily long or malicious script payloads. Replace `z.string().trim().min(1)` with `z.string().trim().min(1).max(10000).regex(/^[a-zA-Z0-9_\-./]+$/)` (or an appropriate allowlist pattern for your use case) to constrain the script field to a reasonable length and safe character set. This fix narrows the blast radius by preventing injection of complex or excessively long scripts. Verify by attempting to pass a script value exceeding the max length or containing forbidden characters, and confirm the schema validation rejects it.
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # Cursor integration |
| 2 | |
| 3 | This page targets Cursor Agent in a trusted local workspace. Cursor supports project MCP |
| 4 | configuration in `.cursor/mcp.json`, project rules in `.cursor/rules/`, and project hooks in |
| 5 | `.cursor/hooks.json`. |
| 6 | |
| 7 | ## 1. Install Guard |
| 8 | |
| 9 | Guard is listed in the official MCP Registry as `io.github.codapult/guard`. In a client that can |
| 10 | import Registry entries, search for that identifier and select the npm/stdio server. Cursor's |
| 11 | native MCP directory and one-click install are separate from |
RemediationAI
The problem is that the MCP manifest (or integration documentation) does not declare any authentication mechanism, making it unclear whether the server is protected and how. Add an explicit `auth` or `authorization` field to the MCP manifest (typically in `package.json` or a dedicated manifest file) documenting the actual security modelโfor example, `"auth": "local-only"` or `"auth": "host-level-isolation"` if relying on OS-level access controls. This fix clarifies the security boundary for reviewers and users. Verify by checking that the manifest now includes an `auth` field and that documentation explains how the server validates or restricts caller identity.
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # Codapult Guard: Architecture Guardrails for AI Coding Agents |
| 2 | |
| 3 | Codapult Guard is local-first architecture guardrail software for AI coding agents and JavaScript |
| 4 | or TypeScript repositories. It detects architectural drift by observing the existing software |
| 5 | architecture, recording project memory, and checking code changes against team-approved rules and |
| 6 | contracts. It also provides bounded project context for AI code review and deterministic checks for |
| 7 | local development and CI. |
| 8 | |
| 9 | Guard is not a repl |
RemediationAI
The problem is that the documentation does not declare an authentication mechanism for the MCP server, leaving the security model ambiguous. Add an explicit statement in `docs/guard.md` (e.g., in an "Security" or "Authentication" section) clarifying whether the server relies on local-only execution, host-level isolation, or another mechanism, and update the MCP manifest to include an `auth` field. This fix ensures reviewers and integrators understand the trust boundary. Verify by confirming the documentation now includes a clear security/auth section and the manifest declares the auth model.
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # @codapult/guard |
| 2 | |
| 3 | [](https://github.com/codapult/codapult-guard/actions/workflows/ci.yml) |
| 4 | [](https://github.com/codapult/codapult-guard/actions/workflows/guard-fixtures.yml) |
| 5 | [](https://www.npmjs.com/package/@codapult/guard) |
| 6 | [. This fix makes the security posture transparent to users and auditors. Verify by checking that the README now includes authentication/security documentation and the manifest declares the auth mechanism.
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # GitHub Copilot integration |
| 2 | |
| 3 | GitHub Copilot has two relevant local surfaces. Use the VS Code instructions for Copilot Chat in Agent mode, or the Copilot CLI instructions for the terminal agent. They do not share the same MCP configuration file. |
| 4 | |
| 5 | ## Copilot Chat in VS Code |
| 6 | |
| 7 | Guard is published in the official MCP Registry as `io.github.codapult/guard`. Registry import is client-dependent; if it is not available in your VS Code MCP gallery, configure the server manually. |
| 8 | |
| 9 | Install the package: |
| 10 | |
| 11 | ``` |
RemediationAI
The problem is that the GitHub Copilot integration documentation does not declare an authentication mechanism for the MCP server. Add an explicit statement in `docs/integrations/github-copilot.md` (e.g., "Guard runs locally in your workspace and does not require external authentication") and ensure the MCP manifest includes an `auth` field. This fix clarifies that the integration is workspace-local and does not expose the server to untrusted callers. Verify by confirming the documentation now includes a security/auth statement and the manifest declares the auth model.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 48 | uses: actions/checkout@v6 |
| 49 | |
| 50 | - name: Install pnpm |
| 51 | uses: pnpm/action-setup@v5 |
| 52 | |
| 53 | - name: Setup Node.js |
| 54 | uses: actions/setup-node@v6 |
RemediationAI
The problem is that `actions/checkout@v6` uses a mutable tag that can be rewritten by a compromised maintainer. Replace `uses: actions/checkout@v6` with `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0` (or the appropriate SHA for your target version). This fix pins the action to an immutable commit, preventing silent substitution of malicious code. Verify by running `git ls-remote https://github.com/actions/checkout refs/tags/v6` to confirm the SHA matches the pinned commit.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 18 | runs-on: ubuntu-latest |
| 19 | steps: |
| 20 | - name: Checkout |
| 21 | uses: actions/checkout@v6 |
| 22 | |
| 23 | - name: Install verified actionlint binary |
| 24 | env: |
RemediationAI
The problem is that `actions/checkout@v6` and `actions/setup-node@v6` use mutable version tags. Replace both with their commit SHAs: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0` and `uses: actions/setup-node@1a4442caab129c34a6a49ec8a84507c35757b900 # v6.x.x` (verify the exact SHAs from the GitHub repository). This fix prevents tag rewriting attacks. Verify by confirming the workflow file now contains 40-character SHAs and the actions still execute correctly.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 21 | uses: pnpm/action-setup@v5 |
| 22 | |
| 23 | - name: Setup Node.js |
| 24 | uses: actions/setup-node@v6 |
| 25 | with: |
| 26 | node-version: 24 |
| 27 | cache: pnpm |
RemediationAI
The problem is that `pnpm/action-setup@v5` and `actions/setup-node@v6` use mutable tags. Replace with pinned SHAs: `uses: pnpm/action-setup@c3fa5a432c6e46164b9d6f89f053b5c8c0de433f # v5.x.x` and `uses: actions/setup-node@1a4442caab129c34a6a49ec8a84507c35757b900 # v6.x.x`. This fix prevents supply-chain attacks via tag mutation. Verify by running the workflow and confirming it completes successfully with the pinned SHAs.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 15 | runs-on: ubuntu-latest |
| 16 | steps: |
| 17 | - name: Checkout Guard |
| 18 | uses: actions/checkout@v6 |
| 19 | |
| 20 | - name: Install pnpm |
| 21 | uses: pnpm/action-setup@v5 |
RemediationAI
The problem is that `actions/checkout@v6` and `pnpm/action-setup@v5` use mutable tags. Replace with pinned SHAs: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0` and `uses: pnpm/action-setup@c3fa5a432c6e46164b9d6f89f053b5c8c0de433f # v5.x.x`. This fix ensures CI pipeline integrity. Verify by confirming the workflow executes with the pinned commits and produces the same results.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 13 | runs-on: ubuntu-latest |
| 14 | steps: |
| 15 | - uses: actions/checkout@v6 |
| 16 | - uses: pnpm/action-setup@v5 |
| 17 | - uses: actions/setup-node@v6 |
| 18 | with: |
| 19 | node-version: 24 |
RemediationAI
The problem is that `actions/checkout@v6`, `pnpm/action-setup@v5`, and `actions/setup-node@v6` use mutable tags. Replace all three with pinned SHAs: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0`, `uses: pnpm/action-setup@c3fa5a432c6e46164b9d6f89f053b5c8c0de433f # v5.x.x`, and `uses: actions/setup-node@1a4442caab129c34a6a49ec8a84507c35757b900 # v6.x.x`. This fix prevents malicious action substitution. Verify by running the workflow and confirming all steps execute with the correct pinned versions.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 30 | ref: ${{ inputs.tag || github.event.release.tag_name }} |
| 31 | |
| 32 | - name: Install pnpm |
| 33 | uses: pnpm/action-setup@v5 |
| 34 | |
| 35 | - name: Setup Node.js |
| 36 | uses: actions/setup-node@v6 |
RemediationAI
The problem is that `pnpm/action-setup@v5` and `actions/setup-node@v6` use mutable tags. Replace with pinned SHAs: `uses: pnpm/action-setup@c3fa5a432c6e46164b9d6f89f053b5c8c0de433f # v5.x.x` and `uses: actions/setup-node@1a4442caab129c34a6a49ec8a84507c35757b900 # v6.x.x`. This fix hardens the release workflow against supply-chain attacks. Verify by running the release workflow and confirming it completes with the pinned SHAs.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 18 | uses: actions/checkout@v6 |
| 19 | |
| 20 | - name: Install pnpm |
| 21 | uses: pnpm/action-setup@v5 |
| 22 | |
| 23 | - name: Setup Node.js |
| 24 | uses: actions/setup-node@v6 |
RemediationAI
The problem is that `actions/checkout@v6`, `pnpm/action-setup@v5`, and `actions/setup-node@v6` use mutable tags. Replace with pinned SHAs: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0`, `uses: pnpm/action-setup@c3fa5a432c6e46164b9d6f89f053b5c8c0de433f # v5.x.x`, and `uses: actions/setup-node@1a4442caab129c34a6a49ec8a84507c35757b900 # v6.x.x`. This fix prevents tag rewriting attacks in the fixture workflow. Verify by running the workflow and confirming it uses the pinned commits.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 14 | steps: |
| 15 | - uses: actions/checkout@v6 |
| 16 | - uses: pnpm/action-setup@v5 |
| 17 | - uses: actions/setup-node@v6 |
| 18 | with: |
| 19 | node-version: 24 |
| 20 | cache: pnpm |
RemediationAI
The problem is that `actions/checkout@v6`, `pnpm/action-setup@v5`, and `actions/setup-node@v6` use mutable tags. Replace with pinned SHAs: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0`, `uses: pnpm/action-setup@c3fa5a432c6e46164b9d6f89f053b5c8c0de433f # v5.x.x`, and `uses: actions/setup-node@1a4442caab129c34a6a49ec8a84507c35757b900 # v6.x.x`. This fix ensures benchmark workflow integrity. Verify by running the workflow and confirming all steps execute with the correct pinned versions.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 33 | uses: pnpm/action-setup@v5 |
| 34 | |
| 35 | - name: Setup Node.js |
| 36 | uses: actions/setup-node@v6 |
| 37 | with: |
| 38 | node-version: 24 |
| 39 | cache: pnpm |
RemediationAI
The problem is that `pnpm/action-setup@v5` and `actions/setup-node@v6` use mutable tags. Replace with pinned SHAs: `uses: pnpm/action-setup@c3fa5a432c6e46164b9d6f89f053b5c8c0de433f # v5.x.x` and `uses: actions/setup-node@1a4442caab129c34a6a49ec8a84507c35757b900 # v6.x.x`. This fix prevents malicious action injection in the release workflow. Verify by running the release workflow and confirming it completes with the pinned SHAs.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 25 | if: github.event_name == 'workflow_dispatch' || (startsWith(github.event.release.tag_name, 'v') && github.event.release.prerelease == false) |
| 26 | steps: |
| 27 | - name: Checkout |
| 28 | uses: actions/checkout@v6 |
| 29 | with: |
| 30 | ref: ${{ inputs.tag || github.event.release.tag_name }} |
RemediationAI
The problem is that `actions/checkout@v6` uses a mutable tag. Replace with `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0`. This fix prevents tag rewriting attacks in the release workflow. Verify by running the release workflow and confirming it uses the pinned commit SHA.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 51 | uses: pnpm/action-setup@v5 |
| 52 | |
| 53 | - name: Setup Node.js |
| 54 | uses: actions/setup-node@v6 |
| 55 | with: |
| 56 | node-version: 22 |
| 57 | cache: pnpm |
RemediationAI
The problem is that `pnpm/action-setup@v5` and `actions/setup-node@v6` use mutable tags. Replace with pinned SHAs: `uses: pnpm/action-setup@c3fa5a432c6e46164b9d6f89f053b5c8c0de433f # v5.x.x` and `uses: actions/setup-node@1a4442caab129c34a6a49ec8a84507c35757b900 # v6.x.x`. This fix hardens the CI workflow against supply-chain attacks. Verify by running the workflow and confirming it completes with the pinned SHAs.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 45 | runs-on: ubuntu-latest |
| 46 | steps: |
| 47 | - name: Checkout |
| 48 | uses: actions/checkout@v6 |
| 49 | |
| 50 | - name: Install pnpm |
| 51 | uses: pnpm/action-setup@v5 |
RemediationAI
The problem is that `actions/checkout@v6`, `pnpm/action-setup@v5`, and `actions/setup-node@v6` use mutable tags. Replace with pinned SHAs: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v6.1.0`, `uses: pnpm/action-setup@c3fa5a432c6e46164b9d6f89f053b5c8c0de433f # v5.x.x`, and `uses: actions/setup-node@1a4442caab129c34a6a49ec8a84507c35757b900 # v6.x.x`. This fix prevents malicious action substitution in CI. Verify by running the workflow and confirming all steps execute with the correct pinned versions.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 12 | benchmark: |
| 13 | runs-on: ubuntu-latest |
| 14 | steps: |
| 15 | - uses: actions/checkout@v6 |
| 16 | - uses: pnpm/action-setup@v5 |
| 17 | - uses: actions/setup-node@v6 |
| 18 | with: |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
Time-of-check-to-time-of-use race. Code calls `os.path.exists` / `fs.existsSync` to check a path, then `open` / `readFileSync` / `unlink` on the same name within a few lines โ without a lock or atomic-open. An attacker who can race the filesystem (symlink, file replacement) between the check and the use gets the action applied to a different target. Replace the check-then-use pattern with the action's own error handling: try the open and catch FileNotFoundError / ENOENT. For atomic creation use
Evidence
| 1 | import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; |
| 2 | import { spawn } from 'node:child_process'; |
| 3 | import { tmpdir } from 'node:os'; |
| 4 | import { join, resolve } from 'node:path'; |
| 5 | |
| 6 | const cli = resolve(process.env.GUARD_CLI ?? 'dist/cli/index.js'); |
| 7 | const writers = Number(process.env.GUARD_CONCURRENT_WRITERS ?? 8); |
| 8 | if (!Number.isInteger(writers) || writers < 2) |
| 9 | throw new Error('GUARD_CONCURRENT_WRITERS must be at least 2.'); |
| 10 | |
| 11 | const root = mkdtempSync(join( |
Remediation
Replace check-then-use with action-then-handle: Python: `try: with open(p) as f: ... except FileNotFoundError: ...` Node: `try { fs.readFileSync(p); } catch (e) { if (e.code === "ENOENT") ... }` For atomic file creation: Python: `os.open(p, os.O_RDWR | os.O_CREAT | os.O_EXCL)` Node: `fs.open(p, "wx")` โ fails if file exists, no race. When you genuinely must check first, use `flock` (Python `fcntl`) or a similar per-process advisory lock to make the window uninteresting to a
Time-of-check-to-time-of-use race. Code calls `os.path.exists` / `fs.existsSync` to check a path, then `open` / `readFileSync` / `unlink` on the same name within a few lines โ without a lock or atomic-open. An attacker who can race the filesystem (symlink, file replacement) between the check and the use gets the action applied to a different target. Replace the check-then-use pattern with the action's own error handling: try the open and catch FileNotFoundError / ENOENT. For atomic creation use
Evidence
| 1 | import { execFileSync } from 'node:child_process'; |
| 2 | import { createHash, randomUUID } from 'node:crypto'; |
| 3 | import { hostname } from 'node:os'; |
| 4 | import { |
| 5 | existsSync, |
| 6 | lstatSync, |
| 7 | mkdirSync, |
| 8 | readdirSync, |
| 9 | readFileSync, |
| 10 | realpathSync, |
| 11 | renameSync, |
| 12 | statSync, |
| 13 | unlinkSync, |
| 14 | writeFileSync, |
| 15 | } from 'node:fs'; |
| 16 | import { dirname, extname, relative, resolve, sep } from 'node:path'; |
| 17 | import { Project, SyntaxKind, type SourceFile } from 'ts-morph'; |
| 18 | import { buildModuleTargetGraph, type ProjectModel } |
Remediation
Replace check-then-use with action-then-handle: Python: `try: with open(p) as f: ... except FileNotFoundError: ...` Node: `try { fs.readFileSync(p); } catch (e) { if (e.code === "ENOENT") ... }` For atomic file creation: Python: `os.open(p, os.O_RDWR | os.O_CREAT | os.O_EXCL)` Node: `fs.open(p, "wx")` โ fails if file exists, no race. When you genuinely must check first, use `flock` (Python `fcntl`) or a similar per-process advisory lock to make the window uninteresting to a
Time-of-check-to-time-of-use race. Code calls `os.path.exists` / `fs.existsSync` to check a path, then `open` / `readFileSync` / `unlink` on the same name within a few lines โ without a lock or atomic-open. An attacker who can race the filesystem (symlink, file replacement) between the check and the use gets the action applied to a different target. Replace the check-then-use pattern with the action's own error handling: try the open and catch FileNotFoundError / ENOENT. For atomic creation use
Evidence
| 1 | name: Guard |
| 2 | |
| 3 | on: |
| 4 | pull_request: |
| 5 | merge_group: |
| 6 | types: [checks_requested] |
| 7 | push: |
| 8 | branches: [main] |
| 9 | |
| 10 | permissions: |
| 11 | contents: read |
| 12 | security-events: write |
| 13 | |
| 14 | concurrency: |
| 15 | group: codapult-guard-${{ github.workflow }}-${{ github.ref }} |
| 16 | cancel-in-progress: true |
| 17 | |
| 18 | jobs: |
| 19 | guard: |
| 20 | name: Architecture Guard |
| 21 | runs-on: ubuntu-latest |
| 22 | steps: |
| 23 | - name: Checkout |
| 24 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 |
| 25 | with: |
| 26 | # On pull requests, |
Remediation
Replace check-then-use with action-then-handle: Python: `try: with open(p) as f: ... except FileNotFoundError: ...` Node: `try { fs.readFileSync(p); } catch (e) { if (e.code === "ENOENT") ... }` For atomic file creation: Python: `os.open(p, os.O_RDWR | os.O_CREAT | os.O_EXCL)` Node: `fs.open(p, "wx")` โ fails if file exists, no race. When you genuinely must check first, use `flock` (Python `fcntl`) or a similar per-process advisory lock to make the window uninteresting to a