Skip to content
Merged
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
162 changes: 162 additions & 0 deletions .github/workflows/security-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
name: code-to-gate Security Gate

on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: security-gate-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
security-gate:
runs-on: ubuntu-24.04
timeout-minutes: 25
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .node-version
cache: npm

- name: Verify locked security toolchain
run: node scripts/verify-security-toolchain.mjs

- name: Use locked npm and install without lifecycle scripts
shell: bash
run: |
set -euo pipefail
NPM_VERSION="$(jq -r '.tools.npm.version' security/toolchain-lock.json)"
if [ "$(npm --version)" != "$NPM_VERSION" ]; then
npm install --global "npm@$NPM_VERSION"
fi
test "$(npm --version)" = "$NPM_VERSION"
npm ci --ignore-scripts

- name: Install verified Gitleaks binary
shell: bash
run: |
set -euo pipefail
mkdir -p .security-tools
GITLEAKS_VERSION="$(jq -r '.tools.gitleaks.version' security/toolchain-lock.json)"
GITLEAKS_URL="$(jq -r '.tools.gitleaks.archive.url' security/toolchain-lock.json)"
GITLEAKS_SHA="$(jq -r '.tools.gitleaks.archive.sha256' security/toolchain-lock.json)"
CHECKSUMS_URL="$(jq -r '.tools.gitleaks.checksums.url' security/toolchain-lock.json)"
CHECKSUMS_SHA="$(jq -r '.tools.gitleaks.checksums.sha256' security/toolchain-lock.json)"

curl --proto '=https' --tlsv1.2 --fail --location --retry 3 "$GITLEAKS_URL" --output .security-tools/gitleaks.tar.gz
curl --proto '=https' --tlsv1.2 --fail --location --retry 3 "$CHECKSUMS_URL" --output .security-tools/gitleaks.checksums.txt

echo "$GITLEAKS_SHA .security-tools/gitleaks.tar.gz" | sha256sum --check --strict
echo "$CHECKSUMS_SHA .security-tools/gitleaks.checksums.txt" | sha256sum --check --strict
grep -F "$GITLEAKS_SHA" .security-tools/gitleaks.checksums.txt
tar -xzf .security-tools/gitleaks.tar.gz -C .security-tools gitleaks
test "$(.security-tools/gitleaks version)" = "$GITLEAKS_VERSION"

- name: Run npm audit and generate CycloneDX SBOM
id: dependencies
shell: bash
run: |
set -euo pipefail
mkdir -p .qh/security
AUDIT_LEVEL="$(jq -r '.tools.npm.auditLevel' security/toolchain-lock.json)"
set +e
npm audit --audit-level="$AUDIT_LEVEL" --json > .qh/security/npm-audit.json
AUDIT_EXIT=$?
set -e
echo "$AUDIT_EXIT" > .qh/security/npm-audit.exit
test -s .qh/security/npm-audit.json
jq -e '.metadata.vulnerabilities != null' .qh/security/npm-audit.json

npm sbom --sbom-format cyclonedx --sbom-type application > .qh/security/sbom.cdx.json
jq -e '.bomFormat == "CycloneDX" and (.specVersion | type == "string") and (.components | type == "array")' .qh/security/sbom.cdx.json

- name: Run Semgrep with a digest-pinned offline container
id: semgrep
shell: bash
run: |
set -euo pipefail
SEMGREP_IMAGE="$(jq -r '.tools.semgrep.image' security/toolchain-lock.json)"
set +e
docker run --rm --platform linux/amd64 --network none --read-only --cap-drop ALL --security-opt no-new-privileges --pids-limit 256 --memory 2g --cpus 2 --tmpfs /tmp:rw,noexec,nosuid,size=128m --env HOME=/tmp --volume "$PWD:/src:ro" --workdir /src "$SEMGREP_IMAGE" semgrep scan --config .semgrep/security.yml --metrics=off --error --jobs 2 --timeout 30 --max-memory 1500 --exclude .qh --exclude .security-tools --json . > .qh/security/semgrep.json
SEMGREP_EXIT=$?
set -e
echo "$SEMGREP_EXIT" > .qh/security/semgrep.exit
test -s .qh/security/semgrep.json
jq -e '.results | type == "array"' .qh/security/semgrep.json

- name: Run Gitleaks across repository history
id: gitleaks
shell: bash
run: |
set -euo pipefail
set +e
.security-tools/gitleaks git --redact=100 --no-banner --report-format json --report-path .qh/security/gitleaks.json --exit-code 13
GITLEAKS_EXIT=$?
set -e
echo "$GITLEAKS_EXIT" > .qh/security/gitleaks.exit
if [ ! -s .qh/security/gitleaks.json ]; then
echo '[]' > .qh/security/gitleaks.json
fi
jq -e 'type == "array"' .qh/security/gitleaks.json

