hackmyagent 0.17.6 → 0.17.8

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,17 +6680,95 @@ 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
+ /\.tmLanguage\.json$/i, // TextMate grammars
6709
+ /\.schema\.json$/i, // JSON schema definitions
6710
+ /\.nls\.json$/i, // localization/NLS files
6711
+ /\bcglicenses/i, // CG license files
6712
+ /\blicenses?\//i, // license directories
6713
+ ];
6714
+ /**
6715
+ * Check IDs for security-sensitive pattern matches that produce false
6716
+ * positives on documentation and generated files. These checks look for
6717
+ * credential patterns, prompt injection, governance gaps, and skill
6718
+ * definitions — all of which appear naturally in docs that DESCRIBE
6719
+ * these concepts without being vulnerable.
6720
+ */
6721
+ const DOCS_FALSE_POSITIVE_PREFIXES = [
6722
+ 'AST-GOV', // governance checks
6723
+ 'AST-GOVERN', // governance checks
6724
+ 'AST-PROMPT', // prompt security checks
6725
+ 'AST-HEARTBEAT', // heartbeat/liveness checks
6726
+ 'AST-CRED', // credential pattern checks
6727
+ 'AST-INJECT', // injection pattern checks
6728
+ 'AST-EXFIL', // exfiltration pattern checks
6729
+ 'SKILL-', // skill definition checks
6730
+ 'SUPPLY-', // supply chain checks
6731
+ ];
6732
+ /**
6733
+ * Build scripts, CI/CD pipelines, and infrastructure files.
6734
+ * These are not runtime attack surfaces — findings here are lower risk.
6735
+ */
6736
+ const BUILD_CI_PATTERNS = [
6737
+ /\bbuild\//i, // build/ directory
6738
+ /\bdist\//i, // dist/ directory
6739
+ /\b\.github\//, // GitHub Actions
6740
+ /\b\.circleci\//, // CircleCI
6741
+ /\bazure-pipelines/i, // Azure Pipelines
6742
+ /\bjenkins/i, // Jenkins
6743
+ /\b\.gitlab-ci/, // GitLab CI
6744
+ /\bMakefile$/i, // Makefiles
6745
+ /\bGruntfile/i, // Grunt
6746
+ /\bgulpfile/i, // Gulp
6747
+ /\bwebpack\.\w+\.js$/i, // Webpack configs
6748
+ /\brollup\.\w+\.js$/i, // Rollup configs
6749
+ /\bscripts\//i, // scripts/ directory
6750
+ /\btools?\//i, // tools/ directory
6751
+ /\binfra\//i, // infra/ directory
6752
+ ];
6606
6753
  function isTestFile(filePath) {
6607
6754
  return TEST_FILE_PATTERNS.some(p => p.test(filePath));
6608
6755
  }
6756
+ function isDocsOrGenerated(filePath) {
6757
+ return DOCS_AND_GENERATED_PATTERNS.some(p => p.test(filePath));
6758
+ }
6759
+ function isBuildOrCiFile(filePath) {
6760
+ return BUILD_CI_PATTERNS.some(p => p.test(filePath));
6761
+ }
6609
6762
  function isAiToolingFile(filePath) {
6610
6763
  return AI_TOOLING_PATH_PATTERNS.some(p => p.test(filePath));
6611
6764
  }
6612
6765
  /**
6613
6766
  * Filter out local-dev-only findings that are meaningless for downloaded
6614
6767
  * 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.
6768
+ * governance findings on AI tooling files, removes false-positive
6769
+ * pattern matches on documentation/generated files, and demotes test
6770
+ * and build file findings. Mutates `result.findings` in place and
6771
+ * recalculates the score.
6617
6772
  */
