hackmyagent 0.11.2 → 0.11.4

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
@@ -1538,43 +1538,22 @@ function resolvePackageVersion(targetDir) {
1538
1538
  catch { /* ignore */ }
1539
1539
  return null;
1540
1540
  }
1541
- /**
1542
- * Build an anonymized contribution summary from scan findings.
1543
- * Contains only aggregate data -- no file paths, no code, no project names.
1544
- */
1545
- function buildContributionSummary(findings, score, projectType) {
1546
- const failed = findings.filter((f) => !f.passed);
1547
- const categories = [...new Set(failed.map((f) => f.category).filter(Boolean))].sort();
1548
- const counts = { critical: 0, high: 0, medium: 0, low: 0 };
1549
- for (const f of failed) {
1550
- if (f.severity in counts)
1551
- counts[f.severity]++;
1552
- }
1553
- return {
1554
- packageType: projectType,
1555
- findingCategories: categories,
1556
- findingCounts: counts,
1557
- score,
1558
- toolVersion: index_1.VERSION,
1559
- };
1560
- }
1561
1541
  /**
1562
1542
  * Handle community contribution after a scan completes.
1563
1543
  *
1564
1544
  * Determines whether to contribute based on:
1565
1545
  * 1. --contribute / --no-contribute CLI flags (highest priority)
1566
- * 2. --ci mode: skip prompt, only contribute if --contribute is explicit
1567
- * 3. ~/.opena2a/config.json contribute.enabled setting
1568
- * 4. Interactive opt-in prompt (first scan or scan #10)
1546
+ * 2. ~/.opena2a/config.json contribute.enabled setting
1547
+ * 3. Interactive opt-in prompt (first scan or scan #10)
1569
1548
  *
1570
- * Shows what will be sent before sending (transparency).
1549
+ * If contributing, builds an anonymized payload and submits it
1550
+ * asynchronously (non-blocking). Failures are logged as warnings.
1571
1551
  */
