Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
48 changes: 48 additions & 0 deletions docs/gitlab-ci.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions scanner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions scanner/examples/gitlab/.gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -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
98 changes: 98 additions & 0 deletions scanner/scripts/isnad-to-gitlab-sast.mjs
Original file line number Diff line number Diff line change
@@ -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 <isnad-results.json> [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`);
4 changes: 3 additions & 1 deletion scanner/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down