6618
6773
  function filterLocalOnlyFindings(result, scanner) {
6619
6774
  result.findings = result.findings.filter(f => {
@@ -6626,14 +6781,29 @@ function filterLocalOnlyFindings(result, scanner) {
6626
6781
  // files are false positives — they describe security practices, not vulnerabilities.
6627
6782
  if (f.file && isAiToolingFile(f.file))
6628
6783
  return false;
6784
+ // Exclude governance/credential/prompt/skill pattern-match findings on
6785
+ // documentation, generated specs, and vendored files. These files
6786
+ // naturally describe security concepts without being vulnerable.
6787
+ // Structural checks (unicode steganography, TOCTOU, deserialization)
6788
+ // are kept — those detect actual content issues regardless of file type.
6789
+ if (f.file && isDocsOrGenerated(f.file)) {
6790
+ const checkId = f.checkId || '';
6791
+ if (DOCS_FALSE_POSITIVE_PREFIXES.some(p => checkId.startsWith(p))) {
6792
+ return false;
6793
+ }
6794
+ }
6629
6795
  return true;
6630
6796
  });
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)
6797
+ // Demote test file and build script findings to low severity.
6798
+ // Test code patterns are lower risk (pickle.load in a test file is not
6799
+ // an attack surface). Build scripts, CI configs, and vendored code are
6800
+ // not runtime attack surfaces for the end user.
6633
6801
  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';
6802
+ if (f.file && (f.severity === 'critical' || f.severity === 'high')) {
6803
+ if (isTestFile(f.file) || isBuildOrCiFile(f.file)) {
6804
+ f.originalSeverity = f.severity;
6805
+ f.severity = 'low';
6806
+ }
6637
6807
  }
6638
6808
  }
6639
6809
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
@@ -6666,10 +6836,16 @@ function printCheckNextSteps(target, context) {
6666
6836
  }
6667
6837
  if (context?.hasFindings) {
6668
6838
  console.log(` ${colors.cyan}Full project audit:${RESET()} ${getFullScanHint()}`);
6839
+ if (!context?.usedAnalm) {
6840
+ console.log(` ${colors.cyan}AI analysis:${RESET()} ${CLI_PREFIX} check ${target} --analm`);
6841
+ }
6669
6842
  }
6670
6843
  else if (context?.isCleanScan && isLocal) {
6671
6844
  console.log(` ${colors.cyan}Governance scan:${RESET()} ${CLI_PREFIX} scan-soul ${target}`);
6672
6845
  console.log(` ${colors.cyan}Red-team test:${RESET()} ${CLI_PREFIX} attack --local`);
6846
+ if (!context?.usedAnalm) {
6847
+ console.log(` ${colors.cyan}AI analysis:${RESET()} ${CLI_PREFIX} check ${target} --analm`);
6848
+ }
6673
6849
  }
