xploitscan 1.0.11 → 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;
1987
- if (findings.length === 0) return findings;
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;
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,8 +2030,18 @@ async function filterFalsePositives(findings, fileContents) {
2026
2030
  }
2027
2031
  }
2028
2032
  }
2029
- const filtered = toReview.filter((_, i) => !fpIndices.has(i));
2030
- return [...filtered, ...overflow];
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
+ };
2031
2045
  }
2032
2046
 
2033
2047
  // src/scanners/ast-analyzer.ts
@@ -3626,6 +3640,10 @@ async function scanCommand(directory, options) {
3626
3640
  chalk2.gray("Tip: Set ANTHROPIC_API_KEY for AI-powered contextual analysis")
3627
3641
  );
3628
3642
  }
3643
+ let aiReviewed = false;
3644
+ let aiRemovedCount = 0;
3645
+ let aiTotalBefore = 0;
3646
+ let aiFilteredFindings = [];
3629
3647
  if (process.env.ANTHROPIC_API_KEY && allFindings.length > 0) {
3630
3648
  spinner.text = "AI reviewing findings for false positives...";
3631
3649
  spinner.color = "cyan";
@@ -3637,15 +3655,14 @@ async function scanCommand(directory, options) {
3637
3655
  if (content) fileContentsMap.set(f.file, content);
3638
3656
  }
3639
3657
  }
3640
- const beforeCount = allFindings.length;
3641
- const filtered = await filterFalsePositives(allFindings, fileContentsMap);
3642
- const removed = beforeCount - filtered.length;
3643
- if (removed > 0) {
3658
+ const result2 = await filterFalsePositives(allFindings, fileContentsMap);
3659
+ aiReviewed = result2.aiReviewed;
3660
+ aiRemovedCount = result2.removedCount;
3661
+ aiTotalBefore = result2.totalBefore;
3662
+ aiFilteredFindings = result2.filteredFindings;
3663
+ if (result2.removedCount > 0) {
3644
3664
  allFindings.length = 0;
3645
- allFindings.push(...filtered);
3646
- }
3647
- if (removed > 0 && verbose) {
3648
- spinner.info(`AI review removed ${removed} false positive${removed !== 1 ? "s" : ""}`);
3665
+ allFindings.push(...result2.findings);
3649
3666
  }
3650
3667
  } catch {
3651
3668
  }
@@ -3676,6 +3693,22 @@ async function scanCommand(directory, options) {
3676
3693
  renderTerminalReport(result, fileContentsForAnalysis);
3677
3694
  break;
3678
3695
  }
3696
+ if (aiReviewed && !isSilent) {
3697
+ console.log("");
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
+ }
3711
+ }
3679
3712
  if (tier === "free" && !isSilent) {
3680
3713
  console.log("");
3681
3714
  if (userPlan === "anonymous") {