mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-09-13 13:39:04 +02:00
fix(sast): align Capella export with the submit-time code-path contract
The export gate required every code_paths entry to be file:line, but submit only requires the primary sink to be file:line and accepts bare trace steps. A single malformed trace step therefore dropped an otherwise-valid finding at export. - add isValidPrimaryCodePath as the one shared primary-sink contract - validate only the primary at export; buildResult already drops unusable steps - route the submit-time validator through the same helper so the two cannot drift
This commit is contained in:
@@ -55,7 +55,7 @@ import {
|
||||
type TriageChecklist,
|
||||
type TriageRuleKey,
|
||||
} from './finding-types.js';
|
||||
import { isNormalizedRepositoryPath, parseCodePath } from './paths.js';
|
||||
import { isValidPrimaryCodePath, parseCodePath } from './paths.js';
|
||||
|
||||
// === Result Helpers ===
|
||||
|
||||
@@ -265,8 +265,7 @@ export function validateCodePaths(
|
||||
error: `code_paths[0] must be a sink location "<path>:<line>", not a URL (got ${JSON.stringify(first)}).`,
|
||||
};
|
||||
}
|
||||
const parsed = parseCodePath(first);
|
||||
if (!parsed || !isNormalizedRepositoryPath(parsed.file)) {
|
||||
if (!isValidPrimaryCodePath(first)) {
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
@@ -275,6 +274,10 @@ export function validateCodePaths(
|
||||
'contain traversal, be a bare file, a symbol or an offset.',
|
||||
};
|
||||
}
|
||||
const parsed = parseCodePath(first);
|
||||
if (!parsed) {
|
||||
return { ok: false, error: 'code_paths[0] must be a "<path>:<line>" locator.' };
|
||||
}
|
||||
return { ok: true, codePaths, file: parsed.file, line: parsed.line };
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,17 @@ export function parseCodePath(entry: string): ParsedCodePath | undefined {
|
||||
return { file, line };
|
||||
}
|
||||
|
||||
/**
|
||||
* The primary (sink) code-path contract, shared by the submit-time collector and the
|
||||
* export gate so the two ends enforce the same rule and cannot drift. `code_paths[0]`
|
||||
* becomes the finding's SARIF location, so it must be a normalized repository-relative
|
||||
* `file:line`. Trace steps are held to no such requirement on either side.
|
||||
*/
|
||||
export function isValidPrimaryCodePath(entry: string): boolean {
|
||||
const parsed = parseCodePath(entry);
|
||||
return parsed !== undefined && isNormalizedRepositoryPath(parsed.file);
|
||||
}
|
||||
|
||||
/**
|
||||
* The normalized repository-relative POSIX path contract shared by SARIF
|
||||
* locations and code-path scoping. Rejects absolute, drive-letter, encoded,
|
||||
|
||||
@@ -22,7 +22,7 @@ import type { AgenticSastOmission, AgenticSastReduction, SarifRef } from '../typ
|
||||
import { atomicPublishBytes, sha256Bytes, stableJson } from './artifacts.js';
|
||||
import { SastContractError } from './errors.js';
|
||||
import type { CapellaFinding, CapellaSeverity } from './finding-types.js';
|
||||
import { isExcludedCodePath, isNormalizedRepositoryPath, parseCodePath } from './paths.js';
|
||||
import { isExcludedCodePath, isNormalizedRepositoryPath, isValidPrimaryCodePath, parseCodePath } from './paths.js';
|
||||
import { renderCapellaReport } from './report.js';
|
||||
import type { AtomicPublishOptions } from './types.js';
|
||||
import { isCapellaFinding } from './validation.js';
|
||||
@@ -102,14 +102,15 @@ function classifyExportCandidate(
|
||||
if (!isCapellaFinding(value)) {
|
||||
return { omission: buildOmission(value, 'invalid_finding_record') };
|
||||
}
|
||||
if (value.code_paths.length === 0) {
|
||||
const primary = value.code_paths[0];
|
||||
if (value.code_paths.length === 0 || primary === undefined) {
|
||||
return { omission: buildOmission(value, 'missing_code_path') };
|
||||
}
|
||||
const codePathsAreValid = value.code_paths.every((entry) => {
|
||||
const parsed = parseCodePath(entry);
|
||||
return parsed !== undefined && isNormalizedRepositoryPath(parsed.file);
|
||||
});
|
||||
if (!codePathsAreValid) {
|
||||
// Only the primary (sink) must be a `file:line` locator: it becomes the SARIF location.
|
||||
// Trace steps are held to no such rule at submit time, and `buildResult` already drops any
|
||||
// that are not `file:line`, so gating on them here would discard a whole finding for a
|
||||
// malformed step the two ends never agreed to require.
|
||||
if (!isValidPrimaryCodePath(primary)) {
|
||||
return { omission: buildOmission(value, 'invalid_code_path') };
|
||||
}
|
||||
return { finding: value };
|
||||
|
||||
Reference in New Issue
Block a user