hackmyagent 0.17.6 → 0.17.7

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/cli.js CHANGED
@@ -247,6 +247,7 @@ Examples:
247
247
  },
248
248
  verbose: !!options.verbose,
249
249
  usedAnalm: !!options.analm,
250
+ analystFindings: nmResult.analystFindings,
250
251
  });
251
252
  const risk = critical.length > 0 ? 'critical' : high.length > 0 ? 'high' : issues.length > 0 ? 'medium' : 'low';
252
253
  if (risk === 'critical' || risk === 'high')
@@ -773,6 +774,81 @@ function displayUnifiedCheck(opts) {
773
774
  console.log(` ${colors.dim}${legend}${RESET()}`);
774
775
  }
775
776
  }
777
+ // ── AnaLM Analysis ──────────────────────────────────────────────────
778
+ if (opts.analystFindings && opts.analystFindings.length > 0) {
779
+ divider('AnaLM Analysis');
780
+ for (const af of opts.analystFindings) {
781
+ const r = af.result;
782
+ if (af.taskType === 'threatAnalysis') {
783
+ const level = String(r.threatLevel ?? 'unknown').toUpperCase();
784
+ const levelColor = level === 'CRITICAL' || level === 'HIGH' ? colors.red : level === 'MEDIUM' ? colors.yellow : colors.dim;
785
+ console.log(` ${levelColor}${colors.bold}${level}${RESET()} ${r.attackVector ?? ''}`);
786
+ if (r.description)
787
+ console.log(` ${colors.dim}${r.description}${RESET()}`);
788
+ if (Array.isArray(r.mitigations) && r.mitigations.length > 0) {
789
+ for (const m of r.mitigations) {
790
+ console.log(` ${colors.cyan}Fix:${RESET()} ${m}`);
791
+ }
792
+ }
793
+ }
794
+ else if (af.taskType === 'credentialContextClassification') {
795
+ const cls = String(r.classification ?? 'unknown');
796
+ const clsColor = cls === 'real' ? colors.red : cls === 'test' || cls === 'example' ? colors.green : colors.yellow;
797
+ console.log(` Credential: ${clsColor}${colors.bold}${cls}${RESET()}`);
798
+ if (r.reasoning)
799
+ console.log(` ${colors.dim}${r.reasoning}${RESET()}`);
800
+ }
801
+ else if (af.taskType === 'intelReport') {
802
+ if (r.summary)
803
+ console.log(` ${colors.cyan}Summary:${RESET()} ${r.summary}`);
804
+ if (Array.isArray(r.keyFindings) && r.keyFindings.length > 0) {
805
+ for (const kf of r.keyFindings) {
806
+ console.log(` ${colors.dim}${kf}${RESET()}`);
807
+ }
808
+ }
809
+ if (r.riskAssessment)
810
+ console.log(` ${colors.cyan}Risk:${RESET()} ${r.riskAssessment}`);
811
+ if (Array.isArray(r.recommendations) && r.recommendations.length > 0) {
812
+ for (const rec of r.recommendations) {
813
+ console.log(` ${colors.dim}${rec}${RESET()}`);
814
+ }
815
+ }
816
+ }
817
+ else if (af.taskType === 'governanceReasoning') {
818
+ if (Array.isArray(r.gaps) && r.gaps.length > 0) {
819
+ console.log(` ${colors.yellow}Governance gaps:${RESET()}`);
820
+ for (const gap of r.gaps)
821
+ console.log(` ${colors.dim}- ${gap}${RESET()}`);
822
+ }
823
+ if (Array.isArray(r.recommendations) && r.recommendations.length > 0) {
824
+ for (const rec of r.recommendations) {
825
+ console.log(` ${colors.cyan}Fix:${RESET()} ${rec}`);
826
+ }
827
+ }
828
+ }
829
+ else if (af.taskType === 'checkExplanation') {
830
+ if (r.explanation)
831
+ console.log(` ${r.explanation}`);
832
+ if (r.impact)
833
+ console.log(` ${colors.yellow}Impact:${RESET()} ${r.impact}`);
834
+ if (r.recommendation)
835
+ console.log(` ${colors.cyan}Fix:${RESET()} ${r.recommendation}`);
836
+ }
837
+ else if (af.taskType === 'falsePositiveDetection') {
838
+ const fp = Boolean(r.isFalsePositive);
839
+ console.log(` ${fp ? colors.green : colors.yellow}${fp ? 'Likely false positive' : 'Likely real finding'}${RESET()}`);
840
+ if (r.reasoning)
841
+ console.log(` ${colors.dim}${r.reasoning}${RESET()}`);
842
+ }
843
+ else {
844
+ // Generic display
845
+ if (r.description)
846
+ console.log(` ${r.description}`);
847
+ }
848
+ console.log(` ${colors.dim}Confidence: ${Math.round(af.confidence * 100)}% | ${af.modelVersion} (${af.durationMs}ms)${RESET()}`);
849
+ console.log();
850
+ }
851
+ }
776
852
  // ── Next steps ──────────────────────────────────────────────────────
