hackmyagent 0.17.5 → 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
@@ -246,6 +246,8 @@ Examples:
246
246
  findings: issues,
247
247
  },
248
248
  verbose: !!options.verbose,
249
+ usedAnalm: !!options.analm,
250
+ analystFindings: nmResult.analystFindings,
249
251
  });
250
252
  const risk = critical.length > 0 ? 'critical' : high.length > 0 ? 'high' : issues.length > 0 ? 'medium' : 'low';
251
253
  if (risk === 'critical' || risk === 'high')
@@ -452,7 +454,7 @@ function shortenPath(filePath) {
452
454
  return parts.slice(-2).join('/');
453
455
  }
454
456
  function displayUnifiedCheck(opts) {
455
- const { name, sourceLabel, projectType, localScan, registry, verbose, version, nanomindScan } = opts;
457
+ const { name, sourceLabel, projectType, localScan, registry, verbose, version, nanomindScan, usedAnalm } = opts;
456
458
  // ── Visual helpers ──────────────────────────────────────────────────
457
459
  const METER_WIDTH = 20;
458
460
  const divider = (label) => {
@@ -772,6 +774,81 @@ function displayUnifiedCheck(opts) {
772
774
  console.log(` ${colors.dim}${legend}${RESET()}`);
773
775
  }
774
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
+ }
775
852
  // ── Next steps ──────────────────────────────────────────────────────
776
853
  const hasGovIssues = failed.some(f => f.category === 'governance' || f.category === 'Governance' || f.checkId?.startsWith('AST-GOV') || f.checkId?.startsWith('AST-PROMPT'));
777
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'));
@@ -787,6 +864,7 @@ function displayUnifiedCheck(opts) {
787
864
  hasCredentialFindings: hasCredIssues,
788
865
  hasCodeVulns,
789
866
  isCleanScan: totalFindings === 0 && (!!localScan || !!nanomindScan),
867
+ usedAnalm,
790
868
  });
791
869
  }
792
870
  function groupFindingsBySeverity(findings) {
@@ -2778,9 +2856,26 @@ Examples:
2778
2856
  if (r.reasoning)
2779
2857
  console.log(` ${r.reasoning}`);
2780
2858
  }
2859
+ else if (af.taskType === 'intelReport') {
2860
+ if (r.summary)
2861
+ console.log(` ${colors.cyan}Summary:${RESET()} ${r.summary}`);
2862
+ if (Array.isArray(r.keyFindings) && r.keyFindings.length > 0) {
2863
+ for (const kf of r.keyFindings) {
2864
+ console.log(` ${kf}`);
2865
+ }
2866
+ }
2867
+ if (r.riskAssessment)
2868
+ console.log(` ${colors.cyan}Risk:${RESET()} ${r.riskAssessment}`);
2869
+ if (Array.isArray(r.recommendations) && r.recommendations.length > 0) {
2870
+ for (const rec of r.recommendations) {
2871
+ console.log(` ${colors.dim}${rec}${RESET()}`);
2872
+ }
2873
+ }
2874
+ }
2781
2875
  else {
2782
2876
  // Generic display for other task types
2783
- console.log(` ${af.taskType}: ${JSON.stringify(r)}`);
2877
+ if (r.description)
2878
+ console.log(` ${r.description}`);
2784
2879
  }
2785
2880
  console.log(` ${colors.dim}Confidence: ${Math.round(af.confidence * 100)}% | ${af.modelVersion} (${af.durationMs}ms)${RESET()}`);
2786
2881
  console.log();
