feat: initial open marketplace with llm-security, config-audit, ultraplan-local
This commit is contained in:
commit
f93d6abdae
380 changed files with 65935 additions and 0 deletions
154
plugins/config-audit/scanners/lib/suppression.mjs
Normal file
154
plugins/config-audit/scanners/lib/suppression.mjs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* Suppression engine for config-audit.
|
||||
* Lets users suppress known false positives via .config-audit-ignore files.
|
||||
* Supports exact IDs (CA-CML-001) and glob patterns (CA-SET-*).
|
||||
* Zero external dependencies.
|
||||
*/
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
/**
|
||||
* Load suppressions from .config-audit-ignore files.
|
||||
* Searches targetPath first, then ~/.claude/config-audit/.
|
||||
* Project-level file takes precedence (loaded first).
|
||||
* @param {string} targetPath - Project root to search
|
||||
* @returns {Promise<{ suppressions: Array<{ pattern: string, comment: string }>, source: string }>}
|
||||
*/
|
||||
export async function loadSuppressions(targetPath) {
|
||||
const sources = [
|
||||
{ path: join(targetPath, '.config-audit-ignore'), label: 'project' },
|
||||
{ path: join(homedir(), '.config-audit', '.config-audit-ignore'), label: 'global' },
|
||||
];
|
||||
|
||||
for (const src of sources) {
|
||||
try {
|
||||
const content = await readFile(src.path, 'utf-8');
|
||||
const suppressions = parseIgnoreFile(content);
|
||||
return { suppressions, source: src.label };
|
||||
} catch {
|
||||
// File doesn't exist — try next
|
||||
}
|
||||
}
|
||||
|
||||
return { suppressions: [], source: 'none' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a .config-audit-ignore file into suppression entries.
|
||||
* @param {string} content - File content
|
||||
* @returns {Array<{ pattern: string, comment: string }>}
|
||||
*/
|
||||
export function parseIgnoreFile(content) {
|
||||
const suppressions = [];
|
||||
|
||||
for (const rawLine of content.split('\n')) {
|
||||
const line = rawLine.trim();
|
||||
|
||||
// Skip empty lines and comment-only lines
|
||||
if (!line || line.startsWith('#')) continue;
|
||||
|
||||
// Split on first # for inline comment
|
||||
const hashIdx = line.indexOf('#');
|
||||
let pattern, comment;
|
||||
if (hashIdx > 0) {
|
||||
pattern = line.slice(0, hashIdx).trim();
|
||||
comment = line.slice(hashIdx + 1).trim();
|
||||
} else {
|
||||
pattern = line;
|
||||
comment = '';
|
||||
}
|
||||
|
||||
// Validate pattern looks like a finding ID or glob
|
||||
if (/^CA-[A-Z]{2,4}[-*\d]+/.test(pattern) || /^CA-[A-Z]{2,4}-\*$/.test(pattern)) {
|
||||
suppressions.push({ pattern, comment });
|
||||
}
|
||||
}
|
||||
|
||||
return suppressions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply suppressions to a findings array.
|
||||
* @param {object[]} findings - Array of finding objects with .id
|
||||
* @param {Array<{ pattern: string, comment: string }>} suppressions
|
||||
* @returns {{ active: object[], suppressed: object[] }}
|
||||
*/
|
||||
export function applySuppressions(findings, suppressions) {
|
||||
if (!suppressions || suppressions.length === 0) {
|
||||
return { active: [...findings], suppressed: [] };
|
||||
}
|
||||
|
||||
const active = [];
|
||||
const suppressed = [];
|
||||
|
||||
for (const f of findings) {
|
||||
if (isMatchedByAny(f.id, suppressions)) {
|
||||
suppressed.push(f);
|
||||
} else {
|
||||
active.push(f);
|
||||
}
|
||||
}
|
||||
|
||||
return { active, suppressed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a finding ID matches any suppression pattern.
|
||||
* @param {string} id - Finding ID (e.g. CA-CML-001)
|
||||
* @param {Array<{ pattern: string }>} suppressions
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isMatchedByAny(id, suppressions) {
|
||||
for (const s of suppressions) {
|
||||
if (matchPattern(id, s.pattern)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match a finding ID against a suppression pattern.
|
||||
* Supports exact match and glob-style CA-XXX-* patterns.
|
||||
* @param {string} id - e.g. "CA-CML-001"
|
||||
* @param {string} pattern - e.g. "CA-CML-001" or "CA-CML-*"
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function matchPattern(id, pattern) {
|
||||
// Exact match
|
||||
if (id === pattern) return true;
|
||||
|
||||
// Glob: CA-XXX-* matches any CA-XXX-NNN
|
||||
if (pattern.endsWith('-*')) {
|
||||
const prefix = pattern.slice(0, -1); // "CA-XXX-"
|
||||
return id.startsWith(prefix);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a human-readable suppression summary line.
|
||||
* @param {object[]} suppressed - Array of suppressed findings
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatSuppressionSummary(suppressed) {
|
||||
if (!suppressed || suppressed.length === 0) {
|
||||
return '0 findings suppressed';
|
||||
}
|
||||
|
||||
// Group by scanner prefix pattern
|
||||
const groups = new Map();
|
||||
for (const f of suppressed) {
|
||||
// Extract prefix: CA-CML-001 → CA-CML
|
||||
const prefix = f.id.replace(/-\d+$/, '');
|
||||
groups.set(prefix, (groups.get(prefix) || 0) + 1);
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
for (const [prefix, count] of groups) {
|
||||
parts.push(`${count} \u00d7 ${prefix}-*`);
|
||||
}
|
||||
|
||||
return `${suppressed.length} finding(s) suppressed (${parts.join(', ')})`;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue