From 526200561bff1356151f95de084472488e00e2cc Mon Sep 17 00:00:00 2001 From: yashiagar2507 Date: Tue, 12 May 2026 19:49:24 +0530 Subject: [PATCH] Add GitLab CI scanner template --- docs/README.md | 1 + docs/gitlab-ci.md | 48 ++++++++++++ scanner/README.md | 6 ++ scanner/examples/gitlab/.gitlab-ci.yml | 30 ++++++++ scanner/scripts/isnad-to-gitlab-sast.mjs | 98 ++++++++++++++++++++++++ scanner/src/cli.ts | 4 +- 6 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 docs/gitlab-ci.md create mode 100644 scanner/examples/gitlab/.gitlab-ci.yml create mode 100644 scanner/scripts/isnad-to-gitlab-sast.mjs diff --git a/docs/README.md b/docs/README.md index a3cf87c3..c1da5730 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ ISNAD (إسناد) is a decentralized trust layer for AI resources. This documen - **[Jury System](./jury.md)** — How slashing and appeals work - **[API Reference](./api.md)** — REST API documentation - **[Smart Contracts](./contracts.md)** — On-chain architecture +- **[GitLab CI Integration](./gitlab-ci.md)** — Run the scanner in GitLab CI and publish SAST reports ## Getting Started diff --git a/docs/gitlab-ci.md b/docs/gitlab-ci.md new file mode 100644 index 00000000..0a21581c --- /dev/null +++ b/docs/gitlab-ci.md @@ -0,0 +1,48 @@ +# GitLab CI Integration + +ISNAD Scanner can run in GitLab CI and publish both the native scanner JSON output and a GitLab SAST report artifact. + +## Template + +Copy `scanner/examples/gitlab/.gitlab-ci.yml` into the repository that should be scanned, or include the `isnad-scan` job in an existing `.gitlab-ci.yml`. + +The template: + +- installs and builds the scanner +- scans configurable targets with `npm --silent run scan -- batch` +- writes the native ISNAD JSON report +- converts findings to GitLab SAST JSON +- publishes both reports as CI artifacts + +## Configuration + +Set these variables in GitLab CI to customize the scan: + +| Variable | Default | Description | +| --- | --- | --- | +| `ISNAD_SCAN_TARGETS` | `**/*.{js,jsx,ts,tsx,mjs,cjs,json,yml,yaml}` | Glob of files to scan. | +| `ISNAD_JSON_REPORT` | `isnad-scan-results.json` | Native ISNAD batch JSON output path. | +| `ISNAD_GITLAB_SAST_REPORT` | `gl-sast-isnad.json` | GitLab SAST report output path. | +| `ISNAD_FAIL_ON_FINDINGS` | `true` | Fails the job when the SAST report contains findings. | + +## Example + +```yaml +include: + - local: scanner/examples/gitlab/.gitlab-ci.yml + +variables: + ISNAD_SCAN_TARGETS: "skills/**/*.js" + ISNAD_FAIL_ON_FINDINGS: "false" +``` + +For projects that vendor or publish `@isnad/scanner`, adjust the job's `before_script` to install the scanner package instead of building it from the repository checkout. + +## Reports + +The job publishes: + +- `isnad-scan-results.json` for complete ISNAD scanner output +- `gl-sast-isnad.json` for GitLab's SAST report ingestion + +GitLab shows the SAST report in merge request security widgets when GitLab security report ingestion is enabled for the project. diff --git a/scanner/README.md b/scanner/README.md index 4c735c00..4fcdb0f7 100644 --- a/scanner/README.md +++ b/scanner/README.md @@ -35,6 +35,12 @@ npm run scan -- batch "./skills/**/*.js" npm run scan -- batch "./skills/**/*.js" --fail-fast ``` +### GitLab CI + +Use `scanner/examples/gitlab/.gitlab-ci.yml` to run ISNAD Scanner in GitLab CI. The template creates both native scanner JSON and GitLab SAST JSON artifacts. + +See [GitLab CI Integration](../docs/gitlab-ci.md) for setup and configuration. + ### Generate evidence ```bash diff --git a/scanner/examples/gitlab/.gitlab-ci.yml b/scanner/examples/gitlab/.gitlab-ci.yml new file mode 100644 index 00000000..bf83ee25 --- /dev/null +++ b/scanner/examples/gitlab/.gitlab-ci.yml @@ -0,0 +1,30 @@ +stages: + - security + +variables: + ISNAD_SCAN_TARGETS: "**/*.{js,jsx,ts,tsx,mjs,cjs,json,yml,yaml}" + ISNAD_JSON_REPORT: "isnad-scan-results.json" + ISNAD_GITLAB_SAST_REPORT: "gl-sast-isnad.json" + ISNAD_FAIL_ON_FINDINGS: "true" + +isnad-scan: + stage: security + image: node:20-alpine + before_script: + - npm ci + - npm run build + script: + - npm --silent run scan -- batch "$ISNAD_SCAN_TARGETS" --json > "$ISNAD_JSON_REPORT" + - node scripts/isnad-to-gitlab-sast.mjs "$ISNAD_JSON_REPORT" "$ISNAD_GITLAB_SAST_REPORT" + - | + if [ "$ISNAD_FAIL_ON_FINDINGS" = "true" ]; then + node -e "const fs=require('fs'); const r=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); process.exit(r.vulnerabilities.length ? 1 : 0)" "$ISNAD_GITLAB_SAST_REPORT" + fi + artifacts: + when: always + paths: + - "$ISNAD_JSON_REPORT" + - "$ISNAD_GITLAB_SAST_REPORT" + reports: + sast: "$ISNAD_GITLAB_SAST_REPORT" + expire_in: 1 week diff --git a/scanner/scripts/isnad-to-gitlab-sast.mjs b/scanner/scripts/isnad-to-gitlab-sast.mjs new file mode 100644 index 00000000..752e7210 --- /dev/null +++ b/scanner/scripts/isnad-to-gitlab-sast.mjs @@ -0,0 +1,98 @@ +#!/usr/bin/env node +import { readFileSync, writeFileSync } from 'node:fs'; +import { createHash } from 'node:crypto'; + +const [inputPath, outputPath = 'gl-sast-isnad.json'] = process.argv.slice(2); + +if (!inputPath) { + console.error('Usage: node scripts/isnad-to-gitlab-sast.mjs [gl-sast-isnad.json]'); + process.exit(1); +} + +const severityMap = { + critical: 'Critical', + high: 'High', + medium: 'Medium', + low: 'Low' +}; + +const confidenceMap = { + critical: 'High', + high: 'High', + medium: 'Medium', + low: 'Low' +}; + +const raw = JSON.parse(readFileSync(inputPath, 'utf8')); +const scanResults = Array.isArray(raw) ? raw : [{ file: 'unknown', result: raw }]; + +const vulnerabilities = scanResults.flatMap(({ file = 'unknown', result = {} }) => { + const findings = Array.isArray(result.findings) ? result.findings : []; + + return findings.map((finding) => { + const idSource = [ + file, + finding.patternId, + finding.line, + finding.column, + finding.match + ].join(':'); + const id = createHash('sha256').update(idSource).digest('hex'); + const severity = severityMap[finding.severity] || 'Info'; + + return { + id, + category: 'sast', + name: finding.name || 'ISNAD scanner finding', + message: finding.description || finding.name || 'ISNAD scanner finding', + description: finding.description || '', + severity, + confidence: confidenceMap[finding.severity] || 'Unknown', + scanner: { + id: 'isnad-scanner', + name: 'ISNAD Scanner' + }, + location: { + file, + start_line: finding.line || 1 + }, + identifiers: [ + { + type: 'isnad_pattern', + name: finding.patternId || 'isnad-pattern', + value: finding.patternId || 'isnad-pattern', + url: 'https://github.com/counterspec/isnad/tree/main/scanner' + } + ] + }; + }); +}); + +const report = { + version: '15.0.0', + vulnerabilities, + scan: { + analyzer: { + id: 'isnad-scanner', + name: 'ISNAD Scanner', + version: '0.1.0', + vendor: { + name: 'ISNAD' + } + }, + scanner: { + id: 'isnad-scanner', + name: 'ISNAD Scanner', + version: '0.1.0', + vendor: { + name: 'ISNAD' + } + }, + type: 'sast', + start_time: new Date().toISOString(), + end_time: new Date().toISOString(), + status: 'success' + } +}; + +writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`); diff --git a/scanner/src/cli.ts b/scanner/src/cli.ts index 5f516738..a177f9ce 100644 --- a/scanner/src/cli.ts +++ b/scanner/src/cli.ts @@ -185,7 +185,9 @@ program return; } - console.log(chalk.blue(`Scanning ${files.length} files...\n`)); + if (!options.json) { + console.log(chalk.blue(`Scanning ${files.length} files...\n`)); + } const results: { file: string; result: AnalysisResult }[] = []; let hasHighRisk = false;