aislop 0.1.0 → 0.1.2
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 +20 -0
- package/dist/cli.js +142 -12
- package/dist/{engine-info-DBG3uXLc.js → engine-info-B4Eq4giL.js} +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +141 -12
- package/dist/{json-DkpW9UQj.js → json-BMSa_G7o.js} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -326,6 +326,26 @@ npx aislop scan --staged
|
|
|
326
326
|
|
|
327
327
|
---
|
|
328
328
|
|
|
329
|
+
## Telemetry
|
|
330
|
+
|
|
331
|
+
`aislop` collects anonymous usage analytics to help us understand how the tool is used and prioritize improvements. **No code, file paths, project names, or secrets are ever collected.**
|
|
332
|
+
|
|
333
|
+
What we collect: command run (scan/fix/ci), languages detected, score bucket, issue counts per engine, engine timing, OS, Node version, and aislop version.
|
|
334
|
+
|
|
335
|
+
Telemetry is **off in CI** by default. To opt out anywhere:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
# Environment variable (any of these)
|
|
339
|
+
AISLOP_NO_TELEMETRY=1 aislop scan
|
|
340
|
+
DO_NOT_TRACK=1 aislop scan
|
|
341
|
+
|
|
342
|
+
# Or in .aislop/config.yml
|
|
343
|
+
telemetry:
|
|
344
|
+
enabled: false
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
329
349
|
## Contributing
|
|
330
350
|
|
|
331
351
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, project architecture, and how to add new rules or engines.
|
package/dist/cli.js
CHANGED
|
@@ -51,7 +51,8 @@ const DEFAULT_CONFIG = {
|
|
|
51
51
|
ci: {
|
|
52
52
|
failBelow: 0,
|
|
53
53
|
format: "json"
|
|
54
|
-
}
|
|
54
|
+
},
|
|
55
|
+
telemetry: { enabled: true }
|
|
55
56
|
};
|
|
56
57
|
const DEFAULT_CONFIG_YAML = `version: 1
|
|
57
58
|
|
|
@@ -88,6 +89,9 @@ scoring:
|
|
|
88
89
|
ci:
|
|
89
90
|
failBelow: 0
|
|
90
91
|
format: json
|
|
92
|
+
|
|
93
|
+
# telemetry:
|
|
94
|
+
# enabled: true # set to false to disable anonymous usage analytics
|
|
91
95
|
`;
|
|
92
96
|
const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
|
|
93
97
|
# Uncomment and customize to enforce your project's conventions.
|
|
@@ -148,6 +152,7 @@ const CiSchema = z.object({
|
|
|
148
152
|
failBelow: z.number().default(0),
|
|
149
153
|
format: z.enum(["json"]).default("json")
|
|
150
154
|
});
|
|
155
|
+
const TelemetrySchema = z.object({ enabled: z.boolean().default(true) });
|
|
151
156
|
const AislopConfigSchema = z.object({
|
|
152
157
|
version: z.number().default(1),
|
|
153
158
|
engines: EnginesSchema.default(() => ({
|
|
@@ -178,7 +183,8 @@ const AislopConfigSchema = z.object({
|
|
|
178
183
|
ci: CiSchema.default(() => ({
|
|
179
184
|
failBelow: 0,
|
|
180
185
|
format: "json"
|
|
181
|
-
}))
|
|
186
|
+
})),
|
|
187
|
+
telemetry: TelemetrySchema.default(() => ({ enabled: true }))
|
|
182
188
|
});
|
|
183
189
|
const defaults = AislopConfigSchema.parse({});
|
|
184
190
|
/**
|
|
@@ -1380,17 +1386,21 @@ const isDataFile = (content) => {
|
|
|
1380
1386
|
const dataLinePattern = /^\s*[{}[\]"']/;
|
|
1381
1387
|
return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
|
|
1382
1388
|
};
|
|
1389
|
+
const ARROW_BLOCK_RE = /* @__PURE__ */ new RegExp("=>\\s*\\{");
|
|
1390
|
+
const ARROW_END_RE = /* @__PURE__ */ new RegExp("=>\\s*$");
|
|
1391
|
+
const BRACE_START_RE = /* @__PURE__ */ new RegExp("^\\s*\\{");
|
|
1392
|
+
const NEW_STATEMENT_RE = /* @__PURE__ */ new RegExp("^(?:export\\s+)?(?:const|let|var|function|class)\\s");
|
|
1383
1393
|
const isBlockArrow = (lines, startIndex) => {
|
|
1384
|
-
if (
|
|
1385
|
-
if (
|
|
1394
|
+
if (ARROW_BLOCK_RE.test(lines[startIndex])) return true;
|
|
1395
|
+
if (ARROW_END_RE.test(lines[startIndex])) {
|
|
1386
1396
|
const next = lines[startIndex + 1];
|
|
1387
|
-
if (next &&
|
|
1397
|
+
if (next && BRACE_START_RE.test(next)) return true;
|
|
1388
1398
|
}
|
|
1389
1399
|
for (let j = startIndex + 1; j < Math.min(startIndex + 3, lines.length); j++) {
|
|
1390
1400
|
const l = lines[j];
|
|
1391
|
-
if (l.trim() === "" ||
|
|
1392
|
-
if (
|
|
1393
|
-
if (
|
|
1401
|
+
if (l.trim() === "" || NEW_STATEMENT_RE.test(l.trim())) break;
|
|
1402
|
+
if (ARROW_BLOCK_RE.test(l)) return true;
|
|
1403
|
+
if (BRACE_START_RE.test(l)) return true;
|
|
1394
1404
|
}
|
|
1395
1405
|
return false;
|
|
1396
1406
|
};
|
|
@@ -2750,7 +2760,7 @@ const RISKY_PATTERNS = [
|
|
|
2750
2760
|
help: "Avoid dynamic code execution — refactor to use static code paths"
|
|
2751
2761
|
},
|
|
2752
2762
|
{
|
|
2753
|
-
pattern:
|
|
2763
|
+
pattern: new RegExp(`\\.innerHTML\\s*=`, "g"),
|
|
2754
2764
|
extensions: [
|
|
2755
2765
|
".ts",
|
|
2756
2766
|
".tsx",
|
|
@@ -2847,6 +2857,10 @@ const detectRiskyConstructs = async (context) => {
|
|
|
2847
2857
|
let match;
|
|
2848
2858
|
while ((match = regex.exec(content)) !== null) {
|
|
2849
2859
|
const line = content.slice(0, match.index).split("\n").length;
|
|
2860
|
+
if (name === "innerhtml") {
|
|
2861
|
+
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
2862
|
+
if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
|
|
2863
|
+
}
|
|
2850
2864
|
if (name === "sql-injection") {
|
|
2851
2865
|
const afterMatch = content.slice(match.index + match[0].length, match.index + match[0].length + 100);
|
|
2852
2866
|
if (/^(?:\w+\.join\s*\(|[A-Z_]+\}|tableName\}|table\})/.test(afterMatch)) continue;
|
|
@@ -3104,7 +3118,7 @@ const logger = {
|
|
|
3104
3118
|
* Application version — injected at build time by tsdown from package.json.
|
|
3105
3119
|
* The fallback should always match the "version" field in package.json.
|
|
3106
3120
|
*/
|
|
3107
|
-
const APP_VERSION = "0.1.
|
|
3121
|
+
const APP_VERSION = "0.1.2";
|
|
3108
3122
|
|
|
3109
3123
|
//#endregion
|
|
3110
3124
|
//#region src/output/layout.ts
|
|
@@ -3613,6 +3627,95 @@ const spinner = (text) => ({ start() {
|
|
|
3613
3627
|
};
|
|
3614
3628
|
} });
|
|
3615
3629
|
|
|
3630
|
+
//#endregion
|
|
3631
|
+
//#region src/utils/telemetry.ts
|
|
3632
|
+
/**
|
|
3633
|
+
* Anonymous, opt-out telemetry for aislop.
|
|
3634
|
+
*
|
|
3635
|
+
* What we collect:
|
|
3636
|
+
* - Command run (scan, fix, ci)
|
|
3637
|
+
* - Languages detected in the project
|
|
3638
|
+
* - Score bucket (0-25, 25-50, 50-75, 75-100)
|
|
3639
|
+
* - Issue counts per engine (not file paths or code)
|
|
3640
|
+
* - Engine timing (milliseconds)
|
|
3641
|
+
* - OS, architecture, and Node version
|
|
3642
|
+
* - aislop version
|
|
3643
|
+
*
|
|
3644
|
+
* What we never collect:
|
|
3645
|
+
* - File paths, file contents, or code snippets
|
|
3646
|
+
* - Project names or directory paths
|
|
3647
|
+
* - Git remotes, branch names, or commit hashes
|
|
3648
|
+
* - Environment variables or secrets
|
|
3649
|
+
* - IP addresses are not stored (PostHog configured to discard)
|
|
3650
|
+
*
|
|
3651
|
+
* How to opt out (any one of these):
|
|
3652
|
+
* - Set AISLOP_NO_TELEMETRY=1
|
|
3653
|
+
* - Set DO_NOT_TRACK=1 (https://consoledonottrack.com)
|
|
3654
|
+
* - Set CI=true (telemetry is off in CI by default)
|
|
3655
|
+
* - Set telemetry.enabled: false in .aislop/config.yml
|
|
3656
|
+
*/
|
|
3657
|
+
const POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
3658
|
+
const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
3659
|
+
/**
|
|
3660
|
+
* Returns true if telemetry should be disabled.
|
|
3661
|
+
* Telemetry is opt-out: it runs unless explicitly disabled.
|
|
3662
|
+
*/
|
|
3663
|
+
const isTelemetryDisabled = (configEnabled) => {
|
|
3664
|
+
if (process.env.AISLOP_NO_TELEMETRY === "1" || process.env.DO_NOT_TRACK === "1") return true;
|
|
3665
|
+
if (process.env.CI === "true" || process.env.CI === "1") return true;
|
|
3666
|
+
if (configEnabled === false) return true;
|
|
3667
|
+
return false;
|
|
3668
|
+
};
|
|
3669
|
+
const getScoreBucket = (score) => {
|
|
3670
|
+
if (score >= 75) return "75-100";
|
|
3671
|
+
if (score >= 50) return "50-75";
|
|
3672
|
+
if (score >= 25) return "25-50";
|
|
3673
|
+
return "0-25";
|
|
3674
|
+
};
|
|
3675
|
+
/**
|
|
3676
|
+
* Returns a stable anonymous device ID derived from hostname + OS.
|
|
3677
|
+
* This is NOT personally identifiable — it's a hash used only to
|
|
3678
|
+
* count unique devices, not to identify users.
|
|
3679
|
+
*/
|
|
3680
|
+
const getAnonymousId = () => {
|
|
3681
|
+
const raw = `${os.hostname()}-${os.platform()}-${os.arch()}`;
|
|
3682
|
+
let hash = 5381;
|
|
3683
|
+
for (let i = 0; i < raw.length; i++) hash = hash * 33 ^ raw.charCodeAt(i);
|
|
3684
|
+
return `aislop_${(hash >>> 0).toString(36)}`;
|
|
3685
|
+
};
|
|
3686
|
+
/**
|
|
3687
|
+
* Fire-and-forget telemetry event to PostHog.
|
|
3688
|
+
* Never throws, never blocks, never affects CLI output or exit code.
|
|
3689
|
+
*/
|
|
3690
|
+
const trackEvent = (event) => {
|
|
3691
|
+
const payload = {
|
|
3692
|
+
api_key: POSTHOG_KEY,
|
|
3693
|
+
event: `cli_${event.command}`,
|
|
3694
|
+
distinct_id: getAnonymousId(),
|
|
3695
|
+
properties: {
|
|
3696
|
+
version: APP_VERSION,
|
|
3697
|
+
node_version: process.version,
|
|
3698
|
+
os: os.platform(),
|
|
3699
|
+
arch: os.arch(),
|
|
3700
|
+
languages: event.languages,
|
|
3701
|
+
score_bucket: event.scoreBucket,
|
|
3702
|
+
engine_issues: event.engineIssues,
|
|
3703
|
+
engine_timings: event.engineTimings,
|
|
3704
|
+
elapsed_ms: event.elapsedMs,
|
|
3705
|
+
file_count: event.fileCount,
|
|
3706
|
+
fix_steps: event.fixSteps,
|
|
3707
|
+
fix_resolved: event.fixResolved
|
|
3708
|
+
},
|
|
3709
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3710
|
+
};
|
|
3711
|
+
fetch(`${POSTHOG_HOST}/capture/`, {
|
|
3712
|
+
method: "POST",
|
|
3713
|
+
headers: { "Content-Type": "application/json" },
|
|
3714
|
+
body: JSON.stringify(payload),
|
|
3715
|
+
signal: AbortSignal.timeout(3e3)
|
|
3716
|
+
}).catch(() => {});
|
|
3717
|
+
};
|
|
3718
|
+
|
|
3616
3719
|
//#endregion
|
|
3617
3720
|
//#region src/commands/scan.ts
|
|
3618
3721
|
const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
|
|
@@ -3669,7 +3772,24 @@ const scanCommand = async (directory, config, options) => {
|
|
|
3669
3772
|
const allDiagnostics = results.flatMap((r) => r.diagnostics);
|
|
3670
3773
|
const elapsedMs = performance.now() - startTime;
|
|
3671
3774
|
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds);
|
|
3672
|
-
const exitCode = scoreResult.score < config.ci.failBelow ? 1 : 0;
|
|
3775
|
+
const exitCode = allDiagnostics.some((d) => d.severity === "error") || scoreResult.score < config.ci.failBelow ? 1 : 0;
|
|
3776
|
+
if (!isTelemetryDisabled(config.telemetry?.enabled)) {
|
|
3777
|
+
const engineIssues = {};
|
|
3778
|
+
const engineTimings = {};
|
|
3779
|
+
for (const r of results) {
|
|
3780
|
+
engineIssues[r.engine] = r.diagnostics.length;
|
|
3781
|
+
engineTimings[r.engine] = Math.round(r.elapsed);
|
|
3782
|
+
}
|
|
3783
|
+
trackEvent({
|
|
3784
|
+
command: options.command ?? "scan",
|
|
3785
|
+
languages: projectInfo.languages,
|
|
3786
|
+
scoreBucket: getScoreBucket(scoreResult.score),
|
|
3787
|
+
engineIssues,
|
|
3788
|
+
engineTimings,
|
|
3789
|
+
elapsedMs: Math.round(elapsedMs),
|
|
3790
|
+
fileCount: projectInfo.sourceFileCount
|
|
3791
|
+
});
|
|
3792
|
+
}
|
|
3673
3793
|
if (options.json) {
|
|
3674
3794
|
const { buildJsonOutput } = await import("./json-L5x3hQdy.js");
|
|
3675
3795
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
@@ -3692,7 +3812,8 @@ const ciCommand = async (directory, config) => {
|
|
|
3692
3812
|
changes: false,
|
|
3693
3813
|
staged: false,
|
|
3694
3814
|
verbose: false,
|
|
3695
|
-
json: true
|
|
3815
|
+
json: true,
|
|
3816
|
+
command: "ci"
|
|
3696
3817
|
});
|
|
3697
3818
|
};
|
|
3698
3819
|
|
|
@@ -3944,6 +4065,15 @@ const fixCommand = async (directory, config, options = {
|
|
|
3944
4065
|
logger.break();
|
|
3945
4066
|
summarizeFixRun(steps);
|
|
3946
4067
|
}
|
|
4068
|
+
if (!isTelemetryDisabled(config.telemetry?.enabled)) {
|
|
4069
|
+
const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
|
|
4070
|
+
trackEvent({
|
|
4071
|
+
command: "fix",
|
|
4072
|
+
languages: projectInfo.languages,
|
|
4073
|
+
fixSteps: steps.length,
|
|
4074
|
+
fixResolved: totalResolved
|
|
4075
|
+
});
|
|
4076
|
+
}
|
|
3947
4077
|
logger.break();
|
|
3948
4078
|
logger.success(" ✓ Done. Run `aislop scan` to verify.");
|
|
3949
4079
|
logger.break();
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Application version — injected at build time by tsdown from package.json.
|
|
4
4
|
* The fallback should always match the "version" field in package.json.
|
|
5
5
|
*/
|
|
6
|
-
const APP_VERSION = "0.1.
|
|
6
|
+
const APP_VERSION = "0.1.2";
|
|
7
7
|
|
|
8
8
|
//#endregion
|
|
9
9
|
//#region src/output/engine-info.ts
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,9 @@ declare const AislopConfigSchema: z.ZodObject<{
|
|
|
37
37
|
json: "json";
|
|
38
38
|
}>>;
|
|
39
39
|
}, z.core.$strip>>;
|
|
40
|
+
telemetry: z.ZodDefault<z.ZodObject<{
|
|
41
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
42
|
+
}, z.core.$strip>>;
|
|
40
43
|
}, z.core.$strip>;
|
|
41
44
|
type AislopConfig = z.infer<typeof AislopConfigSchema>;
|
|
42
45
|
//#endregion
|
|
@@ -60,6 +63,8 @@ interface ScanOptions {
|
|
|
60
63
|
verbose: boolean;
|
|
61
64
|
json: boolean;
|
|
62
65
|
showHeader?: boolean;
|
|
66
|
+
/** Used for telemetry to distinguish scan vs ci invocation */
|
|
67
|
+
command?: "scan" | "ci";
|
|
63
68
|
}
|
|
64
69
|
declare const scanCommand: (directory: string, config: AislopConfig, options: ScanOptions) => Promise<{
|
|
65
70
|
exitCode: number;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as getEngineLabel, r as APP_VERSION, t as ENGINE_INFO } from "./engine-info-
|
|
1
|
+
import { n as getEngineLabel, r as APP_VERSION, t as ENGINE_INFO } from "./engine-info-B4Eq4giL.js";
|
|
2
2
|
import { n as runSubprocess, t as isToolInstalled } from "./subprocess-99puEEGl.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import fs from "node:fs";
|
|
@@ -1022,6 +1022,95 @@ const printMaybePaged = async (text) => {
|
|
|
1022
1022
|
if (!await pipeToPager(pager.command, pager.args, text)) writeToStdout(text);
|
|
1023
1023
|
};
|
|
1024
1024
|
|
|
1025
|
+
//#endregion
|
|
1026
|
+
//#region src/utils/telemetry.ts
|
|
1027
|
+
/**
|
|
1028
|
+
* Anonymous, opt-out telemetry for aislop.
|
|
1029
|
+
*
|
|
1030
|
+
* What we collect:
|
|
1031
|
+
* - Command run (scan, fix, ci)
|
|
1032
|
+
* - Languages detected in the project
|
|
1033
|
+
* - Score bucket (0-25, 25-50, 50-75, 75-100)
|
|
1034
|
+
* - Issue counts per engine (not file paths or code)
|
|
1035
|
+
* - Engine timing (milliseconds)
|
|
1036
|
+
* - OS, architecture, and Node version
|
|
1037
|
+
* - aislop version
|
|
1038
|
+
*
|
|
1039
|
+
* What we never collect:
|
|
1040
|
+
* - File paths, file contents, or code snippets
|
|
1041
|
+
* - Project names or directory paths
|
|
1042
|
+
* - Git remotes, branch names, or commit hashes
|
|
1043
|
+
* - Environment variables or secrets
|
|
1044
|
+
* - IP addresses are not stored (PostHog configured to discard)
|
|
1045
|
+
*
|
|
1046
|
+
* How to opt out (any one of these):
|
|
1047
|
+
* - Set AISLOP_NO_TELEMETRY=1
|
|
1048
|
+
* - Set DO_NOT_TRACK=1 (https://consoledonottrack.com)
|
|
1049
|
+
* - Set CI=true (telemetry is off in CI by default)
|
|
1050
|
+
* - Set telemetry.enabled: false in .aislop/config.yml
|
|
1051
|
+
*/
|
|
1052
|
+
const POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
1053
|
+
const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
1054
|
+
/**
|
|
1055
|
+
* Returns true if telemetry should be disabled.
|
|
1056
|
+
* Telemetry is opt-out: it runs unless explicitly disabled.
|
|
1057
|
+
*/
|
|
1058
|
+
const isTelemetryDisabled = (configEnabled) => {
|
|
1059
|
+
if (process.env.AISLOP_NO_TELEMETRY === "1" || process.env.DO_NOT_TRACK === "1") return true;
|
|
1060
|
+
if (process.env.CI === "true" || process.env.CI === "1") return true;
|
|
1061
|
+
if (configEnabled === false) return true;
|
|
1062
|
+
return false;
|
|
1063
|
+
};
|
|
1064
|
+
const getScoreBucket = (score) => {
|
|
1065
|
+
if (score >= 75) return "75-100";
|
|
1066
|
+
if (score >= 50) return "50-75";
|
|
1067
|
+
if (score >= 25) return "25-50";
|
|
1068
|
+
return "0-25";
|
|
1069
|
+
};
|
|
1070
|
+
/**
|
|
1071
|
+
* Returns a stable anonymous device ID derived from hostname + OS.
|
|
1072
|
+
* This is NOT personally identifiable — it's a hash used only to
|
|
1073
|
+
* count unique devices, not to identify users.
|
|
1074
|
+
*/
|
|
1075
|
+
const getAnonymousId = () => {
|
|
1076
|
+
const raw = `${os.hostname()}-${os.platform()}-${os.arch()}`;
|
|
1077
|
+
let hash = 5381;
|
|
1078
|
+
for (let i = 0; i < raw.length; i++) hash = hash * 33 ^ raw.charCodeAt(i);
|
|
1079
|
+
return `aislop_${(hash >>> 0).toString(36)}`;
|
|
1080
|
+
};
|
|
1081
|
+
/**
|
|
1082
|
+
* Fire-and-forget telemetry event to PostHog.
|
|
1083
|
+
* Never throws, never blocks, never affects CLI output or exit code.
|
|
1084
|
+
*/
|
|
1085
|
+
const trackEvent = (event) => {
|
|
1086
|
+
const payload = {
|
|
1087
|
+
api_key: POSTHOG_KEY,
|
|
1088
|
+
event: `cli_${event.command}`,
|
|
1089
|
+
distinct_id: getAnonymousId(),
|
|
1090
|
+
properties: {
|
|
1091
|
+
version: APP_VERSION,
|
|
1092
|
+
node_version: process.version,
|
|
1093
|
+
os: os.platform(),
|
|
1094
|
+
arch: os.arch(),
|
|
1095
|
+
languages: event.languages,
|
|
1096
|
+
score_bucket: event.scoreBucket,
|
|
1097
|
+
engine_issues: event.engineIssues,
|
|
1098
|
+
engine_timings: event.engineTimings,
|
|
1099
|
+
elapsed_ms: event.elapsedMs,
|
|
1100
|
+
file_count: event.fileCount,
|
|
1101
|
+
fix_steps: event.fixSteps,
|
|
1102
|
+
fix_resolved: event.fixResolved
|
|
1103
|
+
},
|
|
1104
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1105
|
+
};
|
|
1106
|
+
fetch(`${POSTHOG_HOST}/capture/`, {
|
|
1107
|
+
method: "POST",
|
|
1108
|
+
headers: { "Content-Type": "application/json" },
|
|
1109
|
+
body: JSON.stringify(payload),
|
|
1110
|
+
signal: AbortSignal.timeout(3e3)
|
|
1111
|
+
}).catch(() => {});
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1025
1114
|
//#endregion
|
|
1026
1115
|
//#region src/commands/fix.ts
|
|
1027
1116
|
const uniqueFiles = (diagnostics) => [...new Set(diagnostics.map((d) => d.filePath))];
|
|
@@ -1136,6 +1225,15 @@ const fixCommand = async (directory, config, options = {
|
|
|
1136
1225
|
logger.break();
|
|
1137
1226
|
summarizeFixRun(steps);
|
|
1138
1227
|
}
|
|
1228
|
+
if (!isTelemetryDisabled(config.telemetry?.enabled)) {
|
|
1229
|
+
const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
|
|
1230
|
+
trackEvent({
|
|
1231
|
+
command: "fix",
|
|
1232
|
+
languages: projectInfo.languages,
|
|
1233
|
+
fixSteps: steps.length,
|
|
1234
|
+
fixResolved: totalResolved
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1139
1237
|
logger.break();
|
|
1140
1238
|
logger.success(" ✓ Done. Run `aislop scan` to verify.");
|
|
1141
1239
|
logger.break();
|
|
@@ -1180,7 +1278,8 @@ const DEFAULT_CONFIG = {
|
|
|
1180
1278
|
ci: {
|
|
1181
1279
|
failBelow: 0,
|
|
1182
1280
|
format: "json"
|
|
1183
|
-
}
|
|
1281
|
+
},
|
|
1282
|
+
telemetry: { enabled: true }
|
|
1184
1283
|
};
|
|
1185
1284
|
const DEFAULT_CONFIG_YAML = `version: 1
|
|
1186
1285
|
|
|
@@ -1217,6 +1316,9 @@ scoring:
|
|
|
1217
1316
|
ci:
|
|
1218
1317
|
failBelow: 0
|
|
1219
1318
|
format: json
|
|
1319
|
+
|
|
1320
|
+
# telemetry:
|
|
1321
|
+
# enabled: true # set to false to disable anonymous usage analytics
|
|
1220
1322
|
`;
|
|
1221
1323
|
const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
|
|
1222
1324
|
# Uncomment and customize to enforce your project's conventions.
|
|
@@ -1277,6 +1379,7 @@ const CiSchema = z.object({
|
|
|
1277
1379
|
failBelow: z.number().default(0),
|
|
1278
1380
|
format: z.enum(["json"]).default("json")
|
|
1279
1381
|
});
|
|
1382
|
+
const TelemetrySchema = z.object({ enabled: z.boolean().default(true) });
|
|
1280
1383
|
const AislopConfigSchema = z.object({
|
|
1281
1384
|
version: z.number().default(1),
|
|
1282
1385
|
engines: EnginesSchema.default(() => ({
|
|
@@ -1307,7 +1410,8 @@ const AislopConfigSchema = z.object({
|
|
|
1307
1410
|
ci: CiSchema.default(() => ({
|
|
1308
1411
|
failBelow: 0,
|
|
1309
1412
|
format: "json"
|
|
1310
|
-
}))
|
|
1413
|
+
})),
|
|
1414
|
+
telemetry: TelemetrySchema.default(() => ({ enabled: true }))
|
|
1311
1415
|
});
|
|
1312
1416
|
const defaults = AislopConfigSchema.parse({});
|
|
1313
1417
|
/**
|
|
@@ -2395,17 +2499,21 @@ const isDataFile = (content) => {
|
|
|
2395
2499
|
const dataLinePattern = /^\s*[{}[\]"']/;
|
|
2396
2500
|
return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
|
|
2397
2501
|
};
|
|
2502
|
+
const ARROW_BLOCK_RE = /* @__PURE__ */ new RegExp("=>\\s*\\{");
|
|
2503
|
+
const ARROW_END_RE = /* @__PURE__ */ new RegExp("=>\\s*$");
|
|
2504
|
+
const BRACE_START_RE = /* @__PURE__ */ new RegExp("^\\s*\\{");
|
|
2505
|
+
const NEW_STATEMENT_RE = /* @__PURE__ */ new RegExp("^(?:export\\s+)?(?:const|let|var|function|class)\\s");
|
|
2398
2506
|
const isBlockArrow = (lines, startIndex) => {
|
|
2399
|
-
if (
|
|
2400
|
-
if (
|
|
2507
|
+
if (ARROW_BLOCK_RE.test(lines[startIndex])) return true;
|
|
2508
|
+
if (ARROW_END_RE.test(lines[startIndex])) {
|
|
2401
2509
|
const next = lines[startIndex + 1];
|
|
2402
|
-
if (next &&
|
|
2510
|
+
if (next && BRACE_START_RE.test(next)) return true;
|
|
2403
2511
|
}
|
|
2404
2512
|
for (let j = startIndex + 1; j < Math.min(startIndex + 3, lines.length); j++) {
|
|
2405
2513
|
const l = lines[j];
|
|
2406
|
-
if (l.trim() === "" ||
|
|
2407
|
-
if (
|
|
2408
|
-
if (
|
|
2514
|
+
if (l.trim() === "" || NEW_STATEMENT_RE.test(l.trim())) break;
|
|
2515
|
+
if (ARROW_BLOCK_RE.test(l)) return true;
|
|
2516
|
+
if (BRACE_START_RE.test(l)) return true;
|
|
2409
2517
|
}
|
|
2410
2518
|
return false;
|
|
2411
2519
|
};
|
|
@@ -3250,7 +3358,7 @@ const RISKY_PATTERNS = [
|
|
|
3250
3358
|
help: "Avoid dynamic code execution — refactor to use static code paths"
|
|
3251
3359
|
},
|
|
3252
3360
|
{
|
|
3253
|
-
pattern:
|
|
3361
|
+
pattern: new RegExp(`\\.innerHTML\\s*=`, "g"),
|
|
3254
3362
|
extensions: [
|
|
3255
3363
|
".ts",
|
|
3256
3364
|
".tsx",
|
|
@@ -3347,6 +3455,10 @@ const detectRiskyConstructs = async (context) => {
|
|
|
3347
3455
|
let match;
|
|
3348
3456
|
while ((match = regex.exec(content)) !== null) {
|
|
3349
3457
|
const line = content.slice(0, match.index).split("\n").length;
|
|
3458
|
+
if (name === "innerhtml") {
|
|
3459
|
+
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
3460
|
+
if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
|
|
3461
|
+
}
|
|
3350
3462
|
if (name === "sql-injection") {
|
|
3351
3463
|
const afterMatch = content.slice(match.index + match[0].length, match.index + match[0].length + 100);
|
|
3352
3464
|
if (/^(?:\w+\.join\s*\(|[A-Z_]+\}|tableName\}|table\})/.test(afterMatch)) continue;
|
|
@@ -3860,9 +3972,26 @@ const scanCommand = async (directory, config, options) => {
|
|
|
3860
3972
|
const allDiagnostics = results.flatMap((r) => r.diagnostics);
|
|
3861
3973
|
const elapsedMs = performance.now() - startTime;
|
|
3862
3974
|
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds);
|
|
3863
|
-
const exitCode = scoreResult.score < config.ci.failBelow ? 1 : 0;
|
|
3975
|
+
const exitCode = allDiagnostics.some((d) => d.severity === "error") || scoreResult.score < config.ci.failBelow ? 1 : 0;
|
|
3976
|
+
if (!isTelemetryDisabled(config.telemetry?.enabled)) {
|
|
3977
|
+
const engineIssues = {};
|
|
3978
|
+
const engineTimings = {};
|
|
3979
|
+
for (const r of results) {
|
|
3980
|
+
engineIssues[r.engine] = r.diagnostics.length;
|
|
3981
|
+
engineTimings[r.engine] = Math.round(r.elapsed);
|
|
3982
|
+
}
|
|
3983
|
+
trackEvent({
|
|
3984
|
+
command: options.command ?? "scan",
|
|
3985
|
+
languages: projectInfo.languages,
|
|
3986
|
+
scoreBucket: getScoreBucket(scoreResult.score),
|
|
3987
|
+
engineIssues,
|
|
3988
|
+
engineTimings,
|
|
3989
|
+
elapsedMs: Math.round(elapsedMs),
|
|
3990
|
+
fileCount: projectInfo.sourceFileCount
|
|
3991
|
+
});
|
|
3992
|
+
}
|
|
3864
3993
|
if (options.json) {
|
|
3865
|
-
const { buildJsonOutput } = await import("./json-
|
|
3994
|
+
const { buildJsonOutput } = await import("./json-BMSa_G7o.js");
|
|
3866
3995
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
3867
3996
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
3868
3997
|
return { exitCode };
|