- name: Run security golden regression
shell: bash
run: |
set -euo pipefail
node scripts/security-golden.mjs prepare .qh/security/golden
SEMGREP_IMAGE="$(jq -r '.tools.semgrep.image' security/toolchain-lock.json)"

set +e
docker run --rm --platform linux/amd64 --network none --read-only --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --tmpfs /tmp:rw,noexec,nosuid,size=64m --env HOME=/tmp --volume "$PWD:/src:ro" --workdir /src "$SEMGREP_IMAGE" semgrep scan --config .semgrep/security.yml --metrics=off --error --no-git-ignore --json .qh/security/golden > .qh/security/golden-semgrep.json
GOLDEN_SEMGREP_EXIT=$?

.security-tools/gitleaks dir .qh/security/golden --redact=100 --no-banner --report-format json --report-path .qh/security/golden-gitleaks.json --exit-code 13
GOLDEN_GITLEAKS_EXIT=$?
set -e

test "$GOLDEN_SEMGREP_EXIT" -eq 1
test "$GOLDEN_GITLEAKS_EXIT" -eq 13
node scripts/security-golden.mjs verify .qh/security/golden-semgrep.json .qh/security/golden-gitleaks.json

- name: Upload security evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ctg-security-evidence-${{ github.sha }}
path: |
.qh/security/npm-audit.json
.qh/security/npm-audit.exit
.qh/security/sbom.cdx.json
.qh/security/semgrep.json
.qh/security/semgrep.exit
.qh/security/gitleaks.json
.qh/security/gitleaks.exit
.qh/security/golden-semgrep.json
.qh/security/golden-gitleaks.json
security/toolchain-lock.json
include-hidden-files: true
if-no-files-found: warn
retention-days: 90

- name: Enforce security gate
shell: bash
run: |
set -euo pipefail
test "$(cat .qh/security/npm-audit.exit)" -eq 0
test "$(cat .qh/security/semgrep.exit)" -eq 0
test "$(cat .qh/security/gitleaks.exit)" -eq 0
echo "Security gate passed: npm audit, Semgrep, Gitleaks, CycloneDX SBOM, and golden regression."
3 changes: 3 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
692bb21c2c9fdd5334066b88f38c508ac5c79a36:src/rules/__tests__/hardcoded-secret.test.ts:stripe-access-token:39
910f4f47882542c0e3a69e2f489dc86fa2e4af50:src/cli/__tests__/llm-trust.test.ts:generic-api-key:357
9d56518274e6ca52bf24a7571c3c12915c18da2d:plugins/example-custom-rule/src/index.js:generic-api-key:112
22 changes: 22 additions & 0 deletions .semgrep/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
rules:
- id: ctg.javascript.no-eval
message: Avoid eval(); untrusted input can become arbitrary code execution.
severity: ERROR
languages: [javascript, typescript]
pattern: eval(...)

- id: ctg.javascript.no-function-constructor
message: Avoid the Function constructor; it evaluates strings as code.
severity: ERROR
languages: [javascript, typescript]
pattern: new Function(...)

- id: ctg.javascript.no-disabled-tls-verification
message: TLS certificate verification must not be disabled.
severity: ERROR
languages: [javascript, typescript]
pattern-either:
- pattern: |
$CLIENT.request({..., rejectUnauthorized: false, ...}, ...)
- pattern: |
$CLIENT.get({..., rejectUnauthorized: false, ...}, ...)
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ readiness:
| [Quickstart](docs/quickstart.md) | First run and CI setup |
| [Distribution Status](docs/distribution-status.md) | Package, GitHub release, and npm publication state |
| [CLI Reference](docs/cli-reference.md) | Commands, flags, output formats |
| [Security Gate](docs/security-gate.md) | Pinned scanners, SBOM, audit, and CI evidence |
| [Policy Guide](docs/policy-guide.md) | Gate policy configuration |
| [Integrations](docs/integrations.md) | GitHub Actions and downstream exports |
| [Plugin Development](docs/plugin-development.md) | Custom rule SDK |
Expand Down
1 change: 1 addition & 0 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ should use `ctg/v1`.
| [docs/quickstart.md](docs/quickstart.md) | First-run guide |
| [docs/distribution-status.md](docs/distribution-status.md) | Package, GitHub release, and npm publication state |
| [docs/cli-reference.md](docs/cli-reference.md) | CLI details |
| [docs/security-gate.md](docs/security-gate.md) | Pinned scanners, SBOM, audit, and CI evidence |
| [docs/policy-guide.md](docs/policy-guide.md) | Gate policy configuration |
| [docs/integrations.md](docs/integrations.md) | Tool integrations |
| [docs/plugin-development.md](docs/plugin-development.md) | Plugin development |
Expand Down
1 change: 1 addition & 0 deletions README_JA.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ readiness:
| [docs/quickstart.md](docs/quickstart.md) | 初回実行ガイド |
| [docs/distribution-status.md](docs/distribution-status.md) | package / GitHub release / npm 公開状態 |
| [docs/cli-reference.md](docs/cli-reference.md) | CLI 詳細 |
| [docs/security-gate.md](docs/security-gate.md) | Scanner固定、SBOM、監査、CI証跡 |
| [docs/policy-guide.md](docs/policy-guide.md) | Gate policy 設定 |
| [docs/integrations.md](docs/integrations.md) | CI / 外部連携 |
| [docs/plugin-development.md](docs/plugin-development.md) | Plugin 開発 |
Expand Down
29 changes: 24 additions & 5 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ code-to-gate scan <repo-path> --out <output-dir>

