@blundergoat/goat-flow
Version:
AI coding agent harness and local dashboard for Claude Code, OpenAI Codex, Google Antigravity, and GitHub Copilot - setup audits, guardrails, structured skills, deny hooks, and persistent learning loops.
310 lines • 11.4 kB
JavaScript
/**
* SARIF 2.1.0 renderer for `goat-flow audit`.
*
* This is a renderer-only export: it maps the existing AuditReport contract to
* SARIF without changing audit status, scoring, or check semantics.
*/
import { AUDIT_VERSION } from "../constants.js";
const SARIF_SCHEMA_URI = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
const TOOL_INFORMATION_URI = "https://github.com/blundergoat/goat-flow";
const ACKNOWLEDGED_SUPPRESSION = "Acknowledged by goat-flow harness configuration.";
const SCOPE_ORDER = {
setup: 0,
agent: 1,
harness: 2,
drift: 3,
content: 4,
};
const DRIFT_RULE_DESCRIPTIONS = {
content: "Installed skill content differs from the goat-flow template.",
missing: "A goat-flow skill mirror expected in the target project is missing.",
orphan: "A goat-flow skill mirror exists without a matching current template.",
deprecated: "A deprecated goat-flow skill mirror exists in the target project.",
};
/**
* Render an AuditReport as a SARIF 2.1.0 JSON string.
*
* @param report - Completed goat-flow audit report to project into SARIF.
* @returns Pretty-printed SARIF JSON ready for stdout or `--output`.
*/
export function renderAuditSarif(report) {
return JSON.stringify(buildAuditSarifLog(report), null, 2);
}
/**
* Build the SARIF log object before JSON serialization.
*
* Invariant: rule and result order must stay deterministic because CI uploads
* and code-scanning baselines diff SARIF across runs.
*/
function buildAuditSarifLog(report) {
const rules = new Map();
const results = [];
collectScope(rules, results, "setup", report.scopes.setup);
collectScope(rules, results, "agent", report.scopes.agent);
if (report.scopes.harness) {
collectScope(rules, results, "harness", report.scopes.harness);
}
if (report.drift) {
collectDrift(rules, results, report.drift.findings);
}
if (report.content) {
collectContent(rules, results, report.content.findings);
}
const orderedRules = [...rules.values()]
.sort(compareRuleRegistrations)
.map((registration) => registration.descriptor);
const orderedResults = results.sort(compareResults);
return {
$schema: SARIF_SCHEMA_URI,
version: "2.1.0",
runs: [
{
tool: {
driver: {
name: "goat-flow",
informationUri: TOOL_INFORMATION_URI,
semanticVersion: AUDIT_VERSION,
rules: orderedRules,
},
},
results: orderedResults,
},
],
};
}
function collectScope(rules, results, scope, auditScope) {
for (const check of auditScope.checks) {
registerRule(rules, scope, ruleFromCheck(scope, check));
if (check.status !== "fail" || !check.failure)
continue;
results.push(resultFromCheck(scope, check, check.failure));
}
}
function collectDrift(rules, results, findings) {
for (const kind of Object.keys(DRIFT_RULE_DESCRIPTIONS)) {
const id = driftRuleId(kind);
registerRule(rules, "drift", {
id,
name: `Skill template drift: ${kind}`,
shortDescription: { text: DRIFT_RULE_DESCRIPTIONS[kind] },
properties: {
scope: "drift",
kind,
},
});
}
for (const finding of findings) {
const ruleId = driftRuleId(finding.kind);
const locations = locationsFromPath(finding.path);
results.push({
ruleId,
level: "error",
message: { text: finding.message },
...(locations.length > 0 ? { locations } : {}),
partialFingerprints: fingerprintFor("drift", ruleId, locationUri(locations), finding.message),
properties: {
scope: "drift",
kind: finding.kind,
path: finding.path,
},
});
}
}
function collectContent(rules, results, findings) {
for (const finding of findings) {
const ruleId = contentRuleId(finding.rule);
registerRule(rules, "content", {
id: ruleId,
name: `Content lint: ${finding.rule}`,
shortDescription: {
text: `Cold-path content lint rule ${finding.rule}.`,
},
properties: {
scope: "content",
contentRule: finding.rule,
},
});
const locations = locationsFromPath(finding.path, finding.line);
results.push({
ruleId,
level: finding.severity === "warning" ? "warning" : "note",
message: { text: finding.message },
...(locations.length > 0 ? { locations } : {}),
partialFingerprints: fingerprintFor("content", ruleId, locationUri(locations), finding.message),
properties: {
scope: "content",
severity: finding.severity,
contentRule: finding.rule,
path: finding.path,
...(finding.line !== undefined ? { line: finding.line } : {}),
...(finding.suggestion ? { suggestion: finding.suggestion } : {}),
},
});
}
}
function registerRule(rules, scope, descriptor) {
if (rules.has(descriptor.id))
return;
rules.set(descriptor.id, { scope, descriptor });
}
function ruleFromCheck(scope, check) {
return {
id: check.id,
name: check.name,
shortDescription: { text: check.name },
helpUri: check.provenance.source_urls[0],
properties: {
scope,
impact: check.impact,
status: check.status,
displayStatus: check.displayStatus,
provenance: check.provenance,
...(check.type ? { type: check.type } : {}),
...(check.evidenceKind ? { evidenceKind: check.evidenceKind } : {}),
...(check.assurance ? { assurance: check.assurance } : {}),
},
};
}
function resultFromCheck(scope, check, failure) {
const locations = locationsFromCheck(check);
const messageText = failure.message;
return {
ruleId: check.id,
level: levelFromImpact(check.impact),
message: { text: messageText },
...(locations.length > 0 ? { locations } : {}),
...(check.acknowledged === true
? {
suppressions: [
{
kind: "external",
justification: ACKNOWLEDGED_SUPPRESSION,
},
],
}
: {}),
partialFingerprints: fingerprintFor(scope, check.id, locationUri(locations), messageText),
properties: {
scope,
status: check.status,
displayStatus: check.displayStatus,
impact: check.impact,
check: failure.check,
provenance: check.provenance,
...(failure.evidence ? { evidence: failure.evidence } : {}),
...(failure.howToFix ? { howToFix: failure.howToFix } : {}),
...(check.type ? { type: check.type } : {}),
...(check.acknowledged !== undefined
? { acknowledged: check.acknowledged }
: {}),
...(check.evidenceKind ? { evidenceKind: check.evidenceKind } : {}),
...(check.assurance ? { assurance: check.assurance } : {}),
},
};
}
/** Map goat-flow audit impact to the closest SARIF level without changing audit semantics. */
function levelFromImpact(impact) {
if (impact === "scope-fail")
return "error";
if (impact === "score-only")
return "warning";
return "note";
}
/** Prefer target evidence so SARIF annotations point at the files users can change first. */
function locationsFromCheck(check) {
const paths = [
...(check.provenance.target_evidence_paths ?? []),
...(check.provenance.evidence_paths ?? []),
];
return locationsFromPaths(paths);
}
/**
* Convert a single safe repo path into a SARIF location.
*
* Unsafe paths return no locations because SARIF uploads should omit
* annotations rather than leak host paths or invent placeholder files.
*/
function locationsFromPath(path, line) {
const uri = normalizeRepoUri(path);
if (!uri)
return [];
return [
{
physicalLocation: {
artifactLocation: { uri },
...(line !== undefined ? { region: { startLine: line } } : {}),
},
},
];
}
/** Use only the first addressable evidence path because one result should create one annotation. */
function locationsFromPaths(paths) {
for (const path of paths) {
const locations = locationsFromPath(path);
if (locations.length > 0)
return locations;
}
return [];
}
/** Keep SARIF URIs repo-relative and prevent local paths or URI schemes from leaking. */
function normalizeRepoUri(path) {
const trimmed = path.trim().replace(/\\/g, "/");
if (trimmed === "")
return null;
if (trimmed.startsWith("/"))
return null;
if (/^[a-z][a-z0-9+.-]*:/iu.test(trimmed))
return null;
if (trimmed.split("/").includes(".."))
return null;
return trimmed.replace(/^\.\//u, "");
}
/** Namespace drift rules away from setup check ids so SARIF rule identities cannot collide. */
function driftRuleId(kind) {
return `drift:${kind}`;
}
/** Namespace content lint rules away from setup check ids so downstream alerts stay distinct. */
function contentRuleId(rule) {
return `content:${rule}`;
}
function compareRuleRegistrations(left, right) {
return (compareScope(left.scope, right.scope) ||
compareString(left.descriptor.id, right.descriptor.id));
}
/** Deterministic result ordering keeps SARIF diffs focused on finding changes. */
function compareResults(left, right) {
return (compareScope(resultScope(left), resultScope(right)) ||
compareString(left.ruleId, right.ruleId) ||
compareString(locationUri(left.locations ?? []), locationUri(right.locations ?? [])) ||
compareString(left.message.text, right.message.text));
}
/** Treat malformed extension properties as setup scope so sorting never throws. */
function resultScope(result) {
const scope = result.properties?.scope;
if (scope === "setup" ||
scope === "agent" ||
scope === "harness" ||
scope === "drift" ||
scope === "content") {
return scope;
}
return "setup";
}
/** Compare scopes in the same order the audit summary presents them. */
function compareScope(left, right) {
return SCOPE_ORDER[left] - SCOPE_ORDER[right];
}
/** Locale-pinned string comparison avoids host-locale drift in generated SARIF. */
function compareString(left, right) {
return left.localeCompare(right, "en");
}
/** Empty URI keeps unlocated results sortable and fingerprintable without fake paths. */
function locationUri(locations) {
return locations[0]?.physicalLocation.artifactLocation.uri ?? "";
}
function fingerprintFor(scope, ruleId, uri, message) {
return {
"goatFlowAudit/v1": [scope, ruleId, uri, message].join("|"),
};
}
//# sourceMappingURL=sarif.js.map