import type { CodeFinding } from '../index.js'; const MAX_SNIPPET_CHARS = 500; const MAX_PROMPT_CHARS = 30_000; export const VERIFIER_SYSTEM_PROMPT = `You are a senior security engineer performing false-positive analysis on automated security scan findings. Your task is to classify each finding as likely false positive or real vulnerability. False-positive rules: - Findings in files named *.example, *.sample, *.template, or under test/ directories are likely false positives. - Placeholder values such as "your_key_here", "CHANGE_ME", "xxx" indicate false positives. - Clearly public/example/demo values indicate false positives. - YAML/JSON style or formatting issues (including wrong indentation) are always false positives. - Auto-generated files like package-lock.json and *.min.js are likely false positives. Return ONLY a valid JSON array. No markdown. No code fences. No text outside the JSON array.`; /** * Builds the user prompt for a verifier batch of code findings. */ export function buildVerificationPrompt(findings: CodeFinding[]): string { let findingsForPrompt = [...findings]; let prompt = renderPrompt(findingsForPrompt, findings.length); while (prompt.length > MAX_PROMPT_CHARS && findingsForPrompt.length > 1) { findingsForPrompt = findingsForPrompt.slice(1); prompt = renderPrompt(findingsForPrompt, findings.length); } if (prompt.length > MAX_PROMPT_CHARS) { return prompt.slice(0, MAX_PROMPT_CHARS); } return prompt; } function renderPrompt(findings: CodeFinding[], originalCount: number): string { const droppedCount = originalCount - findings.length; const truncationNote = droppedCount > 0 ? ` ${droppedCount} oldest findings omitted due to prompt size limits.` : ''; const header = `Analyze these ${findings.length} security findings and determine if each is real or FP.${truncationNote}`; const body = findings .map((finding, index) => { const rawSnippet = finding.snippet ?? '(no snippet available)'; const snippet = truncateTextMiddle(rawSnippet, MAX_SNIPPET_CHARS, '\n...[SNIPPET TRUNCATED]...\n'); const cwe = finding.cwe ?? 'null'; const line = finding.line === null ? 'null' : String(finding.line); return [ `=== FINDING ${index + 1} ===`, `Scanner: ${finding.scanner}`, `File: ${finding.file}`, `Line: ${line}`, `Severity: ${finding.severity}`, `Issue: ${finding.issue}`, `CWE: ${cwe}`, '', 'Code snippet:', snippet, '==================', ].join('\n'); }) .join('\n\n'); const schema = [ `Respond with JSON array of ${findings.length} objects:`, '[', ' {', ' "finding_index": 0,', ' "is_likely_fp": false,', ' "confidence": 97,', ' "reason": "one line reason",', ' "explanation": "detailed explanation why this is real or FP"', ' }', ']', ].join('\n'); return `${header}\n\n${body}\n\n${schema}`; } function truncateTextMiddle(value: string, maxChars: number, marker: string): string { if (value.length <= maxChars) { return value; } if (maxChars <= marker.length + 2) { return value.slice(0, maxChars); } const budget = maxChars - marker.length; const startLength = Math.ceil(budget / 2); const endLength = Math.floor(budget / 2); return `${value.slice(0, startLength)}${marker}${value.slice(value.length - endLength)}`; }