

























@@ -145,6 +145,8 @@ type SourceRule = {
145145pattern: RegExp;
146146/** Secondary context pattern; both must match for the rule to fire. */
147147requiresContext?: RegExp;
148+/** If set, secondary context must be within this many lines of the primary match. */
149+requiresContextWindowLines?: number;
148150};
149151150152const LINE_RULES: LineRule[] = [
@@ -205,6 +207,7 @@ const SOURCE_RULES: SourceRule[] = [
205207"Environment variable access combined with network send — possible credential harvesting",
206208pattern: /process\.env/,
207209requiresContext: NETWORK_SEND_CONTEXT_PATTERN,
210+requiresContextWindowLines: 8,
208211},
209212];
210213@@ -240,6 +243,42 @@ function stripFullLineCommentsForHeuristics(source: string): string {
240243.join("\n");
241244}
242245246+function findSourceRuleMatch(params: {
247+rule: SourceRule;
248+source: string;
249+lines: string[];
250+}): { line: number; evidence: string } | null {
251+if (!params.rule.pattern.test(params.source)) {
252+return null;
253+}
254+if (params.rule.requiresContext && !params.rule.requiresContext.test(params.source)) {
255+return null;
256+}
257+258+for (let i = 0; i < params.lines.length; i++) {
259+if (!params.rule.pattern.test(params.lines[i] ?? "")) {
260+continue;
261+}
262+263+if (params.rule.requiresContext && params.rule.requiresContextWindowLines !== undefined) {
264+const start = Math.max(0, i - params.rule.requiresContextWindowLines);
265+const end = Math.min(params.lines.length, i + params.rule.requiresContextWindowLines + 1);
266+const windowSource = params.lines.slice(start, end).join("\n");
267+if (!params.rule.requiresContext.test(windowSource)) {
268+continue;
269+}
270+}
271+272+return { line: i + 1, evidence: params.lines[i] ?? "" };
273+}
274+275+if (params.rule.requiresContextWindowLines !== undefined) {
276+return null;
277+}
278+279+return { line: 1, evidence: params.source.slice(0, 120) };
280+}
281+243282export function scanSource(source: string, filePath: string): SkillScanFinding[] {
244283const findings: SkillScanFinding[] = [];
245284const lines = source.split("\n");
@@ -300,38 +339,22 @@ export function scanSource(source: string, filePath: string): SkillScanFinding[]
300339continue;
301340}
302341303-if (!rule.pattern.test(heuristicSource)) {
304-continue;
305-}
306-if (rule.requiresContext && !rule.requiresContext.test(heuristicSource)) {
342+const match = findSourceRuleMatch({
343+ rule,
344+source: heuristicSource,
345+lines: heuristicLines,
346+});
347+if (!match) {
307348continue;
308349}
309350310-// Find the first matching line for evidence + line number
311-let matchLine = 0;
312-let matchEvidence = "";
313-for (let i = 0; i < lines.length; i++) {
314-if (rule.pattern.test(heuristicLines[i] ?? "")) {
315-matchLine = i + 1;
316-matchEvidence = lines[i].trim();
317-break;
318-}
319-}
320-321-// For source rules, if we can't find a line match the pattern might span
322-// lines. Report line 0 with truncated source as evidence.
323-if (matchLine === 0) {
324-matchLine = 1;
325-matchEvidence = source.slice(0, 120);
326-}
327-328351findings.push({
329352ruleId: rule.ruleId,
330353severity: rule.severity,
331354file: filePath,
332-line: matchLine,
355+line: match.line,
333356message: rule.message,
334-evidence: truncateEvidence(matchEvidence),
357+evidence: truncateEvidence(lines[match.line - 1]?.trim() ?? match.evidence.trim()),
335358});
336359matchedSourceRules.add(ruleKey);
337360}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。