Note: current CLI help does not expose `--lang`, `--languages`, `--ignore`, or `--exclude`; language detection and default exclusions are handled by the scanner.

Repository discovery is bounded to 100,000 files, depth 64, 10 MiB per
file, 2 GiB total accepted bytes, and 15 minutes. Exceeding a limit, a read
failure, or deadline exhaustion marks `repo-graph.stats.partial`, records the
limit and reason in `repo-graph.stats.scan`, and propagates incomplete input
to findings/readiness instead of silently succeeding.

**Default Exclusions:**

The scanner automatically excludes these directories:
Expand Down Expand Up @@ -327,19 +333,22 @@ Import results from external analysis tools and convert to normalized findings.

**Usage:**
```bash
code-to-gate import <tool> <file> --out <output-dir>
code-to-gate import <tool> <file> --out <output-dir> [--repo-root <dir>] [--max-input-mb <number>] [--producer-version <version>]
```

**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `<tool>` | Yes | Tool name: `semgrep`, `eslint`, `sarif`, `codeql`, `tsc`, `coverage`, `test` |
| `<tool>` | Yes | Tool name: `semgrep`, `eslint`, `sarif`, `codeql`, `npm-audit`, `tsc`, `coverage`, `test` |
| `<file>` | Yes | Path to the tool output file |

**Options:**
| Option | Default | Description |
|--------|---------|-------------|
| `--out <dir>` | `.qh/imports` | Output directory for imported findings |
| `--out <dir>` | `.qh/imports` | Output directory for imported findings and manifest |
| `--repo-root <dir>` | current directory | Root used for path containment, source hashes, and exact Git revision |
| `--max-input-mb <n>` | `50` | Maximum input size in MiB; hard cap is 1024 |
| `--producer-version <v>` | unknown | Upstream producer version recorded in provenance |

**Supported Tools:**
| Tool | Input Format | Notes |
Expand All @@ -348,12 +357,16 @@ code-to-gate import <tool> <file> --out <output-dir>
| `eslint` | JSON formatter output | Code quality and style findings |
| `sarif` | SARIF 2.1.0 | Generic SARIF result import |
| `codeql` | CodeQL SARIF 2.1.0 | CodeQL result import with SARIF severity metadata |
| `npm-audit` | `npm audit --json` | Dependency advisories and affected package paths |
| `tsc` | TypeScript diagnostics JSON | Type errors and warnings |
| `coverage` | Istanbul/nyc coverage-summary.json | Coverage metrics and gaps |
| `test` | Generic test result JSON | Failed tests as testing findings |

**Output:**
- `.qh/imports/<tool>-findings.json` - Normalized findings from the external tool
- `.qh/imports/<tool>-findings.json` - Schema-validated normalized findings
- `.qh/imports/<tool>-import-manifest.json` - Input/output SHA-256, producer metadata, exact repository revision, diagnostics, and completeness

Both files are staged before replacement; existing outputs are backed up for rollback, and the manifest is committed last as the pair marker. Downstream `analyze --from-imports` revalidates the manifest, artifact hash, tool identity, repository revision, source-file hash, and finding IDs. Legacy manifest-less files remain readable but are marked partial.

**Example:**
```bash
Expand All @@ -372,14 +385,20 @@ code-to-gate import tsc ./tsc-errors.json --out .qh/imports

# Import coverage summary
code-to-gate import coverage ./coverage-summary.json --out .qh/imports

# Import npm audit with repository-bound provenance
npm audit --json > npm-audit.json
code-to-gate import npm-audit ./npm-audit.json --repo-root . --out .qh/imports
```

**Exit Codes:**
| Code | Name | Description |
|------|------|-------------|
| 0 | OK | Import completed successfully |
| 2 | USAGE_ERROR | Invalid arguments or tool name |
| 8 | IMPORT_FAILED | Failed to parse or process input file |
| 7 | SCHEMA_FAILED | Findings or import manifest failed schema validation |
| 8 | IMPORT_FAILED | Failed to parse/process input, exceeded size limits, or violated path constraints |
| 12 | PARTIAL_SUCCESS | Import succeeded with dropped or unsupported records; manifest records diagnostics |

---

Expand Down
Loading