@etalon/cli 1.0.2 → 1.0.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/README.md CHANGED
@@ -12,22 +12,22 @@ npm install -g etalon
12
12
 
13
13
  ```bash
14
14
  # Scan a website
15
- optic scan https://example.com
15
+ etalon scan https://example.com
16
16
 
17
17
  # JSON output
18
- optic scan https://example.com --format json
18
+ etalon scan https://example.com --format json
19
19
 
20
20
  # SARIF for CI/CD (GitHub Code Scanning)
21
- optic scan https://example.com --format sarif
21
+ etalon scan https://example.com --format sarif
22
22
 
23
23
  # Deep scan — scroll page, click consent dialogs
24
- optic scan https://example.com --deep
24
+ etalon scan https://example.com --deep
25
25
 
26
26
  # Look up a single domain
27
- optic lookup google-analytics.com
27
+ etalon lookup google-analytics.com
28
28
 
29
29
  # Registry stats
30
- optic info
30
+ etalon info
31
31
  ```
32
32
 
33
33
  ## Options
@@ -11,7 +11,7 @@ var ETALON_VERSION = "1.0.0";
11
11
  var DEFAULT_OPTIONS = {
12
12
  deep: false,
13
13
  timeout: 3e4,
14
- waitForNetworkIdle: true,
14
+ waitForNetworkIdle: false,
15
15
  userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
16
16
  viewport: { width: 1920, height: 1080 },
17
17
  vendorDbPath: ""
@@ -74,8 +74,8 @@ async function checkConsent(url, options = {}) {
74
74
  postRejectRequests.push(req);
75
75
  }
76
76
  });
77
- await page.goto(url, { waitUntil: "networkidle", timeout });
78
- await page.waitForTimeout(2e3);
77
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout });
78
+ await page.waitForTimeout(3e3);
79
79
  const { bannerDetected, bannerType, rejectSelector } = await detectBanner(page);
80
80
  let rejectButtonFound = false;
81
81
  let rejectClicked = false;
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scanSite
4
- } from "./chunk-Z6ZQZ5HI.js";
4
+ } from "./chunk-YN7IXPMI.js";
5
5
 
6
6
  // src/index.ts
7
7
  import { Command } from "commander";
@@ -20,7 +20,18 @@ import {
20
20
  analyzeDataFlow,
21
21
  toMermaid,
22
22
  toTextSummary,
23
- generatePolicy
23
+ generatePolicy,
24
+ AutoFixEngine,
25
+ applyContextScoring,
26
+ reportFalsePositive,
27
+ getFeedbackSummary,
28
+ isTelemetryEnabled,
29
+ enableTelemetry,
30
+ disableTelemetry,
31
+ recordAuditEvent,
32
+ analyzePatterns,
33
+ getLearningStats,
34
+ detectProjectContext
24
35
  } from "@etalon/core";
25
36
 
26
37
  // src/formatters/text.ts
@@ -778,14 +789,14 @@ var program = new Command();
778
789
  program.name("etalon").description("ETALON \u2014 Open-source privacy auditor. Scan websites for trackers and GDPR compliance.").version(VERSION).hook("preAction", () => {
779
790
  showBanner();
780
791
  });