@@ -6300,7 +6395,8 @@ function looksLikeRawUrl(target) {
6300
6395
  */
6301
6396
  function parseGitHubTarget(target) {
6302
6397
  // Full URL: https://github.com/org/repo[.git][/tree/...]
6303
- 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)?(?:\/|$)/);
6304
6400
  if (urlMatch) {
6305
6401
  return {
6306
6402
  org: urlMatch[2],
@@ -6584,6 +6680,77 @@ const TEST_FILE_PATTERNS = [
6584
6680
  /[^/]+\.spec\.\w+$/,
6585
6681
  /\bfixtures?\//i,
6586
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
+ ];
6587
6754
  function isTestFile(filePath) {
6588
6755
  return TEST_FILE_PATTERNS.some(p => p.test(filePath));
6589
6756
  }
@@ -6593,8 +6760,10 @@ function isAiToolingFile(filePath) {
6593
6760
  /**
6594
6761
  * Filter out local-dev-only findings that are meaningless for downloaded
6595
6762
  * packages (e.g. "Missing .gitignore" on an npm tarball). Also filters
6596
- * governance findings on AI tooling files and demotes test file findings.
6597
- * 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.
6598
6767
  */
6599
6768
  function filterLocalOnlyFindings(result, scanner) {
6600
6769
  result.findings = result.findings.filter(f => {
@@ -6607,14 +6776,29 @@ function filterLocalOnlyFindings(result, scanner) {
6607
6776
  // files are false positives — they describe security practices, not vulnerabilities.
6608
6777
  if (f.file && isAiToolingFile(f.file))
6609
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
+ }
6610
6790
  return true;
6611
6791
  });
6612
- // Demote test file findings to low severity (test code patterns are
6613
- // 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.
6614
6796
  for (const f of result.findings) {
6615
- if (f.file && isTestFile(f.file) && (f.severity === 'critical' || f.severity === 'high')) {
6616
- f.originalSeverity = f.severity;
6617
- 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
+ }
6618
6802
  }
6619
6803
  }
6620
6804
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
@@ -6647,16 +6831,26 @@ function printCheckNextSteps(target, context) {
6647
6831
  }
6648
6832
  if (context?.hasFindings) {
6649
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
+ }
6650
6837
  }
6651
6838
  else if (context?.isCleanScan && isLocal) {
6652
6839
  console.log(` ${colors.cyan}Governance scan:${RESET()} ${CLI_PREFIX} scan-soul ${target}`);
6653
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
+ }
6654
6844
  }
6655
6845
  else if (context?.isCleanScan) {
6656
- console.log(` ${colors.cyan}AI analysis:${RESET()} ${CLI_PREFIX} check ${target} --analm`);
6846
+ if (!context?.usedAnalm) {
6847
+ console.log(` ${colors.cyan}AI analysis:${RESET()} ${CLI_PREFIX} check ${target} --analm`);
6848
+ }
6657
6849
  }
6658
6850
  else {
6659
- console.log(` ${colors.cyan}AI analysis:${RESET()} ${CLI_PREFIX} check ${target} --analm`);
6851
+ if (!context?.usedAnalm) {
6852
+ console.log(` ${colors.cyan}AI analysis:${RESET()} ${CLI_PREFIX} check ${target} --analm`);
6853
+ }
6660
6854
  }
6661
6855
  console.log();
6662
6856
  }
@@ -6736,7 +6930,7 @@ async function checkGitHubRepo(target, options) {
6736
6930
  writeJsonStdout({ ...registryData, source: 'registry' });
6737
6931
  return;
6738
6932
  }
6739
- displayUnifiedCheck({ name: displayName, sourceLabel: 'GitHub', registry: registryData, verbose: !!options.verbose });
6933
+ displayUnifiedCheck({ name: displayName, sourceLabel: 'GitHub', registry: registryData, verbose: !!options.verbose, usedAnalm: !!options.analm });
6740
6934
  return;
6741
6935
  }
