Ran repo-standard v0.1.1 against this repo (class: plugin, trait: security). 6 ERROR, 4 WARN, 1 SKIP recorded before any edit; now 0 ERROR, 12 checks pass. BROKEN - INSTALL-NO-CLI: added `claude plugin install llm-security@ktg-plugin-marketplace` beside the existing `marketplace add` line. The settings.json `enabledPlugins` block stays — it is a legitimate second form, just not a CLI command. - LINK-OUTSIDE-REPO (README:7, reported by catalog): the disclosure link pointed at `../../README.md#ai-generated-code-disclosure`, relative to the pre-split monorepo and anchored at a heading that never existed. Replaced with the inline text the polyrepo migration's step 5 was meant to write. MISSING - `## Non-goals`: added as its own heading over the existing out-of-scope table inside `## Project scope`. The `## Project scope` heading is kept because SECURITY.md and CONTRIBUTING.md reference it by name. - `## Changelog`: promoted from the trailing "Full history in CHANGELOG.md" line. - `## Known limitations` (required by the `security` trait): renamed from `## What this plugin does NOT cover` — same table, contract heading. WEAKENING - README-DESC: opening line now matches the forge description verbatim, so description == catalog == README holds. This replaced the tagline "Automated defense and advisory analysis for the agentic AI attack surface." - HEADING-LEVEL: `### Install` promoted to `## Install`; `## Quick Start` split into `## Requirements` / `## Install` / `## First scan`. - BADGE-STATIC-CLAIM: dropped the static `tests-2034` badge. No runner exists on this forge, so the badge asserted a run nothing performs. Replaced under `## Self-scan` with the command that runs the suite from a clean clone, and the plain statement that nothing runs it automatically. - LINK-NON-REPO x2: `open/claude-code-llm-security` (pre-split name) in V3-ANNOUNCEMENT.md updated to `open/llm-security`. Same dead name found in sarif-formatter.mjs TOOL_URI, which ships into SARIF output consumed by other tools, so it is corrected too. Left standing, deliberately: - WARN README-H1 `# LLM Security Plugin for Claude Code` != `# llm-security`. The gate defers this to the operator, and the description thread it protects is now carried by the opening line instead. Suite 2034/2034. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016h37aUdBLVDT9xrvG9osA9
129 lines
3.3 KiB
JavaScript
129 lines
3.3 KiB
JavaScript
// sarif-formatter.mjs — Converts scan-orchestrator envelope to SARIF 2.1.0
|
|
// OASIS SARIF standard: https://docs.oasis-open.org/sarif/sarif/v2.1.0/
|
|
// Zero external dependencies.
|
|
|
|
const SARIF_SCHEMA = 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json';
|
|
const SARIF_VERSION = '2.1.0';
|
|
const TOOL_NAME = 'llm-security';
|
|
const TOOL_URI = 'https://git.fromaitochitta.com/open/llm-security';
|
|
|
|
/**
|
|
* Map finding severity to SARIF level.
|
|
* @param {string} severity - critical|high|medium|low|info
|
|
* @returns {string} SARIF level: error|warning|note
|
|
*/
|
|
function toLevel(severity) {
|
|
switch (severity) {
|
|
case 'critical':
|
|
case 'high':
|
|
return 'error';
|
|
case 'medium':
|
|
return 'warning';
|
|
case 'low':
|
|
case 'info':
|
|
default:
|
|
return 'note';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build SARIF rules array from unique finding scanner+title combos.
|
|
* @param {object[]} findings
|
|
* @returns {{ rules: object[], ruleIndex: Map<string, number> }}
|
|
*/
|
|
function buildRules(findings) {
|
|
const ruleIndex = new Map();
|
|
const rules = [];
|
|
|
|
for (const f of findings) {
|
|
const ruleId = `${f.scanner}/${f.title.replace(/\s+/g, '-').toLowerCase()}`;
|
|
if (!ruleIndex.has(ruleId)) {
|
|
ruleIndex.set(ruleId, rules.length);
|
|
rules.push({
|
|
id: ruleId,
|
|
name: f.title,
|
|
shortDescription: { text: f.title },
|
|
fullDescription: { text: f.description || f.title },
|
|
defaultConfiguration: { level: toLevel(f.severity) },
|
|
properties: {
|
|
tags: f.owasp ? [f.owasp] : [],
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
return { rules, ruleIndex };
|
|
}
|
|
|
|
/**
|
|
* Convert scan-orchestrator envelope JSON to SARIF 2.1.0 format.
|
|
* @param {object} envelopeData - The full scan-orchestrator output
|
|
* @param {string} [version='6.0.0'] - Tool version
|
|
* @returns {object} SARIF 2.1.0 JSON
|
|
*/
|
|
export function toSARIF(envelopeData, version = '6.0.0') {
|
|
// Collect all findings from all scanners
|
|
const allFindings = [];
|
|
if (envelopeData.scanners) {
|
|
for (const scannerResult of Object.values(envelopeData.scanners)) {
|
|
if (scannerResult.findings) {
|
|
allFindings.push(...scannerResult.findings);
|
|
}
|
|
}
|
|
}
|
|
|
|
const { rules, ruleIndex } = buildRules(allFindings);
|
|
|
|
// Build SARIF results
|
|
const results = allFindings.map(f => {
|
|
const ruleId = `${f.scanner}/${f.title.replace(/\s+/g, '-').toLowerCase()}`;
|
|
const result = {
|
|
ruleId,
|
|
ruleIndex: ruleIndex.get(ruleId),
|
|
level: toLevel(f.severity),
|
|
message: { text: f.description || f.title },
|
|
properties: {},
|
|
};
|
|
|
|
// Add OWASP tags
|
|
if (f.owasp) {
|
|
result.properties.tags = [f.owasp];
|
|
}
|
|
|
|
// Add recommendation
|
|
if (f.recommendation) {
|
|
result.properties.recommendation = f.recommendation;
|
|
}
|
|
|
|
// Add location if file is present
|
|
if (f.file) {
|
|
const location = {
|
|
physicalLocation: {
|
|
artifactLocation: { uri: f.file },
|
|
},
|
|
};
|
|
if (f.line) {
|
|
location.physicalLocation.region = { startLine: f.line };
|
|
}
|
|
result.locations = [location];
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
return {
|
|
$schema: SARIF_SCHEMA,
|
|
version: SARIF_VERSION,
|
|
runs: [{
|
|
tool: {
|
|
driver: {
|
|
name: TOOL_NAME,
|
|
version,
|
|
informationUri: TOOL_URI,
|
|
rules,
|
|
},
|
|
},
|
|
results,
|
|
}],
|
|
};
|
|
}
|