781
- program.command("scan").description("Scan a website for third-party trackers").argument("<url>", "URL to scan").option("-f, --format <format>", "Output format: text, json, sarif", "text").option("-d, --deep", "Deep scan: scroll page, interact with consent dialogs", false).option("-t, --timeout <ms>", "Navigation timeout in milliseconds", "30000").option("--no-idle", "Do not wait for network idle").option("--config <path>", "Path to etalon.yaml config file").action(async (url, options) => {
792
+ program.command("scan").description("Scan a website for third-party trackers").argument("<url>", "URL to scan").option("-f, --format <format>", "Output format: text, json, sarif", "text").option("-d, --deep", "Deep scan: scroll page, interact with consent dialogs", false).option("-t, --timeout <ms>", "Navigation timeout in milliseconds", "30000").option("--idle", "Wait for network idle (slower but more thorough)").option("--config <path>", "Path to etalon.yaml config file").action(async (url, options) => {
782
793
  const normalizedUrl = normalizeUrl(url);
783
794
  const format = options.format ?? "text";
784
795
  const config = loadConfig(options.config);
785
796
  const scanOptions = {
786
797
  deep: options.deep,
787
798
  timeout: parseInt(options.timeout, 10),
788
- waitForNetworkIdle: options.idle !== false
799
+ waitForNetworkIdle: options.idle === true
789
800
  };
790
801
  if (config?.scan) {
791
802
  if (config.scan.timeout && !options.timeout) scanOptions.timeout = config.scan.timeout;
@@ -841,14 +852,26 @@ program.command("audit").description("Scan a codebase for GDPR compliance (track
841
852
  severity: options.severity,
842
853
  includeBlame
843
854
  });
855
+ const scoring = applyContextScoring(report.findings, dir);
856
+ report.findings = scoring.adjustedFindings;
857
+ if (scoring.adjustments.length > 0 && format === "text") {
858
+ spinner?.stop();
859
+ console.log(chalk4.bold(`
860
+ \u{1F3AF} Context: ${scoring.projectContext.industry} / ${scoring.projectContext.region}`));
861
+ console.log(chalk4.dim(` ${scoring.adjustments.length} finding(s) had severity adjusted based on context`));
862
+ for (const adj of scoring.adjustments.slice(0, 5)) {
863
+ console.log(` ${chalk4.dim(adj.finding_rule)}: ${adj.original_severity} \u2192 ${chalk4.yellow(adj.adjusted_severity)} (${adj.reason})`);
864
+ }
865
+ if (scoring.adjustments.length > 5) {
866
+ console.log(chalk4.dim(` ... and ${scoring.adjustments.length - 5} more`));
867
+ }
868
+ }
844
869
  spinner?.stop();
845
870
  if (autoFix) {
846
871
  const patches = generatePatches(report.findings, dir);
847
- if (patches.length === 0) {
848
- console.log(chalk4.yellow("\nNo auto-fixable issues found."));
849
- } else {
872
+ if (patches.length > 0) {
850
873
  console.log(chalk4.bold(`
851
- \u{1F527} ${patches.length} auto-fixable issue(s):`));
874
+ \u{1F527} ${patches.length} config fix(es):`));
852
875
  for (const p of patches) {
853
876
  console.log(` ${chalk4.dim(p.file)}:${p.line} \u2014 ${p.description}`);
854
877
  console.log(` ${chalk4.red("- " + p.oldContent.trim())}`);
@@ -856,9 +879,58 @@ program.command("audit").description("Scan a codebase for GDPR compliance (track
856
879
  }
857
880
  const applied = applyPatches(patches, dir);
858
881
  console.log(chalk4.green(`
859
- \u2713 Applied ${applied} fix(es).`));
882
+ \u2713 Applied ${applied} config fix(es).`));
883
+ }
884
+ const engine = new AutoFixEngine();
885
+ const { readdirSync } = await import("fs");
886
+ const { join: joinPath } = await import("path");
887
+ const codeFiles = [];
888
+ const collectCodeFiles = (d) => {
889
+ try {
890
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
891
+ const full = joinPath(d, entry.name);
892
+ if (entry.isDirectory()) {
893
+ if (!["node_modules", ".git", "dist", "build", ".next", "__pycache__", "target"].includes(entry.name)) {
894
+ collectCodeFiles(full);
895
+ }
896
+ } else if (/\.(tsx?|jsx?|vue|svelte|html)$/.test(entry.name)) {
897
+ codeFiles.push(full);
898
+ }
899
+ }
900
+ } catch {
901
+ }
902
+ };
903
+ collectCodeFiles(dir);
904
+ const suggestions = engine.scanFiles(codeFiles);
905
+ if (suggestions.length > 0) {
906
+ console.log(chalk4.bold(`
907
+ \u{1F6E1}\uFE0F ${suggestions.length} tracker consent fix(es) available:`));
908
+ for (const s of suggestions) {
909
+ console.log(` ${chalk4.cyan(s.tracker_name)} in ${chalk4.dim(s.location.file)}:${s.location.line}`);
910
+ console.log(` ${chalk4.dim(s.description)}`);
911
+ }
912
+ const hookPath = engine.generateConsentHook(dir);
913
+ console.log(chalk4.green(`
914
+ \u2713 Generated consent hook: ${chalk4.cyan(hookPath)}`));
915
+ const result = engine.applyAllFixes(suggestions);
916
+ console.log(chalk4.green(`\u2713 Applied ${result.applied} tracker consent fix(es).`));
917
+ if (result.failed > 0) {
918
+ console.log(chalk4.yellow(`\u26A0 ${result.failed} fix(es) could not be applied.`));
919
+ }
920
+ }
921
+ if (patches.length === 0 && suggestions.length === 0) {
922
+ console.log(chalk4.yellow("\nNo auto-fixable issues found."));
860
923
  }
861
924
  }
925
+ recordAuditEvent({
926
+ total_findings: report.findings.length,
927
+ critical: report.summary.critical,
928
+ high: report.summary.high,
929
+ medium: report.summary.medium,
930
+ framework: scoring.projectContext.industry,
931
+ industry: scoring.projectContext.industry,
932
+ region: scoring.projectContext.region
933
+ });
862
934
  switch (format) {
863
935
  case "json":
864
936
  console.log(JSON.stringify(report, null, 2));
@@ -923,7 +995,7 @@ program.command("consent-check").description("Test if trackers fire before/after
923
995
  const format = options.format ?? "text";
924
996
  const spinner = format === "text" ? ora(`Checking consent on ${normalizedUrl}...`).start() : null;
925
997
  try {
926
- const { checkConsent } = await import("./consent-checker-B6J7GESG.js");
998
+ const { checkConsent } = await import("./consent-checker-QRPTMQWN.js");
927
999
  const result = await checkConsent(normalizedUrl, {
928
1000
  timeout: parseInt(options.timeout ?? "15000", 10)
929
1001
  });
@@ -977,7 +1049,7 @@ program.command("policy-check").description("Cross-reference privacy policy text
977
1049
  const format = options.format ?? "text";
978
1050
  const spinner = format === "text" ? ora(`Analyzing privacy policy for ${normalizedUrl}...`).start() : null;
979
1051
  try {
980
- const { checkPolicy } = await import("./policy-checker-J2WPHGU3.js");
1052
+ const { checkPolicy } = await import("./policy-checker-ONMTI7X2.js");
981
1053
  const result = await checkPolicy(normalizedUrl, {
982
1054
  timeout: parseInt(options.timeout ?? "30000", 10),
983
1055
  policyUrl: options.policyUrl
@@ -1149,23 +1221,22 @@ Error: ${error.message}`);
1149
1221
  program.command("data-flow").description("Map PII data flows through your codebase").argument("[dir]", "Directory to analyze", "./").option("-f, --format <format>", "Output format: text, mermaid, json", "text").action(async (dir, options) => {
1150
1222
  const spinner = ora("Analyzing data flows...").start();
1151
1223
  try {
1152
- let walk2 = function(d) {
1224
+ const { readdirSync } = await import("fs");
1225
+ const { join: join3, relative } = await import("path");
1226
+ const files = [];
1227
+ const walk = (d) => {
1153
1228
  for (const entry of readdirSync(d, { withFileTypes: true })) {
1154
1229
  const full = join3(d, entry.name);
1155
1230
  if (entry.isDirectory()) {
1156
1231
  if (!["node_modules", ".git", "dist", "build", ".next", "__pycache__"].includes(entry.name)) {
1157
- walk2(full);
1232
+ walk(full);
1158
1233
  }
1159
1234
  } else {
1160
1235
  files.push(relative(dir, full));
1161
1236
  }
1162
1237
  }
1163
1238
  };
1164
- var walk = walk2;
1165
- const { readdirSync, statSync } = await import("fs");
1166
- const { join: join3, relative } = await import("path");
1167
- const files = [];
1168
- walk2(dir);
1239
+ walk(dir);
1169
1240
  const flow = analyzeDataFlow(files, dir);
1170
1241
  spinner.stop();
1171
1242
  switch (options.format) {
@@ -1187,4 +1258,84 @@ Error: ${error.message}`);
1187
1258
  process.exit(2);
1188
1259
  }
1189
1260
  });
1261
+ program.command("report-fp").description("Report a false positive finding").requiredOption("--domain <domain>", "Domain that was incorrectly flagged").requiredOption("--rule <rule>", "Rule that triggered the false positive").option("--file <file>", "File where the false positive was found").option("--reason <reason>", "Why this is a false positive", "Not a tracker").action((options) => {
1262
+ const report = reportFalsePositive({
1263
+ domain: options.domain,
1264
+ rule: options.rule,
1265
+ file: options.file,
1266
+ reason: options.reason ?? "Not a tracker",
1267
+ suggested_action: "whitelist_domain"
1268
+ });
1269
+ console.log(chalk4.green(`\u2713 False positive reported: ${chalk4.bold(report.id)}`));
1270
+ console.log(chalk4.dim(` Domain: ${report.domain}`));
1271
+ console.log(chalk4.dim(` Rule: ${report.rule}`));
1272
+ console.log(chalk4.dim(` Reason: ${report.reason}`));
1273
+ console.log("");
1274
+ console.log(chalk4.dim("Reports help ETALON learn and reduce false positives over time."));
1275
+ });
1276
+ program.command("telemetry").description("Manage anonymous usage telemetry").argument("<action>", "enable, disable, or status").action((action) => {
1277
+ switch (action) {
1278
+ case "enable":
1279
+ enableTelemetry();
1280
+ console.log(chalk4.green("\u2713 Telemetry enabled."));
1281
+ console.log(chalk4.dim(" Anonymous usage data helps improve ETALON for everyone."));
1282
+ console.log(chalk4.dim(" No PII is ever collected. Set DO_NOT_TRACK=1 to override."));
1283
+ break;
1284
+ case "disable":
1285
+ disableTelemetry();
1286
+ console.log(chalk4.yellow("\u2713 Telemetry disabled."));
1287
+ break;
1288
+ case "status":
1289
+ console.log(`Telemetry: ${isTelemetryEnabled() ? chalk4.green("enabled") : chalk4.yellow("disabled")}`);
1290
+ break;
1291
+ default:
1292
+ console.error(chalk4.red(`Unknown action: ${action}. Use enable, disable, or status.`));
1293
+ process.exit(1);
1294
+ }
1295
+ });
1296
+ program.command("intelligence").description("Show intelligence engine status and learned patterns").argument("[dir]", "Project directory for context detection", "./").action((dir) => {
1297
+ console.log(chalk4.bold("ETALON Intelligence Engine"));
1298
+ console.log(chalk4.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
1299
+ console.log("");
1300
+ const ctx = detectProjectContext(dir);
1301
+ console.log(chalk4.bold("\u{1F3AF} Project Context"));
1302
+ console.log(` Industry: ${chalk4.cyan(ctx.industry)}`);
1303
+ console.log(` Region: ${chalk4.cyan(ctx.region)}`);
1304
+ console.log(` Data Sensitivity: ${chalk4.cyan(ctx.data_sensitivity)}`);
1305
+ if (ctx.detected_signals.length > 0) {
1306
+ console.log(chalk4.dim(" Signals:"));
1307
+ for (const s of ctx.detected_signals) {
1308
+ console.log(chalk4.dim(` \u2022 ${s}`));
1309
+ }
1310
+ }
1311
+ console.log("");
1312
+ const stats = getLearningStats();
1313
+ console.log(chalk4.bold("\u{1F9E0} Learning Engine"));
1314
+ console.log(` Patterns learned: ${chalk4.cyan(String(stats.patterns_learned))}`);
1315
+ console.log(` Feedback processed: ${chalk4.cyan(String(stats.feedback_processed))}`);
1316
+ console.log(` Impact: ${chalk4.cyan(stats.accuracy_improvement)}`);
1317
+ console.log("");
1318
+ const feedback = getFeedbackSummary();
1319
+ if (feedback.total_reports > 0) {
1320
+ console.log(chalk4.bold("\u{1F4CA} False Positive Reports"));
1321
+ console.log(` Total reports: ${feedback.total_reports}`);
1322
+ if (feedback.suggested_whitelists.length > 0) {
1323
+ console.log(chalk4.dim(" Suggested whitelists (3+ reports):"));
1324
+ for (const d of feedback.suggested_whitelists) {
1325
+ console.log(chalk4.dim(` \u2022 ${d}`));
1326
+ }
1327
+ }
1328
+ console.log("");
1329
+ }
1330
+ const learned = analyzePatterns();
1331
+ if (learned.length > 0) {
1332
+ console.log(chalk4.bold("\u{1F4DD} Learned Patterns"));
1333
+ for (const p of learned) {
1334
+ console.log(` ${chalk4.cyan(p.domain)} \u2014 ${p.suggested_action} (confidence: ${(p.confidence * 100).toFixed(0)}%)`);
1335
+ }
1336
+ console.log("");
1337
+ }
1338
+ console.log(chalk4.dim("Telemetry: ") + (isTelemetryEnabled() ? chalk4.green("enabled") : chalk4.yellow("disabled")));
1339
+ console.log("");
1340
+ });
1190
1341
  program.parse();
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scanSite
4
- } from "./chunk-Z6ZQZ5HI.js";
4
+ } from "./chunk-YN7IXPMI.js";
5
5
 
6
6
  // src/policy-checker.ts
7
7
  import { chromium } from "playwright";
@@ -46,7 +46,7 @@ async function findPolicyPage(page, siteUrl) {
46
46
  const testUrl = `${baseUrl.origin}${path}`;
47
47
  const response = await page.goto(testUrl, { waitUntil: "domcontentloaded", timeout: 1e4 });
48
48
  if (response && response.status() >= 200 && response.status() < 400) {
49
- const bodyText = await page.evaluate(() => globalThis.document.body?.innerText?.trim() ?? "");
49
+ const bodyText = await page.evaluate(() => document.body?.innerText?.trim() ?? "");
50
50
  if (bodyText.length > 200) {
51
51
  return testUrl;
52
52
  }
@@ -82,13 +82,12 @@ async function findPolicyPage(page, siteUrl) {
82
82
  return null;
83
83
  }
84
84
  async function extractPolicyText(page, policyUrl) {
85
- await page.goto(policyUrl, { waitUntil: "networkidle", timeout: 2e4 });
85
+ await page.goto(policyUrl, { waitUntil: "domcontentloaded", timeout: 2e4 });
86
86
  await page.waitForTimeout(1e3);
87
87
  const text = await page.evaluate(() => {
88
- const doc = globalThis.document;
89
- const elementsToRemove = doc.querySelectorAll("script, style, nav, header, footer, iframe");
88
+ const elementsToRemove = document.querySelectorAll("script, style, nav, header, footer, iframe");
90
89
  elementsToRemove.forEach((el) => el.remove());
91
- return doc.body?.innerText ?? "";
90
+ return document.body?.innerText ?? "";
92
91
  });
93
92
  return text.replace(/\s+/g, " ").trim();
94
93
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@etalon/cli",
3
- "version": "1.0.2",
4
- "description": "ETALON \u2014 Privacy audit tool for websites. Scan any site for trackers and GDPR compliance.",
3
+ "version": "1.0.4",
4
+ "description": "ETALON Privacy audit tool for websites. Scan any site for trackers and GDPR compliance.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  "ora": "^8.0.0",
24
24
  "playwright": "^1.42.0",
25
25
  "yaml": "^2.3.0",
26
- "@etalon/core": "^1.0.1"
26
+ "@etalon/core": "^1.0.2"
27
27
  },
28
28
  "keywords": [
29
29
  "privacy",