xploitscan 1.0.12 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1976,17 +1976,19 @@ function parseReviewResponse(text) {
1976
1976
  const parsed = JSON.parse(cleaned);
1977
1977
  if (!Array.isArray(parsed)) return [];
1978
1978
  return parsed.filter(
1979
- (r) => typeof r === "object" && r !== null && "index" in r && "verdict" in r && r.verdict === "real" || r.verdict === "fp"
1979
+ (r) => typeof r === "object" && r !== null && "index" in r && "verdict" in r && (r.verdict === "real" || r.verdict === "fp")
1980
1980
  );
1981
1981
  } catch {
1982
1982
  return [];
1983
1983
  }
1984
1984
  }
1985
1985
  async function filterFalsePositives(findings, fileContents) {
1986
- if (!process.env.ANTHROPIC_API_KEY) return { findings, aiReviewed: false, removedCount: 0 };
1987
- if (findings.length === 0) return { findings, aiReviewed: false, removedCount: 0 };
1986
+ const empty = { findings, filteredFindings: [], aiReviewed: false, removedCount: 0, totalBefore: findings.length };
1987
+ if (!process.env.ANTHROPIC_API_KEY) return empty;
1988
+ if (findings.length === 0) return empty;
1988
1989
  const toReview = findings.slice(0, MAX_TOTAL_FINDINGS);
1989
1990
  const overflow = findings.slice(MAX_TOTAL_FINDINGS);
1991
+ const totalBefore = findings.length;
1990
1992
  const byFile = /* @__PURE__ */ new Map();
1991
1993
  for (const f of toReview) {
1992
1994
  const group = byFile.get(f.file) || [];
@@ -1997,9 +1999,9 @@ async function filterFalsePositives(findings, fileContents) {
1997
1999
  try {
1998
2000
  client = new Anthropic2();
1999
2001
  } catch {
2000
- return { findings, aiReviewed: false, removedCount: 0 };
2002
+ return empty;
2001
2003
  }
2002
- const fpIndices = /* @__PURE__ */ new Set();
2004
+ const fpMap = /* @__PURE__ */ new Map();
2003
2005
  for (const [file, fileFindings] of byFile) {
2004
2006
  const content = fileContents.get(file);
2005
2007
  if (!content) continue;
@@ -2018,7 +2020,9 @@ async function filterFalsePositives(findings, fileContents) {
2018
2020
  for (const r of results) {
2019
2021
  if (r.verdict === "fp" && r.index >= 0 && r.index < batch.length) {
2020
2022
  const globalIndex = toReview.indexOf(batch[r.index]);
2021
- if (globalIndex !== -1) fpIndices.add(globalIndex);
2023
+ if (globalIndex !== -1) {
2024
+ fpMap.set(globalIndex, r.reason);
2025
+ }
2022
2026
  }
2023
2027
  }
2024
2028
  } catch {
@@ -2026,9 +2030,18 @@ async function filterFalsePositives(findings, fileContents) {
2026
2030
  }
2027
2031
  }
2028
2032
  }
2029
- const filtered = toReview.filter((_, i) => !fpIndices.has(i));
2030
- const result = [...filtered, ...overflow];
2031
- return { findings: result, aiReviewed: true, removedCount: fpIndices.size };
2033
+ const filtered = toReview.filter((_, i) => !fpMap.has(i));
2034
+ const filteredFindings = [];
2035
+ for (const [idx, reason] of fpMap) {
2036
+ filteredFindings.push({ finding: toReview[idx], reason });
2037
+ }
2038
+ return {
2039
+ findings: [...filtered, ...overflow],
2040
+ filteredFindings,
2041
+ aiReviewed: true,
2042
+ removedCount: fpMap.size,
2043
+ totalBefore
2044
+ };
2032
2045
  }
2033
2046
 
2034
2047
  // src/scanners/ast-analyzer.ts
@@ -3629,6 +3642,8 @@ async function scanCommand(directory, options) {
3629
3642
  }
3630
3643
  let aiReviewed = false;
3631
3644
  let aiRemovedCount = 0;
3645
+ let aiTotalBefore = 0;
3646
+ let aiFilteredFindings = [];
3632
3647
  if (process.env.ANTHROPIC_API_KEY && allFindings.length > 0) {
3633
3648
  spinner.text = "AI reviewing findings for false positives...";
3634
3649
  spinner.color = "cyan";
@@ -3643,6 +3658,8 @@ async function scanCommand(directory, options) {
3643
3658
  const result2 = await filterFalsePositives(allFindings, fileContentsMap);
3644
3659
  aiReviewed = result2.aiReviewed;
3645
3660
  aiRemovedCount = result2.removedCount;
3661
+ aiTotalBefore = result2.totalBefore;
3662
+ aiFilteredFindings = result2.filteredFindings;
3646
3663
  if (result2.removedCount > 0) {
3647
3664
  allFindings.length = 0;
3648
3665
  allFindings.push(...result2.findings);
@@ -3677,9 +3694,20 @@ async function scanCommand(directory, options) {
3677
3694
  break;
3678
3695
  }
3679
3696
  if (aiReviewed && !isSilent) {
3680
- const msg = aiRemovedCount > 0 ? ` AI review: ${aiRemovedCount} false positive${aiRemovedCount !== 1 ? "s" : ""} removed, ${dedupedFindings.length} verified finding${dedupedFindings.length !== 1 ? "s" : ""} remain` : ` AI review: all ${dedupedFindings.length} finding${dedupedFindings.length !== 1 ? "s" : ""} verified`;
3681
3697
  console.log("");
3682
- console.log(chalk2.cyan("\u{1F916} ") + chalk2.gray(msg));
3698
+ if (aiRemovedCount > 0) {
3699
+ console.log(chalk2.cyan("\u{1F916} AI Review: ") + chalk2.white(`${aiTotalBefore} findings scanned \u2192 ${aiRemovedCount} false positive${aiRemovedCount !== 1 ? "s" : ""} filtered \u2192 `) + chalk2.green(`${dedupedFindings.length} verified`));
3700
+ } else {
3701
+ console.log(chalk2.cyan("\u{1F916} AI Review: ") + chalk2.green(`all ${dedupedFindings.length} finding${dedupedFindings.length !== 1 ? "s" : ""} verified`));
3702
+ }
3703
+ if (verbose && aiFilteredFindings.length > 0) {
3704
+ console.log(chalk2.gray(`
3705
+ Filtered findings (AI determined these are false positives):`));
3706
+ for (const { finding: f, reason } of aiFilteredFindings) {
3707
+ console.log(chalk2.gray(` ${f.severity.toUpperCase().padEnd(8)} ${f.rule} ${f.file}:${f.line}`));
3708
+ console.log(chalk2.gray(` Reason: ${reason}`));
3709
+ }
3710
+ }
3683
3711
  }
3684
3712
  if (tier === "free" && !isSilent) {
3685
3713
  console.log("");