From 0a0e586fd10cff0310ed8fd26cedbdbaafc01362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20D=C3=A1vid?= Date: Sat, 27 Jun 2026 15:39:21 +0200 Subject: [PATCH 1/2] Add CodeQL SAST workflow --- .github/codeql/codeql-config.yml | 20 +++++++ .github/workflows/codeql.yml | 93 ++++++++++++++++++++++++++++++++ docs/security.md | 32 +++++++++++ 3 files changed, 145 insertions(+) create mode 100644 .github/codeql/codeql-config.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 docs/security.md diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 0000000..cf5574d --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,20 @@ +name: TariffShield CodeQL configuration + +queries: + - uses: security-and-quality + +paths: + - apps/api/src + - apps/web + - packages/sdk/src + - scripts + +paths-ignore: + - "**/node_modules/**" + - "**/dist/**" + - "**/build/**" + - "**/.next/**" + - "**/coverage/**" + - "**/generated/**" + - "**/*.generated.ts" + - "**/*.generated.tsx" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..d159707 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,93 @@ +name: CodeQL + +on: + pull_request: + branches: [main] + schedule: + - cron: "0 3 * * 1" + workflow_dispatch: + +permissions: + actions: read + contents: read + security-events: write + +jobs: + analyze: + name: Analyze JavaScript and TypeScript + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: javascript-typescript + config-file: ./.github/codeql/codeql-config.yml + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL analysis + id: analyze + uses: github/codeql-action/analyze@v3 + with: + category: "/language:javascript-typescript" + output: codeql-results + + - name: Fail on blocking CodeQL findings + shell: bash + run: | + node <<'NODE' + const fs = require('fs'); + const path = require('path'); + + const sarifDir = process.env.SARIF_DIR || 'codeql-results'; + if (!fs.existsSync(sarifDir)) { + console.log(`No SARIF output directory found at ${sarifDir}; skipping severity gate.`); + process.exit(0); + } + + const sarifFiles = fs.readdirSync(sarifDir) + .filter((file) => file.endsWith('.sarif')) + .map((file) => path.join(sarifDir, file)); + + let blocking = 0; + let warnings = 0; + + for (const sarifFile of sarifFiles) { + const sarif = JSON.parse(fs.readFileSync(sarifFile, 'utf8')); + for (const run of sarif.runs || []) { + for (const result of run.results || []) { + const securitySeverity = Number(result.properties?.['security-severity'] || 0); + if (result.level === 'error' || securitySeverity >= 9) { + blocking += 1; + } else if (result.level === 'warning') { + warnings += 1; + } + } + } + } + + console.log(`CodeQL warnings: ${warnings}`); + console.log(`CodeQL blocking findings: ${blocking}`); + + if (blocking > 0) { + console.error('CodeQL found error-level or critical-severity results. Review the code scanning alerts before merging.'); + process.exit(1); + } + NODE + env: + SARIF_DIR: ${{ steps.analyze.outputs.sarif-output }} diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..0dcb4ff --- /dev/null +++ b/docs/security.md @@ -0,0 +1,32 @@ +# Security scanning + +TariffShield uses GitHub CodeQL to scan the JavaScript and TypeScript codebase for security and code-quality issues. + +## CodeQL coverage + +The CodeQL workflow runs for pull requests targeting `main`, on a weekly schedule at `03:00 UTC` every Monday, and on manual dispatch. It analyzes JavaScript and TypeScript with the `security-and-quality` query suite. + +The scan covers the API, web app, SDK, and TypeScript scripts: + +- `apps/api/src` +- `apps/web` +- `packages/sdk/src` +- `scripts` + +Generated and build outputs are excluded through `.github/codeql/codeql-config.yml`, including `node_modules`, `dist`, `build`, `.next`, coverage output, and generated TypeScript files. + +## Triage policy + +CodeQL findings with `error` level or critical security severity are release blockers. The workflow reads the generated SARIF report after analysis and fails the pull request check when those blocking findings are present. Maintainers should also require the CodeQL check before merging protected branches and should fix or explicitly dismiss those alerts in GitHub code scanning before a pull request is merged. + +Warning-level findings should be reviewed from the pull request annotations and the repository Security tab. They do not block merge by default, but they should be marked as fixed, false positive, or accepted risk during triage. + +## Reviewing alerts + +1. Open the repository Security tab. +2. Select Code scanning alerts. +3. Filter by tool `CodeQL` and sort by severity. +4. Assign each alert to the relevant owner. +5. Close the alert only after the fix is merged or after the maintainer records why the finding is not exploitable. + +If a CodeQL alert identifies a credential exposure, authentication bypass, injection path, or unsafe cryptographic use, treat it as a security incident and follow `docs/security/incident-response-playbook.md`. From 8cd46a62f7ed17e436b36892627ca42803390473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20D=C3=A1vid?= Date: Sat, 27 Jun 2026 19:27:50 +0200 Subject: [PATCH 2/2] Read CodeQL severity from SARIF rules --- .github/workflows/codeql.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d159707..416b903 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -70,8 +70,12 @@ jobs: for (const sarifFile of sarifFiles) { const sarif = JSON.parse(fs.readFileSync(sarifFile, 'utf8')); for (const run of sarif.runs || []) { + const rules = run.tool?.driver?.rules || []; for (const result of run.results || []) { - const securitySeverity = Number(result.properties?.['security-severity'] || 0); + const rule = Number.isInteger(result.ruleIndex) + ? rules[result.ruleIndex] + : rules.find((candidate) => candidate.id === result.ruleId); + const securitySeverity = Number(rule?.properties?.['security-severity'] || 0); if (result.level === 'error' || securitySeverity >= 9) { blocking += 1; } else if (result.level === 'warning') {