6742
6936
  if (!options.json && !globalCiMode) {
@@ -6762,6 +6956,7 @@ async function checkGitHubRepo(target, options) {
6762
6956
  const scanner = new index_1.HardeningScanner();
6763
6957
  const result = await scanner.scan({ targetDir: repoDir, autoFix: false });
6764
6958
  // Run NanoMind semantic analysis and re-filter
6959
+ let analystFindings;
6765
6960
  try {
6766
6961
  const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
6767
6962
  const nmResult = await orchestrateNanoMind(repoDir, result.findings, { silent: true, analm: options.analm });
@@ -6769,6 +6964,7 @@ async function checkGitHubRepo(target, options) {
6769
6964
  const projectType = result.projectType || 'library';
6770
6965
  result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
6771
6966
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
6967
+ analystFindings = nmResult.analystFindings;
6772
6968
  }
6773
6969
  catch {
6774
6970
  // NanoMind unavailable — use base scan results
@@ -6781,7 +6977,7 @@ async function checkGitHubRepo(target, options) {
6781
6977
  const medium = failed.filter(f => f.severity === 'medium');
6782
6978
  const low = failed.filter(f => f.severity === 'low');
6783
6979
  if (options.json) {
6784
- writeJsonStdout({
6980
+ const jsonOut = {
6785
6981
  name: displayName,
6786
6982
  type: 'github-repo',
6787
6983
  source: 'local-scan',
@@ -6789,7 +6985,10 @@ async function checkGitHubRepo(target, options) {
6789
6985
  score: result.score,
6790
6986
  maxScore: result.maxScore,
6791
6987
  findings: result.findings,
6792
- });
6988
+ };
6989
+ if (analystFindings?.length)
6990
+ jsonOut.analystFindings = analystFindings;
6991
+ writeJsonStdout(jsonOut);
6793
6992
  return;
6794
6993
  }
6795
6994
  // Await registry data (started in parallel with clone)
@@ -6802,6 +7001,8 @@ async function checkGitHubRepo(target, options) {
6802
7001
  localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
6803
7002
  registry: registryData,
6804
7003
  verbose: !!options.verbose,
7004
+ usedAnalm: !!options.analm,
7005
+ analystFindings,
6805
7006
  });
6806
7007
  // Community contribution
6807
7008
  if (process.stdin.isTTY && !globalCiMode) {
@@ -6925,6 +7126,7 @@ async function checkPyPiPackage(target, options) {
6925
7126
  const scanner = new index_1.HardeningScanner();
6926
7127
  const result = await scanner.scan({ targetDir: extractDir, autoFix: false });
6927
7128
  // Run NanoMind semantic analysis and re-filter
7129
+ let analystFindings;
6928
7130
  try {
6929
7131
  const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
6930
7132
  const nmResult = await orchestrateNanoMind(extractDir, result.findings, { silent: true, analm: options.analm });
@@ -6932,6 +7134,7 @@ async function checkPyPiPackage(target, options) {
6932
7134
  const projectType = result.projectType || 'library';
6933
7135
  result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
6934
7136
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
7137
+ analystFindings = nmResult.analystFindings;
6935
7138
  }
6936
7139
  catch {
6937
7140
  // NanoMind unavailable -- use base scan results
@@ -6944,7 +7147,7 @@ async function checkPyPiPackage(target, options) {
6944
7147
  const medium = failed.filter(f => f.severity === 'medium');
6945
7148
  const low = failed.filter(f => f.severity === 'low');
6946
7149
  if (options.json) {
6947
- writeJsonStdout({
7150
+ const jsonOut = {
6948
7151
  name,
6949
7152
  type: 'pypi-package',
6950
7153
  source: 'local-scan',
@@ -6953,7 +7156,10 @@ async function checkPyPiPackage(target, options) {
6953
7156
  score: result.score,
6954
7157
  maxScore: result.maxScore,
6955
7158
  findings: result.findings,
6956
- });
7159
+ };
7160
+ if (analystFindings?.length)
7161
+ jsonOut.analystFindings = analystFindings;
7162
+ writeJsonStdout(jsonOut);
6957
7163
  return;
6958
7164
  }
6959
7165
  // Display results using unified display
@@ -6967,6 +7173,8 @@ async function checkPyPiPackage(target, options) {
6967
7173
  localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
6968
7174
  registry: registryData,
6969
7175
  verbose: !!options.verbose,
7176
+ usedAnalm: !!options.analm,
7177
+ analystFindings,
6970
7178
  });
6971
7179
  if (critical.length > 0 || high.length > 0)
6972
7180
  process.exit(1);
@@ -7080,6 +7288,7 @@ async function checkRawUrl(url, options) {
7080
7288
  // Run full HMA scan + NanoMind
7081
7289
  const scanner = new index_1.HardeningScanner();
7082
7290
  const result = await scanner.scan({ targetDir: scanDir, autoFix: false });
7291
+ let analystFindings;
7083
7292
  try {
7084
7293
  const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
7085
7294
  const nmResult = await orchestrateNanoMind(scanDir, result.findings, { silent: true, analm: options.analm });
@@ -7087,6 +7296,7 @@ async function checkRawUrl(url, options) {
7087
7296
  const projectType = result.projectType || 'library';
7088
7297
  result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
7089
7298
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
7299
+ analystFindings = nmResult.analystFindings;
7090
7300
  }
7091
7301
  catch {
7092
7302
  // NanoMind unavailable — use base scan results
@@ -7099,7 +7309,7 @@ async function checkRawUrl(url, options) {
7099
7309
  const medium = failed.filter(f => f.severity === 'medium');
7100
7310
  const low = failed.filter(f => f.severity === 'low');
7101
7311
  if (options.json) {
7102
- writeJsonStdout({
7312
+ const jsonOut = {
7103
7313
  name: displayName,
7104
7314
  url,
7105
7315
  type: 'raw-url',
@@ -7108,7 +7318,10 @@ async function checkRawUrl(url, options) {
7108
7318
  score: result.score,
7109
7319
  maxScore: result.maxScore,
7110
7320
  findings: result.findings,
7111
- });
7321
+ };
7322
+ if (analystFindings?.length)
7323
+ jsonOut.analystFindings = analystFindings;
7324
+ writeJsonStdout(jsonOut);
7112
7325
  return;
7113
7326
  }
7114
7327
  // Display results using unified display
@@ -7118,6 +7331,8 @@ async function checkRawUrl(url, options) {
7118
7331
  projectType: result.projectType,
7119
7332
  localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
7120
7333
  verbose: !!options.verbose,
7334
+ usedAnalm: !!options.analm,
7335
+ analystFindings,
7121
7336
  });
7122
7337
  // Community contribution (auto-share if opted in, no first-time prompt for URLs)
7123
7338
  if (process.stdin.isTTY && !globalCiMode) {
@@ -7162,7 +7377,7 @@ async function checkNpmPackage(name, options) {
7162
7377
  writeJsonStdout({ ...registryData, source: 'registry' });
7163
7378
  return;
7164
7379
  }
7165
- displayUnifiedCheck({ name, registry: registryData, verbose: !!options.verbose });
7380
+ displayUnifiedCheck({ name, registry: registryData, verbose: !!options.verbose, usedAnalm: !!options.analm });
7166
7381
  return;
7167
7382
  }
7168
7383
  if (!options.json && !globalCiMode) {
@@ -7218,6 +7433,7 @@ async function checkNpmPackage(name, options) {
7218
7433
  const scanner = new index_1.HardeningScanner();
7219
7434
  const result = await scanner.scan({ targetDir: packageDir, autoFix: false });
7220
7435
  // Run NanoMind semantic analysis and re-filter (matches secure command pipeline)
7436
+ let analystFindings;
7221
7437
  try {
7222
7438
  const { orchestrateNanoMind } = await Promise.resolve().then(() => __importStar(require('./nanomind-core/orchestrate.js')));
7223
7439
  const nmResult = await orchestrateNanoMind(packageDir, result.findings, { silent: true, analm: options.analm });
@@ -7225,6 +7441,7 @@ async function checkNpmPackage(name, options) {
7225
7441
  const projectType = result.projectType || 'library';
7226
7442
  result.findings = refiltered.filter((f) => !f.passed && f.file && scanner.findingAppliesTo(f, projectType));
7227
7443
  result.score = scanner.calculateScore(result.findings.filter((f) => !f.passed && !f.fixed)).score;
7444
+ analystFindings = nmResult.analystFindings;
7228
7445
  }
7229
7446
  catch {
7230
7447
  // NanoMind unavailable — use base scan results
@@ -7235,7 +7452,7 @@ async function checkNpmPackage(name, options) {
7235
7452
  const critical = failed.filter(f => f.severity === 'critical');
7236
7453
  const high = failed.filter(f => f.severity === 'high');
7237
7454
  if (options.json) {
7238
- writeJsonStdout({
7455
+ const jsonOut = {
7239
7456
  name,
7240
7457
  type: 'npm-package',
7241
7458
  source: 'local-scan',
@@ -7243,7 +7460,10 @@ async function checkNpmPackage(name, options) {
7243
7460
  score: result.score,
7244
7461
  maxScore: result.maxScore,
7245
7462
  findings: result.findings,
7246
- });
7463
+ };
7464
+ if (analystFindings?.length)
7465
+ jsonOut.analystFindings = analystFindings;
7466
+ writeJsonStdout(jsonOut);
7247
7467
  return;
7248
7468
  }
7249
7469
  // Await registry data (started in parallel with download)
@@ -7255,6 +7475,8 @@ async function checkNpmPackage(name, options) {
7255
7475
  localScan: { score: result.score, maxScore: result.maxScore, findings: result.findings },
7256
7476
  registry: registryData,
7257
7477
  verbose: !!options.verbose,
7478
+ usedAnalm: !!options.analm,
7479
+ analystFindings,
7258
7480
  });
7259
7481
  // Community contribution (after 3 scans, interactive only)
7260
7482
  if (process.stdin.isTTY && !globalCiMode) {