777
853
  const hasGovIssues = failed.some(f => f.category === 'governance' || f.category === 'Governance' || f.checkId?.startsWith('AST-GOV') || f.checkId?.startsWith('AST-PROMPT'));
778
854
  const hasCredIssues = failed.some(f => f.checkId?.startsWith('CRED-') || f.name?.toLowerCase().includes('credential') || f.name?.toLowerCase().includes('api key') || f.name?.toLowerCase().includes('hardcoded'));
@@ -6319,7 +6395,8 @@ function looksLikeRawUrl(target) {
6319
6395
  */
6320
6396
  function parseGitHubTarget(target) {
6321
6397
  // Full URL: https://github.com/org/repo[.git][/tree/...]
6322
- const urlMatch = target.match(/^https?:\/\/(www\.)?github\.com\/([^/]+)\/([^/.]+)/);
6398
+ // Allow dots in repo names (e.g. next.js, vue.js) but strip trailing .git
6399
+ const urlMatch = target.match(/^https?:\/\/(www\.)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/|$)/);
6323
6400
  if (urlMatch) {
6324
6401
  return {
6325
6402
  org: urlMatch[2],
@@ -6603,6 +6680,77 @@ const TEST_FILE_PATTERNS = [
6603
6680
  /[^/]+\.spec\.\w+$/,
6604
6681
  /\bfixtures?\//i,
6605
6682
  ];
6683
+ /**
6684
+ * Documentation and generated file patterns.
6685
+ * These files describe security concepts (credentials, prompts, governance)
6686
+ * but are not attack surfaces themselves. Governance/credential/prompt
6687
+ * findings on these are almost always false positives in cloned repos.
6688
+ */
6689
+ const DOCS_AND_GENERATED_PATTERNS = [
6690
+ /\.md$/i, // Markdown documentation
6691
+ /\.rst$/i, // reStructuredText docs
6692
+ /\bdocs?\//i, // docs/ directory
6693
+ /\bdocumentation\//i, // documentation/ directory
6694
+ /\bexamples?\//i, // examples/ directory
6695
+ /\bsamples?\//i, // samples/ directory
6696
+ /openapi[^/]*\.json$/i, // OpenAPI spec files
6697
+ /openapi[^/]*\.ya?ml$/i, // OpenAPI spec YAML files
6698
+ /swagger[^/]*\.json$/i, // Swagger spec files
6699
+ /swagger[^/]*\.ya?ml$/i, // Swagger spec YAML files
6700
+ /\bapi\/openapi-spec\//i, // API spec directories
6701
+ /\bapi\/discovery\//i, // API discovery docs
6702
+ /\bvendor\//i, // vendored dependencies
6703
+ /\bthird.?party\//i, // third-party code
6704
+ /\bCHANGELOG/i, // changelog files
6705
+ /\bHISTORY/i, // history files
6706
+ /\bLICENSE/i, // license files
6707
+ /\bCONTRIBUTING/i, // contributing guides
6708
+ ];
6709
+ function isDocsOrGenerated(filePath) {
6710
+ return DOCS_AND_GENERATED_PATTERNS.some(p => p.test(filePath));
6711
+ }
6712
+ /**
6713
+ * Build scripts, CI/CD pipelines, and infrastructure files.
6714
+ * These are not runtime attack surfaces — findings here are lower risk.
6715
+ */
6716
+ const BUILD_CI_PATTERNS = [
6717
+ /\bbuild\//i, // build/ directory
6718
+ /\bdist\//i, // dist/ directory
6719
+ /\b\.github\//, // GitHub Actions
6720
+ /\b\.circleci\//, // CircleCI
6721
+ /\bazure-pipelines/i, // Azure Pipelines
6722
+ /\bjenkins/i, // Jenkins
6723
+ /\b\.gitlab-ci/, // GitLab CI
6724
+ /\bMakefile$/i, // Makefiles
6725
+ /\bGruntfile/i, // Grunt
6726
+ /\bgulpfile/i, // Gulp
6727
+ /\bwebpack\.\w+\.js$/i, // Webpack configs
6728
+ /\brollup\.\w+\.js$/i, // Rollup configs
6729
+ /\bscripts\//i, // scripts/ directory
6730
+ /\btools?\//i, // tools/ directory
6731
+ /\binfra\//i, // infra/ directory
6732
+ ];
6733
+ function isBuildOrCiFile(filePath) {
6734
+ return BUILD_CI_PATTERNS.some(p => p.test(filePath));
6735
+ }
6736
+ /**
6737
+ * Check IDs for security-sensitive pattern matches that produce false
6738
+ * positives on documentation and generated files. These checks look for
6739
+ * credential patterns, prompt injection, governance gaps, and skill
6740
+ * definitions — all of which appear naturally in docs that DESCRIBE
6741
+ * these concepts without being vulnerable.
6742
+ */
6743
+ const DOCS_FALSE_POSITIVE_PREFIXES = [
6744
+ 'AST-GOV', // governance checks
6745
+ 'AST-GOVERN', // governance checks
6746
+ 'AST-PROMPT', // prompt security checks
6747
+ 'AST-HEARTBEAT', // heartbeat/liveness checks
6748
+ 'AST-CRED', // credential pattern checks
6749
+ 'AST-INJECT', // injection pattern checks
6750
+ 'AST-EXFIL', // exfiltration pattern checks
6751
+ 'SKILL-', // skill definition checks
6752
+ 'SUPPLY-', // supply chain checks
6753
+ ];
6606
6754
  function isTestFile(filePath) {
6607
6755
  return TEST_FILE_PATTERNS.some(p => p.test(filePath));
6608
6756
  }
@@ -6612,8 +6760,10 @@ function isAiToolingFile(filePath) {
6612
6760
  /**
6613
6761
  * Filter out local-dev-only findings that are meaningless for downloaded
6614
6762
  * packages (e.g. "Missing .gitignore" on an npm tarball). Also filters
6615
- * governance findings on AI tooling files and demotes test file findings.
6616
- * Mutates `result.findings` in place and recalculates the score.
6763
+ * governance findings on AI tooling files, removes false-positive
6764
+ * pattern matches on documentation/generated files, and demotes test
6765
+ * file findings. Mutates `result.findings` in place and recalculates
6766
+ * the score.
6617
6767
  */
6618
6768
  function filterLocalOnlyFindings(result, scanner) {
6619
6769
  result.findings = result.findings.filter(f => {
@@ -6626,14 +6776,29 @@ function filterLocalOnlyFindings(result, scanner) {
6626
6776
  // files are false positives — they describe security practices, not vulnerabilities.
6627
6777
  if (f.file && isAiToolingFile(f.file))
6628
6778
  return false;
6779
+ // Exclude governance/credential/prompt/skill pattern-match findings on
6780
+ // documentation, generated specs, and vendored files. These files
6781
+ // naturally describe security concepts without being vulnerable.
6782
+ // Structural checks (unicode steganography, TOCTOU, deserialization)
6783
+ // are kept — those detect actual content issues regardless of file type.
6784
+ if (f.file && isDocsOrGenerated(f.file)) {
6785
+ const checkId = f.checkId || '';
6786
+ if (DOCS_FALSE_POSITIVE_PREFIXES.some(p => checkId.startsWith(p))) {
6787
+ return false;
6788
+ }
6789
+ }
6629
6790
  return true;
6630
6791
  });
6631
- // Demote test file findings to low severity (test code patterns are
6632
- // lower risk pickle.load in a test file is not an attack surface)
6792
+ // Demote test file and build script findings to low severity.
6793
+ // Test code patterns are lower risk (pickle.load in a test file is not
6794
+ // an attack surface). Build scripts, CI configs, and vendored code are
6795
+ // not runtime attack surfaces for the end user.
6633
6796
  for (const f of result.findings) {
6634
- if (f.file && isTestFile(f.file) && (f.severity === 'critical' || f.severity === 'high')) {
6635
- f.originalSeverity = f.severity;
6636
- f.severity = 'low';
6797
+ if (f.file && (f.severity === 'critical' || f.severity === 'high')) {
6798
+ if (isTestFile(f.file) || isBuildOrCiFile(f.file)) {
6799
+ f.originalSeverity = f.severity;
6800
+ f.severity = 'low';
6801
+ }
6637
6802
  }
6638
6803
  }
6639
6804
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
@@ -6666,10 +6831,16 @@ function printCheckNextSteps(target, context) {
6666
6831
  }
6667
6832
  if (context?.hasFindings) {
6668
6833
  console.log(` ${colors.cyan}Full project audit:${RESET()} ${getFullScanHint()}`);
6834
+ if (!context?.usedAnalm) {
6835
+ console.log(` ${colors.cyan}AI analysis:${RESET()} ${CLI_PREFIX} check ${target} --analm`);
6836
+ }
6669
6837
  }
6670
6838
  else if (context?.isCleanScan && isLocal) {
6671
6839
  console.log(` ${colors.cyan}Governance scan:${RESET()} ${CLI_PREFIX} scan-soul ${target}`);
6672
6840
  console.log(` ${colors.cyan}Red-team test:${RESET()} ${CLI_PREFIX} attack --local`);
6841
+ if (!context?.usedAnalm) {
6842
+ console.log(` ${colors.cyan}AI analysis:${RESET()} ${CLI_PREFIX} check ${target} --analm`);
6843
+ }
6673
6844
  }
6674
6845
  else if (context?.isCleanScan) {
6675
6846
  if (!context?.usedAnalm) {
@@ -6785,6 +6956,7 @@ async function checkGitHubRepo(target, options) {
6785
6956
  const scanner = new index_1.HardeningScanner();
6786
6957
  const result = await scanner.scan({ targetDir: repoDir, autoFix: false });
6787
6958
  // Run NanoMind semantic analysis and re-filter
6959
+ let analystFindings;
6788
6960
  try {
6789
6961
  const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
6790
6962
  const nmResult = await orchestrateNanoMind(repoDir, result.findings, { silent: true, analm: options.analm });
@@ -6792,6 +6964,7 @@ async function checkGitHubRepo(target, options) {
6792
6964
  const projectType = result.projectType || 'library';
6793
6965
  result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
6794
6966
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
6967
+ analystFindings = nmResult.analystFindings;
6795
6968
  }
6796
6969
  catch {
6797
6970
  // NanoMind unavailable — use base scan results
@@ -6804,7 +6977,7 @@ async function checkGitHubRepo(target, options) {
6804
6977
  const medium = failed.filter(f => f.severity === 'medium');
6805
6978
  const low = failed.filter(f => f.severity === 'low');
6806
6979
  if (options.json) {
6807
- writeJsonStdout({
6980
+ const jsonOut = {
6808
6981
  name: displayName,
6809
6982
  type: 'github-repo',
6810
6983
  source: 'local-scan',
@@ -6812,7 +6985,10 @@ async function checkGitHubRepo(target, options) {
6812
6985
  score: result.score,
6813
6986
  maxScore: result.maxScore,
6814
6987
  findings: result.findings,
6815
- });
6988
+ };
6989
+ if (analystFindings?.length)
6990
+ jsonOut.analystFindings = analystFindings;
6991
+ writeJsonStdout(jsonOut);
6816
6992
  return;
6817
6993
  }
6818
6994
  // Await registry data (started in parallel with clone)
@@ -6825,6 +7001,8 @@ async function checkGitHubRepo(target, options) {
6825
7001
  localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
6826
7002
  registry: registryData,
6827
7003
  verbose: !!options.verbose,
7004
+ usedAnalm: !!options.analm,
7005
+ analystFindings,
6828
7006
  });
6829
7007
  // Community contribution
6830
7008
  if (process.stdin.isTTY && !globalCiMode) {
@@ -6948,6 +7126,7 @@ async function checkPyPiPackage(target, options) {
6948
7126
  const scanner = new index_1.HardeningScanner();
6949
7127
  const result = await scanner.scan({ targetDir: extractDir, autoFix: false });
6950
7128
  // Run NanoMind semantic analysis and re-filter
7129
+ let analystFindings;
6951
7130
  try {
6952
7131
  const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
6953
7132
  const nmResult = await orchestrateNanoMind(extractDir, result.findings, { silent: true, analm: options.analm });
@@ -6955,6 +7134,7 @@ async function checkPyPiPackage(target, options) {
6955
7134
  const projectType = result.projectType || 'library';
6956
7135
  result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
6957
7136
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
7137
+ analystFindings = nmResult.analystFindings;
6958
7138
  }
6959
7139
  catch {
6960
7140
  // NanoMind unavailable -- use base scan results
@@ -6967,7 +7147,7 @@ async function checkPyPiPackage(target, options) {
6967
7147
  const medium = failed.filter(f => f.severity === 'medium');
6968
7148
  const low = failed.filter(f => f.severity === 'low');
6969
7149
  if (options.json) {
6970
- writeJsonStdout({
7150
+ const jsonOut = {
6971
7151
  name,
6972
7152
  type: 'pypi-package',
6973
7153
  source: 'local-scan',
@@ -6976,7 +7156,10 @@ async function checkPyPiPackage(target, options) {
6976
7156
  score: result.score,
6977
7157
  maxScore: result.maxScore,
6978
7158
  findings: result.findings,
6979
- });
7159
+ };
7160
+ if (analystFindings?.length)
7161
+ jsonOut.analystFindings = analystFindings;
7162
+ writeJsonStdout(jsonOut);
6980
7163
  return;
6981
7164
  }
6982
7165
  // Display results using unified display
@@ -6990,6 +7173,8 @@ async function checkPyPiPackage(target, options) {
6990
7173
  localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
6991
7174
  registry: registryData,
6992
7175
  verbose: !!options.verbose,
7176
+ usedAnalm: !!options.analm,
7177
+ analystFindings,
6993
7178
  });
6994
7179
  if (critical.length > 0 || high.length > 0)
6995
7180
  process.exit(1);
@@ -7103,6 +7288,7 @@ async function checkRawUrl(url, options) {
7103
7288
  // Run full HMA scan + NanoMind
7104
7289
  const scanner = new index_1.HardeningScanner();
7105
7290
  const result = await scanner.scan({ targetDir: scanDir, autoFix: false });
7291
+ let analystFindings;
7106
7292
  try {
7107
7293
  const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
7108
7294
  const nmResult = await orchestrateNanoMind(scanDir, result.findings, { silent: true, analm: options.analm });
@@ -7110,6 +7296,7 @@ async function checkRawUrl(url, options) {
7110
7296
  const projectType = result.projectType || 'library';
7111
7297
  result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
7112
7298
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
7299
+ analystFindings = nmResult.analystFindings;
7113
7300
  }
7114
7301
  catch {
7115
7302
  // NanoMind unavailable — use base scan results
@@ -7122,7 +7309,7 @@ async function checkRawUrl(url, options) {
7122
7309
  const medium = failed.filter(f => f.severity === 'medium');
7123
7310
  const low = failed.filter(f => f.severity === 'low');
7124
7311
  if (options.json) {
7125
- writeJsonStdout({
7312
+ const jsonOut = {
7126
7313
  name: displayName,
7127
7314
  url,
7128
7315
  type: 'raw-url',
@@ -7131,7 +7318,10 @@ async function checkRawUrl(url, options) {
7131
7318
  score: result.score,
7132
7319
  maxScore: result.maxScore,
7133
7320
  findings: result.findings,
7134
- });
7321
+ };
7322
+ if (analystFindings?.length)
7323
+ jsonOut.analystFindings = analystFindings;
7324
+ writeJsonStdout(jsonOut);
7135
7325
  return;
7136
7326
  }
7137
7327
  // Display results using unified display
@@ -7141,6 +7331,8 @@ async function checkRawUrl(url, options) {
7141
7331
  projectType: result.projectType,
7142
7332
  localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
7143
7333
  verbose: !!options.verbose,
7334
+ usedAnalm: !!options.analm,
7335
+ analystFindings,
7144
7336
  });
7145
7337
  // Community contribution (auto-share if opted in, no first-time prompt for URLs)
7146
7338
  if (process.stdin.isTTY && !globalCiMode) {
@@ -7185,7 +7377,7 @@ async function checkNpmPackage(name, options) {
7185
7377
  writeJsonStdout({ ...registryData, source: 'registry' });
7186
7378
  return;
7187
7379
  }
7188
- displayUnifiedCheck({ name, registry: registryData, verbose: !!options.verbose });
7380
+ displayUnifiedCheck({ name, registry: registryData, verbose: !!options.verbose, usedAnalm: !!options.analm });
7189
7381
  return;
7190
7382
  }
7191
7383
  if (!options.json && !globalCiMode) {
@@ -7241,6 +7433,7 @@ async function checkNpmPackage(name, options) {
7241
7433
  const scanner = new index_1.HardeningScanner();
7242
7434
  const result = await scanner.scan({ targetDir: packageDir, autoFix: false });
7243
7435
  // Run NanoMind semantic analysis and re-filter (matches secure command pipeline)
7436
+ let analystFindings;
7244
7437
  try {
7245
7438
  const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
7246
7439
  const nmResult = await orchestrateNanoMind(packageDir, result.findings, { silent: true, analm: options.analm });
@@ -7248,6 +7441,7 @@ async function checkNpmPackage(name, options) {
7248
7441
  const projectType = result.projectType || 'library';
7249
7442
  result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
7250
7443
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
7444
+ analystFindings = nmResult.analystFindings;
7251
7445
  }
7252
7446
  catch {
7253
7447
  // NanoMind unavailable — use base scan results
@@ -7258,7 +7452,7 @@ async function checkNpmPackage(name, options) {
7258
7452
  const critical = failed.filter(f => f.severity === 'critical');
7259
7453
  const high = failed.filter(f => f.severity === 'high');
7260
7454
  if (options.json) {
7261
- writeJsonStdout({
7455
+ const jsonOut = {
7262
7456
  name,
7263
7457
  type: 'npm-package',
7264
7458
  source: 'local-scan',
@@ -7266,7 +7460,10 @@ async function checkNpmPackage(name, options) {
7266
7460
  score: result.score,
7267
7461
  maxScore: result.maxScore,
7268
7462
  findings: result.findings,
7269
- });
7463
+ };
7464
+ if (analystFindings?.length)
7465
+ jsonOut.analystFindings = analystFindings;
7466
+ writeJsonStdout(jsonOut);
7270
7467
  return;
7271
7468
  }
7272
7469
  // Await registry data (started in parallel with download)
@@ -7278,6 +7475,8 @@ async function checkNpmPackage(name, options) {
7278
7475
  localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
7279
7476
  registry: registryData,
7280
7477
  verbose: !!options.verbose,
7478
+ usedAnalm: !!options.analm,
7479
+ analystFindings,
7281
7480
  });
7282
7481
  // Community contribution (after 3 scans, interactive only)
7283
7482
  if (process.stdin.isTTY && !globalCiMode) {