6674
6850
  else if (context?.isCleanScan) {
6675
6851
  if (!context?.usedAnalm) {
@@ -6785,6 +6961,7 @@ async function checkGitHubRepo(target, options) {
6785
6961
  const scanner = new index_1.HardeningScanner();
6786
6962
  const result = await scanner.scan({ targetDir: repoDir, autoFix: false });
6787
6963
  // Run NanoMind semantic analysis and re-filter
6964
+ let analystFindings;
6788
6965
  try {
6789
6966
  const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
6790
6967
  const nmResult = await orchestrateNanoMind(repoDir, result.findings, { silent: true, analm: options.analm });
@@ -6792,6 +6969,7 @@ async function checkGitHubRepo(target, options) {
6792
6969
  const projectType = result.projectType || 'library';
6793
6970
  result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
6794
6971
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
6972
+ analystFindings = nmResult.analystFindings;
6795
6973
  }
6796
6974
  catch {
6797
6975
  // NanoMind unavailable — use base scan results
@@ -6804,7 +6982,7 @@ async function checkGitHubRepo(target, options) {
6804
6982
  const medium = failed.filter(f => f.severity === 'medium');
6805
6983
  const low = failed.filter(f => f.severity === 'low');
6806
6984
  if (options.json) {
6807
- writeJsonStdout({
6985
+ const jsonOut = {
6808
6986
  name: displayName,
6809
6987
  type: 'github-repo',
6810
6988
  source: 'local-scan',
@@ -6812,7 +6990,10 @@ async function checkGitHubRepo(target, options) {
6812
6990
  score: result.score,
6813
6991
  maxScore: result.maxScore,
6814
6992
  findings: result.findings,
6815
- });
6993
+ };
6994
+ if (analystFindings?.length)
6995
+ jsonOut.analystFindings = analystFindings;
6996
+ writeJsonStdout(jsonOut);
6816
6997
  return;
6817
6998
  }
6818
6999
  // Await registry data (started in parallel with clone)
@@ -6825,6 +7006,8 @@ async function checkGitHubRepo(target, options) {
6825
7006
  localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
6826
7007
  registry: registryData,
6827
7008
  verbose: !!options.verbose,
7009
+ usedAnalm: !!options.analm,
7010
+ analystFindings,
6828
7011
  });
6829
7012
  // Community contribution
6830
7013
  if (process.stdin.isTTY && !globalCiMode) {
@@ -6948,6 +7131,7 @@ async function checkPyPiPackage(target, options) {
6948
7131
  const scanner = new index_1.HardeningScanner();
6949
7132
  const result = await scanner.scan({ targetDir: extractDir, autoFix: false });
6950
7133
  // Run NanoMind semantic analysis and re-filter
7134
+ let analystFindings;
6951
7135
  try {
6952
7136
  const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
6953
7137
  const nmResult = await orchestrateNanoMind(extractDir, result.findings, { silent: true, analm: options.analm });
@@ -6955,6 +7139,7 @@ async function checkPyPiPackage(target, options) {
6955
7139
  const projectType = result.projectType || 'library';
6956
7140
  result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
6957
7141
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
7142
+ analystFindings = nmResult.analystFindings;
6958
7143
  }
6959
7144
  catch {
6960
7145
  // NanoMind unavailable -- use base scan results
@@ -6967,7 +7152,7 @@ async function checkPyPiPackage(target, options) {
6967
7152
  const medium = failed.filter(f => f.severity === 'medium');
6968
7153
  const low = failed.filter(f => f.severity === 'low');
6969
7154
  if (options.json) {
6970
- writeJsonStdout({
7155
+ const jsonOut = {
6971
7156
  name,
6972
7157
  type: 'pypi-package',
6973
7158
  source: 'local-scan',
@@ -6976,7 +7161,10 @@ async function checkPyPiPackage(target, options) {
6976
7161
  score: result.score,
6977
7162
  maxScore: result.maxScore,
6978
7163
  findings: result.findings,
6979
- });
7164
+ };
7165
+ if (analystFindings?.length)
7166
+ jsonOut.analystFindings = analystFindings;
7167
+ writeJsonStdout(jsonOut);
6980
7168
  return;
6981
7169
  }
6982
7170
  // Display results using unified display
@@ -6990,6 +7178,8 @@ async function checkPyPiPackage(target, options) {
6990
7178
  localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
6991
7179
  registry: registryData,
6992
7180
  verbose: !!options.verbose,
7181
+ usedAnalm: !!options.analm,
7182
+ analystFindings,
6993
7183
  });
6994
7184
  if (critical.length > 0 || high.length > 0)
6995
7185
  process.exit(1);
@@ -7103,6 +7293,7 @@ async function checkRawUrl(url, options) {
7103
7293
  // Run full HMA scan + NanoMind
7104
7294
  const scanner = new index_1.HardeningScanner();
7105
7295
  const result = await scanner.scan({ targetDir: scanDir, autoFix: false });
7296
+ let analystFindings;
7106
7297
  try {
7107
7298
  const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
7108
7299
  const nmResult = await orchestrateNanoMind(scanDir, result.findings, { silent: true, analm: options.analm });
@@ -7110,6 +7301,7 @@ async function checkRawUrl(url, options) {
7110
7301
  const projectType = result.projectType || 'library';
7111
7302
  result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
7112
7303
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
7304
+ analystFindings = nmResult.analystFindings;
7113
7305
  }
7114
7306
  catch {
7115
7307
  // NanoMind unavailable — use base scan results
@@ -7122,7 +7314,7 @@ async function checkRawUrl(url, options) {
7122
7314
  const medium = failed.filter(f => f.severity === 'medium');
7123
7315
  const low = failed.filter(f => f.severity === 'low');
7124
7316
  if (options.json) {
7125
- writeJsonStdout({
7317
+ const jsonOut = {
7126
7318
  name: displayName,
7127
7319
  url,
7128
7320
  type: 'raw-url',
@@ -7131,7 +7323,10 @@ async function checkRawUrl(url, options) {
7131
7323
  score: result.score,
7132
7324
  maxScore: result.maxScore,
7133
7325
  findings: result.findings,
7134
- });
7326
+ };
7327
+ if (analystFindings?.length)
7328
+ jsonOut.analystFindings = analystFindings;
7329
+ writeJsonStdout(jsonOut);
7135
7330
  return;
7136
7331
  }
7137
7332
  // Display results using unified display
@@ -7141,6 +7336,8 @@ async function checkRawUrl(url, options) {
7141
7336
  projectType: result.projectType,
7142
7337
  localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
7143
7338
  verbose: !!options.verbose,
7339
+ usedAnalm: !!options.analm,
7340
+ analystFindings,
7144
7341
  });
7145
7342
  // Community contribution (auto-share if opted in, no first-time prompt for URLs)
7146
7343
  if (process.stdin.isTTY && !globalCiMode) {
@@ -7185,7 +7382,7 @@ async function checkNpmPackage(name, options) {
7185
7382
  writeJsonStdout({ ...registryData, source: 'registry' });
7186
7383
  return;
7187
7384
  }
7188
- displayUnifiedCheck({ name, registry: registryData, verbose: !!options.verbose });
7385
+ displayUnifiedCheck({ name, registry: registryData, verbose: !!options.verbose, usedAnalm: !!options.analm });
7189
7386
  return;
7190
7387
  }
7191
7388
  if (!options.json && !globalCiMode) {
@@ -7241,6 +7438,7 @@ async function checkNpmPackage(name, options) {
7241
7438
  const scanner = new index_1.HardeningScanner();
7242
7439
  const result = await scanner.scan({ targetDir: packageDir, autoFix: false });
7243
7440
  // Run NanoMind semantic analysis and re-filter (matches secure command pipeline)
7441
+ let analystFindings;
7244
7442
  try {
7245
7443
  const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
7246
7444
  const nmResult = await orchestrateNanoMind(packageDir, result.findings, { silent: true, analm: options.analm });
@@ -7248,6 +7446,7 @@ async function checkNpmPackage(name, options) {
7248
7446
  const projectType = result.projectType || 'library';
7249
7447
  result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
7250
7448
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
7449
+ analystFindings = nmResult.analystFindings;
7251
7450
  }
7252
7451
  catch {
7253
7452
  // NanoMind unavailable — use base scan results
@@ -7258,7 +7457,7 @@ async function checkNpmPackage(name, options) {
7258
7457
  const critical = failed.filter(f => f.severity === 'critical');
7259
7458
  const high = failed.filter(f => f.severity === 'high');
7260
7459
  if (options.json) {
7261
- writeJsonStdout({
7460
+ const jsonOut = {
7262
7461
  name,
7263
7462
  type: 'npm-package',
7264
7463
  source: 'local-scan',
@@ -7266,7 +7465,10 @@ async function checkNpmPackage(name, options) {
7266
7465
  score: result.score,
7267
7466
  maxScore: result.maxScore,
7268
7467
  findings: result.findings,
7269
- });
7468
+ };
7469
+ if (analystFindings?.length)
7470
+ jsonOut.analystFindings = analystFindings;
7471
+ writeJsonStdout(jsonOut);
7270
7472
  return;
7271
7473
  }
7272
7474
  // Await registry data (started in parallel with download)
@@ -7278,6 +7480,8 @@ async function checkNpmPackage(name, options) {
7278
7480
  localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
7279
7481
  registry: registryData,
7280
7482
  verbose: !!options.verbose,
7483
+ usedAnalm: !!options.analm,
7484
+ analystFindings,
7281
7485
  });
7282
7486
  // Community contribution (after 3 scans, interactive only)
7283
7487
  if (process.stdin.isTTY && !globalCiMode) {