1572
- async function handleContribution(contributeFlag, targetDir, findings, registryUrl, format, contributionOptions) {
1552
+ async function handleContribution(contributeFlag, targetDir, findings, registryUrl, format) {
1573
1553
  try {
1574
1554
  const { isContributeEnabled, shouldPromptContribute, showContributePrompt, incrementScanCount, buildContributionPayloadFromDir, submitContribution, } = await Promise.resolve().then(() => __importStar(require('./telemetry')));
1575
1555
  // Always increment scan count
1576
1556
  incrementScanCount();
1577
- const isCi = contributionOptions?.ci === true;
1578
1557
  // Determine whether to contribute
1579
1558
  let shouldContribute;
1580
1559
  if (contributeFlag === true) {
@@ -1585,10 +1564,6 @@ async function handleContribution(contributeFlag, targetDir, findings, registryU
1585
1564
  // --no-contribute flag: skip this scan
1586
1565
  shouldContribute = false;
1587
1566
  }
1588
- else if (isCi) {
1589
- // CI mode: never prompt, only contribute if --contribute was explicit
1590
- shouldContribute = false;
1591
- }
1592
1567
  else {
1593
1568
  // Check config
1594
1569
  const configSetting = isContributeEnabled();
@@ -1599,7 +1574,7 @@ async function handleContribution(contributeFlag, targetDir, findings, registryU
1599
1574
  shouldContribute = false;
1600
1575
  }
1601
1576
  else {
1602
- // Not configured -- prompt (interactive TTY only)
1577
+ // Not configured -- prompt after 3 scans (interactive TTY only)
1603
1578
  if (format === 'text' && process.stdout.isTTY && shouldPromptContribute()) {
1604
1579
  shouldContribute = await showContributePrompt();
1605
1580
  }
@@ -1610,21 +1585,6 @@ async function handleContribution(contributeFlag, targetDir, findings, registryU
1610
1585
  }
1611
1586
  if (!shouldContribute)
1612
1587
  return;
1613
- // Build the anonymized contribution summary for transparency display
1614
- const score = contributionOptions?.score ?? 0;
1615
- const projectType = contributionOptions?.projectType ?? 'unknown';
1616
- const summary = buildContributionSummary(findings, score, projectType);
1617
- // Transparency: show what will be sent before sending
1618
- if (format === 'text' && !isCi) {
1619
- console.log('');
1620
- console.log('Contributing anonymized scan summary to OpenA2A Registry:');
1621
- console.log(` Package type: ${summary.packageType}`);
1622
- console.log(` Categories: ${summary.findingCategories.join(', ') || '(none)'}`);
1623
- console.log(` Findings: ${summary.findingCounts.critical} critical, ${summary.findingCounts.high} high, ${summary.findingCounts.medium} medium, ${summary.findingCounts.low} low`);
1624
- console.log(` Score: ${summary.score}`);
1625
- console.log(` Tool version: ${summary.toolVersion}`);
1626
- console.log(' (No file paths, code, or project names are sent)');
1627
- }
1628
1588
  // Build and submit contribution (non-blocking)
1629
1589
  const packageName = resolvePackageName(targetDir);
1630
1590
  if (!packageName)
@@ -1646,7 +1606,7 @@ async function handleContribution(contributeFlag, targetDir, findings, registryU
1646
1606
  * Converts SoulScanResult controls into SecurityFinding-like objects
1647
1607
  * for the contribution module, then delegates to handleContribution.
1648
1608
  */
1649
- async function handleSoulContribution(contributeFlag, targetDir, result, registryUrl, format, contributionOptions) {
1609
+ async function handleSoulContribution(contributeFlag, targetDir, result, registryUrl, format) {
1650
1610
  // Convert soul controls into SecurityFinding-shaped objects
1651
1611
  const findings = [];
1652
1612
  for (const domain of result.domains) {
@@ -1665,11 +1625,7 @@ async function handleSoulContribution(contributeFlag, targetDir, result, registr
1665
1625
  });
1666
1626
  }
1667
1627
  }
1668
- await handleContribution(contributeFlag, targetDir, findings, registryUrl, format, {
1669
- ci: contributionOptions?.ci,
1670
- score: result.score,
1671
- projectType: 'soul',
1672
- });
1628
+ await handleContribution(contributeFlag, targetDir, findings, registryUrl, format);
1673
1629
  }
1674
1630
  program
1675
1631
  .command('secure')
@@ -1730,10 +1686,18 @@ Examples:
1730
1686
  .option('--registry-key <key>', 'Registry API key (default: REGISTRY_API_KEY env)')
1731
1687
  .option('--contribute', 'Share anonymized scan findings with OpenA2A Registry (overrides config)')
1732
1688
  .option('--no-contribute', 'Do not share findings for this scan (overrides config)')
1733
- .option('--ci', 'CI mode: suppress interactive prompts, machine-friendly output')
1689
+ .option('--ci', 'CI mode: suppress interactive prompts, exit non-zero on findings')
1734
1690
  .action(async (directory, options) => {
1735
1691
  try {
1736
1692
  const targetDir = directory.startsWith('/') ? directory : process.cwd() + '/' + directory;
1693
+ // CI mode: force non-interactive defaults
1694
+ if (options.ci) {
1695
+ if (!options.format && !options.json)
1696
+ options.format = 'text';
1697
+ // In CI, never prompt -- only contribute if explicitly --contribute
1698
+ if (options.contribute === undefined)
1699
+ options.contribute = false;
1700
+ }
1737
1701
  // Check if directory exists
1738
1702
  if (!require('fs').existsSync(targetDir)) {
1739
1703
  console.error(`Error: Directory '${targetDir}' does not exist.`);
@@ -1935,7 +1899,7 @@ Examples:
1935
1899
  writeJsonStdout(jsonOutput);
1936
1900
  }
1937
1901
  // Community contribution (non-blocking, runs in JSON mode too)
1938
- await handleContribution(options.contribute, targetDir, result.findings, options.registryUrl, format, { ci: options.ci, score: result.score, projectType: result.projectType });
1902
+ await handleContribution(options.contribute, targetDir, result.findings, options.registryUrl, format);
1939
1903
  const critHigh = result.findings.filter((f) => !f.passed && !f.fixed && (f.severity === 'critical' || f.severity === 'high'));
1940
1904
  if (critHigh.length > 0)
1941
1905
  process.exitCode = 1;
@@ -2015,10 +1979,11 @@ Examples:
2015
1979
  ? `${finding.file}:${finding.line}`
2016
1980
  : finding.file
2017
1981
  : '';
2018
- // Format: SEVERITY file:line
1982
+ // Format: SEVERITY [DRY RUN] Would fix: file:line
2019
1983
  // Description
2020
1984
  // Fix: command
2021
- console.log(`${display.color()}${display.symbol} ${finding.severity.toUpperCase()}${RESET()} ${location}`);
1985
+ const dryRunPrefix = finding.wouldFix ? `${colors.cyan}[DRY RUN] Would fix: ${RESET()}` : '';
1986
+ console.log(`${display.color()}${display.symbol} ${finding.severity.toUpperCase()}${RESET()} ${dryRunPrefix}${location}`);
2022
1987
  console.log(` ${finding.description}`);
2023
1988
  if (finding.fix) {
2024
1989
  console.log(` ${colors.cyan}Fix:${RESET()} ${finding.fix}`);
@@ -2056,6 +2021,14 @@ Examples:
2056
2021
  if (summaryParts.length > 0) {
2057
2022
  console.log(`${summaryParts.join(' | ')}\n`);
2058
2023
  }
2024
+ // Dry-run summary
2025
+ if (result.dryRun) {
2026
+ const wouldFixCount = issues.filter((f) => f.wouldFix).length;
2027
+ if (wouldFixCount > 0) {
2028
+ console.log(`${colors.cyan}Dry run complete:${RESET()} ${wouldFixCount} issue${wouldFixCount === 1 ? '' : 's'} auto-fixable. Run without --dry-run to apply.`);
2029
+ }
2030
+ console.log(` No changes were made.\n`);
2031
+ }
2059
2032
  }
2060
2033
  // Print fixed findings
2061
2034
  if (fixedFindings.length > 0) {
@@ -2155,12 +2128,15 @@ Examples:
2155
2128
  }
2156
2129
  }
2157
2130
  // Community contribution: share anonymized findings with OpenA2A Registry
2158
- await handleContribution(options.contribute, targetDir, result.findings, options.registryUrl, format, { ci: options.ci, score: result.score, projectType: result.projectType });
2131
+ await handleContribution(options.contribute, targetDir, result.findings, options.registryUrl, format);
2159
2132
  // Star prompt (interactive TTY only, text format only)
2160
2133
  if (process.stdout.isTTY) {
2161
2134
  console.log(`${colors.cyan}Helpful?${RESET()} Star the project: https://github.com/opena2a-org/opena2a\n`);
2162
2135
  }
2163
- // Exit with non-zero if critical/high issues remain
2136
+ // Exit with non-zero if critical/high issues remain (or any issues in --ci mode)
2137
+ if (options.ci && issues.length > 0) {
2138
+ process.exit(1);
2139
+ }
2164
2140
  const criticalOrHigh = issues.filter((f) => f.severity === 'critical' || f.severity === 'high');
2165
2141
  if (criticalOrHigh.length > 0) {
2166
2142
  process.exit(1);
@@ -2426,7 +2402,9 @@ Examples:
2426
2402
  .option('-v, --verbose', 'Show detailed finding information')
2427
2403
  .action(async (target, options) => {
2428
2404
  try {
2429
- console.log(`\nScanning ${target}...\n`);
2405
+ if (!options.json) {
2406
+ console.log(`\nScanning ${target}...\n`);
2407
+ }
2430
2408
  const scanner = new index_1.ExternalScanner();
2431
2409
  const customPorts = options.ports
2432
2410
  ? options.ports.split(',').map((p) => parseInt(p.trim(), 10))
@@ -4054,10 +4032,15 @@ Examples:
4054
4032
  .option('--registry-url <url>', 'Registry URL (default: REGISTRY_URL env)', process.env.REGISTRY_URL || 'https://api.oa2a.org')
4055
4033
  .option('--contribute', 'Share anonymized scan findings with OpenA2A Registry (overrides config)')
4056
4034
  .option('--no-contribute', 'Do not share findings for this scan (overrides config)')
4057
- .option('--ci', 'CI mode: suppress interactive prompts, machine-friendly output')
4035
+ .option('--ci', 'CI mode: suppress interactive prompts, exit non-zero on findings')
4058
4036
  .action(async (directory, options) => {
4059
4037
  try {
4060
4038
  const targetDir = directory.startsWith('/') ? directory : process.cwd() + '/' + directory;
4039
+ // CI mode: force non-interactive defaults
4040
+ if (options.ci) {
4041
+ if (options.contribute === undefined)
4042
+ options.contribute = false;
4043
+ }
4061
4044
  if (!require('fs').existsSync(targetDir)) {
4062
4045
  process.stderr.write(`Error: Directory '${targetDir}' does not exist.\n`);
4063
4046
  process.exit(1);
@@ -4220,7 +4203,14 @@ Examples:
4220
4203
  }
4221
4204
  // Community contribution: share anonymized findings with OpenA2A Registry
4222
4205
  const soulFormat = options.json ? 'json' : 'text';
4223
- await handleSoulContribution(options.contribute, targetDir, result, options.registryUrl, soulFormat, { ci: options.ci });
4206
+ await handleSoulContribution(options.contribute, targetDir, result, options.registryUrl, soulFormat);
4207
+ // In CI mode, exit non-zero if any controls failed
4208
+ if (options.ci) {
4209
+ const failedControls = result.domains.flatMap(d => d.controls).filter(c => !c.passed);
4210
+ if (failedControls.length > 0) {
4211
+ process.exit(1);
4212
+ }
4213
+ }
4224
4214
  // Check fail threshold
4225
4215
  if (options.failBelow) {
4226
4216
  const threshold = parseInt(options.failBelow, 10);