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
14 changes: 14 additions & 0 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4583,6 +4583,20 @@ The ids in this example show the field shape. A real scan fills them from the ad

The same package can also carry `CVE-2024-1111` as its own advisory, with alias `GHSA-bbbb`. That is a second result. Its `properties.advisoryId` is `CVE-2024-1111`, its message names that CVE on its own (the id is already the advisory id), and `properties.aliases` is `["GHSA-bbbb"]`.

#### Severity and `level`

Each vulnerability result's `level` follows the advisory severity, which the result also carries in `properties.severity`:

| `properties.severity` | `level` |
| --------------------- | ------- |
| `critical` | `error` |
| `high` | `error` |
| `moderate` | `warning` |
| `low` | `note` |
| `unknown` | `note` |

`unknown` means the advisory data gave no usable severity, for example no severity label and no CVSS score. It gets `note`, the same as `low`, so an unscored advisory never outranks a scored one. Read `properties.severity` to tell the two apart. The `vibgrate/vulnerability` rule carries no `security-severity` property, so GitHub code scanning shows these alerts as Error, Warning, or Note from `level`.

Test reporters that ingest JUnit can take a companion file from the same scan. See [JUnit](#junit). The process exit code is unchanged either way; see [Exit Codes](#exit-codes).

### Markdown
Expand Down
73 changes: 73 additions & 0 deletions src/reporting/formatters/sarif-vulnerability-level.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// `vg scan --vulns --format sarif` sets each vulnerability result's `level`
// from the advisory severity. GitHub code scanning uses `level` to decide how
// loudly to show an alert, so the mapping is pinned here and in the SARIF
// section of DOCS.md. Advisories come from an offline manifest: no network.
import { describe, expect, it } from 'vitest';
import { formatSarif } from '../../core-open/formatters/sarif.js';
import type { ManifestAdvisory, PackageVersionManifest } from '../../core-open/package-version-manifest.js';
import { generateVulnerabilityFindings, scanVulnerabilities } from '../../core-open/scanners/vulnerability-scanner.js';
import type { ScanArtifact } from '../../core-open/types.js';
import { Semaphore } from '../../core-open/utils/semaphore.js';

const advisory = (id: string, fields: Partial<ManifestAdvisory>): ManifestAdvisory => ({ id, versions: ['1.0.0'], ...fields });

const manifest: PackageVersionManifest = {
npm: {
'left-pad': {
vulns: [
advisory('VG-TEST-LOW', { severity: 'low' }),
advisory('VG-TEST-UNSCORED', {}),
advisory('VG-TEST-CRITICAL', { severity: 'critical' }),
advisory('VG-TEST-MODERATE', { severity: 'moderate' }),
advisory('VG-TEST-CVSS-ONLY', { cvss: 7.5 }),
advisory('VG-TEST-HIGH', { severity: 'high' }),
],
},
},
};

async function sarifResults() {
const scan = await scanVulnerabilities([{ ecosystem: 'npm', package: 'left-pad', version: '1.0.0' }], {
sem: new Semaphore(1),
offline: true,
manifest,
});
const artifact = { findings: generateVulnerabilityFindings(scan) } as unknown as ScanArtifact;
const sarif = formatSarif(artifact) as {
runs: Array<{ results: Array<{ ruleId: string; level: string; properties: { advisoryId: string; severity: string } }> }>;
};
return sarif.runs[0].results;
}

describe('SARIF level for vulnerability findings', () => {
it('maps critical and high to error, moderate to warning, low and unscored to note', async () => {
const rows = (await sarifResults()).map((r) => ({
advisoryId: r.properties.advisoryId,
severity: r.properties.severity,
level: r.level,
}));

expect(rows).toEqual([
{ advisoryId: 'VG-TEST-CRITICAL', severity: 'critical', level: 'error' },
{ advisoryId: 'VG-TEST-CVSS-ONLY', severity: 'high', level: 'error' },
{ advisoryId: 'VG-TEST-HIGH', severity: 'high', level: 'error' },
{ advisoryId: 'VG-TEST-MODERATE', severity: 'moderate', level: 'warning' },
{ advisoryId: 'VG-TEST-LOW', severity: 'low', level: 'note' },
{ advisoryId: 'VG-TEST-UNSCORED', severity: 'unknown', level: 'note' },
]);
});

it('keeps the unscored advisory distinguishable from a low one', async () => {
const results = await sarifResults();
const low = results.find((r) => r.properties.advisoryId === 'VG-TEST-LOW');
const unscored = results.find((r) => r.properties.advisoryId === 'VG-TEST-UNSCORED');

expect(low?.level).toBe(unscored?.level);
expect(low?.properties.severity).toBe('low');
expect(unscored?.properties.severity).toBe('unknown');
});

it('produces the same results on every run', async () => {
expect(JSON.stringify(await sarifResults())).toBe(JSON.stringify(await sarifResults()));
});
});
Loading