@scantrix/cli 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Scantrix
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ <p align="center">
2
+ <img src="docs/scantrix-logo-light.svg" alt="Scantrix" width="420" />
3
+ </p>
4
+
5
+ <h3 align="center">
6
+ <em>Playwright</em> · <em>Cypress</em> ·<em> Selenium</em></br>
7
+ Find out why your tests keep failing without running a single one.
8
+ </h3>
9
+
10
+ <p align="center">
11
+ <a href="#quick-start">Quick Start</a> &middot;
12
+ <a href="#output-formats">Output</a> &middot;
13
+ <a href="#cli-reference">CLI Reference</a> &middot;
14
+ <a href="#github-action">GitHub Action</a> &middot;
15
+ <a href="docs/RULES.md">Rules</a> &middot;
16
+ <a href="#license">License</a>
17
+ </p>
18
+
19
+ <p align="center">
20
+ <a href="https://github.com/Scantrix/scantrix/actions/workflows/ci.yml">
21
+ <img src="https://img.shields.io/github/actions/workflow/status/Scantrix/scantrix/ci.yml?branch=main&label=build" alt="Build Status" />
22
+ </a>
23
+ <a href="https://www.npmjs.com/package/@scantrix/cli">
24
+ <img src="https://img.shields.io/npm/v/@scantrix/cli?color=cb3837&logo=npm&logoColor=white" alt="NPM Version" />
25
+ </a>
26
+ <a href="#license">
27
+ <img src="https://img.shields.io/badge/license-MIT-blue" alt="License: MIT" />
28
+ </a>
29
+ </p>
30
+
31
+ ---
32
+
33
+ Most tools tell you a test failed. **Scantrix tells you why your entire approach is failing** and exactly how to fix it.
34
+
35
+ No test execution. No credentials. No file modifications. Just root causes.
36
+
37
+ ### EXAMPLE FINDING
38
+ ![Example Finding](docs/images/findings.svg)
39
+
40
+ ### KEY CAPABILITIES
41
+ >
42
+ >| Capability | Traditional Reporters | Scantrix |
43
+ >|-------------------|---------------------------|-----------------------------------|
44
+ >| Analysis | Runtime (Post Failure) | Static (Pre-emptive) |
45
+ >| Scope | Single Test Result | Repository wide Health |
46
+ >| Root Cause | Stack Trace only | Correlated Design Patterns |
47
+ >| Actionability | "Test Failed" | Step-by-step Fix Guidance |
48
+ >| CI Impact | Log bloat | CI Pipeline Optimization |
49
+ >| Security | Requires Tokens | Zero Footprint (Local Execution) |
50
+
51
+ </br>
52
+
53
+ ### RISK OVERVIEW
54
+
55
+ ![Risk Overview](docs/images/risk.svg)
56
+
57
+ ### RECOMMENDED ACTIONS
58
+
59
+ ![Recommended Actions](docs/images/recommendations.svg)
60
+
61
+
62
+ ## What Scantrix Diagnoses
63
+
64
+ **Flaky test root causes.** Not just which tests fail, but exactly why they fail and what will actually fix them.
65
+
66
+ **Framework-level design debt.** Structural patterns that silently destabilize entire test suites before anyone realizes something is wrong.
67
+
68
+ **CI configuration risks.** The environmental factors making your pipeline unpredictable run to run.
69
+
70
+ **Blast radius of bad patterns.** One anti-pattern can affect hundreds of tests. Scantrix quantifies that exposure so you know where to act first.
71
+
72
+ **Execution waste.** Where time is being burned in your pipeline and the precise changes that recover it.
73
+
74
+
75
+ ## What Scantrix Does NOT Do
76
+ Scantrix operates as a zero-footprint, read-only analyzer, ensuring repository integrity while maintaining a strict security posture. <br>
77
+ - It does not execute your tests <br>
78
+ - It does not modify any files in the target repository <br>
79
+ - It does not make network connections beyond the local filesystem <br>
80
+ - It does not require credentials and access tokens of any kind </br>
81
+
82
+
83
+ ## Quick Start
84
+
85
+ ```bash
86
+ # Install dependencies
87
+ npm install
88
+
89
+ # Build
90
+ npm run build
91
+
92
+ # Scan a repository
93
+ node dist/cli.js /path/to/repo --out ./audit-out
94
+
95
+ # Or use the shorthand (builds + scans)
96
+ npm run audit -- /path/to/repo --out ./audit-out
97
+ ```
98
+
99
+ ## Output Formats
100
+
101
+ Every scan produces an **overall risk grade (A through F)** alongside severity-bucketed findings.
102
+
103
+ | Format | File | Description |
104
+ |--------|------|-------------|
105
+ | Markdown | `audit_summary.md` | Primary report with executive-grade findings and hotspots |
106
+ | HTML | `audit_summary.html` | Styled report for browser viewing |
107
+ | JSON | `findings.json` | Machine-readable findings array |
108
+ | SARIF | `audit.sarif` | Native GitHub Code Scanning and Azure DevOps support — findings appear directly in the Security tab |
109
+ | Email | `audit_email.html` | Email-ready HTML summary |
110
+
111
+ Use `--format` to select specific formats:
112
+
113
+ ```bash
114
+ node dist/cli.js /path/to/repo --out ./audit-out --format md,json,sarif
115
+ ```
116
+
117
+
118
+
119
+ ## CLI Reference
120
+
121
+ ```
122
+ scantrix <repoPath> [options]
123
+ scantrix init [dir]
124
+ ```
125
+
126
+ ### Scan Options
127
+
128
+ | Flag | Description | Default |
129
+ |------|-------------|---------|
130
+ | `--out <dir>` | Output directory for reports | `./audit-out` |
131
+ | `--format <list>` | Comma-separated formats: `md,html,json,sarif,email` | all |
132
+ | `--config <path>` | Path to `.auditrc.json` config file | auto-detected |
133
+ | `--updates` | Check npm registry for outdated dependencies | off |
134
+ | `--diff <path>` | Explicit baseline `findings.json` for comparison | auto |
135
+ | `--json-path <path>` | Write canonical `ScanResult` JSON to this path | — |
136
+
137
+ ### Environment Variables
138
+
139
+ | Variable | Description |
140
+ |----------|-------------|
141
+ | `SCANTRIX_JSON_PATH` | Write canonical results JSON to this path |
142
+
143
+ ### Init Subcommand
144
+
145
+ ```bash
146
+ # Create a starter .auditrc.json in the target repo
147
+ node dist/cli.js init /path/to/repo
148
+ ```
149
+
150
+ ## Configuration
151
+
152
+ Create an `.auditrc.json` in the target repository to customize behavior:
153
+
154
+ ```bash
155
+ node dist/cli.js init /path/to/repo
156
+ ```
157
+
158
+ The config file supports rule overrides (disable rules, adjust severity) and scan options. See [CLI Architecture](docs/CLI_ARCHITECTURE.md) for details.
159
+
160
+ ## GitHub Action
161
+
162
+ Use Scantrix in CI with the provided composite action:
163
+
164
+ ```yaml
165
+ - uses: Scantrix/scantrix@v1
166
+ with:
167
+ repo-path: "."
168
+ output-dir: "./scantrix-out"
169
+ format: "md,html,json,sarif"
170
+ post-comment: "true" # Post summary on PRs
171
+ fail-on-high: "true" # Block merges with high-severity findings
172
+ ```
173
+
174
+ ### Action Inputs
175
+
176
+ | Input | Description | Default |
177
+ |-------|-------------|---------|
178
+ | `repo-path` | Path to repository to scan | `.` |
179
+ | `output-dir` | Directory for audit artifacts | `./scantrix-out` |
180
+ | `config-path` | Path to `.auditrc.json` | — |
181
+ | `format` | Output formats (comma-separated) | `md,html,json,sarif` |
182
+ | `check-updates` | Check for outdated dependencies | `false` |
183
+ | `post-comment` | Post summary comment on PRs | `true` |
184
+ | `fail-on-high` | Fail if high-severity findings exist | `false` |
185
+
186
+ ### Action Outputs
187
+
188
+ | Output | Description |
189
+ |--------|-------------|
190
+ | `findings-count` | Total number of findings |
191
+ | `high-count` | Number of high-severity findings |
192
+ | `medium-count` | Number of medium-severity findings |
193
+ | `low-count` | Number of low-severity findings |
194
+ | `risk-grade` | Overall risk grade (A through F) |
195
+ | `report-path` | Path to generated report directory |
196
+
197
+ ## Documentation
198
+
199
+ - [Rules Reference](docs/RULES.md) — all 87 finding rules with severity and description
200
+ - [CI Integration](docs/CI_INTEGRATION.md) — CI pipeline setup and quality gates
201
+ - [CLI Architecture](docs/CLI_ARCHITECTURE.md) — module diagram and data flow
202
+ - [Security](SECURITY.md) — security model and threat considerations
203
+ - [Finding Reference](docs/FINDING_REFERENCE.md) — complete finding ID catalog
204
+
205
+ ## Contributing
206
+
207
+ Contributions are welcome. Please open an issue to discuss proposed changes before submitting a pull request.
208
+
209
+ ```bash
210
+ # Run the test suite
211
+ npm test
212
+
213
+ # Run tests in watch mode
214
+ npm run test:watch
215
+ ```
216
+
217
+ ## License
218
+
219
+ [MIT](LICENSE) — See LICENSE file.</br>
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ /**
3
+ * AST-based Playwright config parser using the TypeScript Compiler API.
4
+ *
5
+ * This module parses playwright.config.ts/js using a real TypeScript AST instead
6
+ * of regexes, handling:
7
+ * - Computed values: `60 * 1000`, `2 * 60 * 1000`
8
+ * - Spread operators: `...baseUse` (recorded as unknown)
9
+ * - Ternary expressions: `process.env.CI ? 2 : 0` (recorded as the raw source text)
10
+ * - Variable references within the same file
11
+ * - `satisfies` keyword (TS 4.9+)
12
+ * - Template literals (partial)
13
+ * - Array/object literals (recursed)
14
+ *
15
+ * Falls back gracefully when encountering unsupported patterns.
16
+ */
17
+ var __importDefault = (this && this.__importDefault) || function (mod) {
18
+ return (mod && mod.__esModule) ? mod : { "default": mod };
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.parseConfigWithAst = parseConfigWithAst;
22
+ const typescript_1 = __importDefault(require("typescript"));
23
+ // ─── Public API ──────────────────────────────────────────────────────
24
+ /**
25
+ * Parse a Playwright config file using the TypeScript compiler API.
26
+ * Returns a structured PlaywrightConfigSummary, or undefined if parsing fails.
27
+ */
28
+ function parseConfigWithAst(filePath, sourceText) {
29
+ try {
30
+ const sourceFile = typescript_1.default.createSourceFile(filePath, sourceText, typescript_1.default.ScriptTarget.Latest,
31
+ /* setParentNodes */ true);
32
+ // Build a simple scope of top-level `const x = ...` declarations
33
+ const scope = buildFileScope(sourceFile);
34
+ // Find the config object: `defineConfig({...})` or `export default {...}`
35
+ const configNode = findConfigObject(sourceFile);
36
+ if (!configNode)
37
+ return undefined;
38
+ const raw = evaluateObject(configNode, scope);
39
+ if (!raw || typeof raw !== "object")
40
+ return undefined;
41
+ return mapToSummary(filePath, raw, scope);
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ function buildFileScope(sourceFile) {
48
+ const scope = new Map();
49
+ for (const stmt of sourceFile.statements) {
50
+ // const FOO = expr;
51
+ if (typescript_1.default.isVariableStatement(stmt)) {
52
+ for (const decl of stmt.declarationList.declarations) {
53
+ if (typescript_1.default.isIdentifier(decl.name) && decl.initializer) {
54
+ scope.set(decl.name.text, decl.initializer);
55
+ }
56
+ }
57
+ }
58
+ }
59
+ return scope;
60
+ }
61
+ // ─── Config Object Finder ────────────────────────────────────────────
62
+ function findConfigObject(sourceFile) {
63
+ let result;
64
+ function visit(node) {
65
+ // defineConfig({ ... })
66
+ if (typescript_1.default.isCallExpression(node) &&
67
+ typescript_1.default.isIdentifier(node.expression) &&
68
+ node.expression.text === "defineConfig" &&
69
+ node.arguments.length >= 1) {
70
+ const arg = node.arguments[0];
71
+ if (typescript_1.default.isObjectLiteralExpression(arg)) {
72
+ result = arg;
73
+ return;
74
+ }
75
+ }
76
+ // export default { ... }
77
+ if (typescript_1.default.isExportAssignment(node) && !node.isExportEquals) {
78
+ let expr = node.expression;
79
+ // Handle: export default { ... } satisfies PlaywrightTestConfig
80
+ if (typescript_1.default.isSatisfiesExpression?.(expr)) {
81
+ expr = expr.expression;
82
+ }
83
+ // Handle: export default { ... } as PlaywrightTestConfig
84
+ if (typescript_1.default.isAsExpression(expr)) {
85
+ expr = expr.expression;
86
+ }
87
+ if (typescript_1.default.isObjectLiteralExpression(expr)) {
88
+ result = expr;
89
+ return;
90
+ }
91
+ }
92
+ typescript_1.default.forEachChild(node, visit);
93
+ }
94
+ visit(sourceFile);
95
+ return result;
96
+ }
97
+ // ─── AST Evaluator ───────────────────────────────────────────────────
98
+ // Tries to evaluate AST nodes into plain JS values.
99
+ // Returns undefined for unevaluable nodes, or the raw source text as a string.
100
+ function evaluateNode(node, scope) {
101
+ // Numeric literal
102
+ if (typescript_1.default.isNumericLiteral(node)) {
103
+ return Number(node.text);
104
+ }
105
+ // String literal
106
+ if (typescript_1.default.isStringLiteral(node) || typescript_1.default.isNoSubstitutionTemplateLiteral(node)) {
107
+ return node.text;
108
+ }
109
+ // Boolean
110
+ if (node.kind === typescript_1.default.SyntaxKind.TrueKeyword)
111
+ return true;
112
+ if (node.kind === typescript_1.default.SyntaxKind.FalseKeyword)
113
+ return false;
114
+ if (node.kind === typescript_1.default.SyntaxKind.UndefinedKeyword)
115
+ return undefined;
116
+ if (node.kind === typescript_1.default.SyntaxKind.NullKeyword)
117
+ return null;
118
+ // Identifier — look up in scope
119
+ if (typescript_1.default.isIdentifier(node)) {
120
+ const ref = scope.get(node.text);
121
+ if (ref)
122
+ return evaluateNode(ref, scope);
123
+ return `<ref:${node.text}>`;
124
+ }
125
+ // Binary expression (multiplication, addition)
126
+ if (typescript_1.default.isBinaryExpression(node)) {
127
+ const left = evaluateNode(node.left, scope);
128
+ const right = evaluateNode(node.right, scope);
129
+ if (typeof left === "number" && typeof right === "number") {
130
+ switch (node.operatorToken.kind) {
131
+ case typescript_1.default.SyntaxKind.AsteriskToken: return left * right;
132
+ case typescript_1.default.SyntaxKind.PlusToken: return left + right;
133
+ case typescript_1.default.SyntaxKind.MinusToken: return left - right;
134
+ case typescript_1.default.SyntaxKind.SlashToken: return right !== 0 ? left / right : undefined;
135
+ }
136
+ }
137
+ // Logical OR for fallback patterns: `process.env.X || 'default'`
138
+ if (node.operatorToken.kind === typescript_1.default.SyntaxKind.BarBarToken) {
139
+ // Return the raw source expression as a string for display
140
+ return node.getText();
141
+ }
142
+ // Return raw expression text
143
+ return node.getText();
144
+ }
145
+ // Conditional (ternary) expression
146
+ if (typescript_1.default.isConditionalExpression(node)) {
147
+ // We can't evaluate process.env.CI at parse time, so return the raw text
148
+ return node.getText();
149
+ }
150
+ // Property access: process.env.CI, devices['Desktop Chrome']
151
+ if (typescript_1.default.isPropertyAccessExpression(node) || typescript_1.default.isElementAccessExpression(node)) {
152
+ return node.getText();
153
+ }
154
+ // Prefix unary: !condition, -number
155
+ if (typescript_1.default.isPrefixUnaryExpression(node)) {
156
+ const operand = evaluateNode(node.operand, scope);
157
+ if (node.operator === typescript_1.default.SyntaxKind.ExclamationToken && typeof operand === "boolean")
158
+ return !operand;
159
+ if (node.operator === typescript_1.default.SyntaxKind.MinusToken && typeof operand === "number")
160
+ return -operand;
161
+ return node.getText();
162
+ }
163
+ // Object literal
164
+ if (typescript_1.default.isObjectLiteralExpression(node)) {
165
+ return evaluateObject(node, scope);
166
+ }
167
+ // Array literal
168
+ if (typescript_1.default.isArrayLiteralExpression(node)) {
169
+ return node.elements.map((el) => evaluateNode(el, scope));
170
+ }
171
+ // Spread element
172
+ if (typescript_1.default.isSpreadElement(node)) {
173
+ return { __spread: node.expression.getText() };
174
+ }
175
+ // Call expression: require.resolve('...'), etc.
176
+ if (typescript_1.default.isCallExpression(node)) {
177
+ // Try to get the first string argument
178
+ if (node.arguments.length > 0 && typescript_1.default.isStringLiteral(node.arguments[0])) {
179
+ return node.arguments[0].text;
180
+ }
181
+ return node.getText();
182
+ }
183
+ // Template expression: `${process.env.BASE_URL}/api`
184
+ if (typescript_1.default.isTemplateExpression(node)) {
185
+ return node.getText();
186
+ }
187
+ // ParenthesizedExpression
188
+ if (typescript_1.default.isParenthesizedExpression(node)) {
189
+ return evaluateNode(node.expression, scope);
190
+ }
191
+ // `satisfies` expression (TS 4.9+)
192
+ if (typescript_1.default.isSatisfiesExpression?.(node)) {
193
+ return evaluateNode(node.expression, scope);
194
+ }
195
+ // `as` expression
196
+ if (typescript_1.default.isAsExpression(node)) {
197
+ return evaluateNode(node.expression, scope);
198
+ }
199
+ // Fallback: raw text
200
+ return node.getText();
201
+ }
202
+ function evaluateObject(node, scope) {
203
+ const result = {};
204
+ for (const prop of node.properties) {
205
+ // Spread assignment: ...baseConfig
206
+ if (typescript_1.default.isSpreadAssignment(prop)) {
207
+ result.__spreads = result.__spreads || [];
208
+ result.__spreads.push(prop.expression.getText());
209
+ continue;
210
+ }
211
+ if (typescript_1.default.isPropertyAssignment(prop)) {
212
+ const key = getPropertyName(prop);
213
+ if (key) {
214
+ result[key] = evaluateNode(prop.initializer, scope);
215
+ }
216
+ }
217
+ // Shorthand property: { workers }
218
+ if (typescript_1.default.isShorthandPropertyAssignment(prop)) {
219
+ const key = prop.name.text;
220
+ const ref = scope.get(key);
221
+ result[key] = ref ? evaluateNode(ref, scope) : `<ref:${key}>`;
222
+ }
223
+ // Method declaration: ignored (not typical in config)
224
+ }
225
+ return result;
226
+ }
227
+ function getPropertyName(prop) {
228
+ const name = prop.name;
229
+ if (typescript_1.default.isIdentifier(name))
230
+ return name.text;
231
+ if (typescript_1.default.isStringLiteral(name))
232
+ return name.text;
233
+ if (typescript_1.default.isComputedPropertyName(name))
234
+ return undefined; // can't evaluate
235
+ return undefined;
236
+ }
237
+ // ─── Mapping to PlaywrightConfigSummary ──────────────────────────────
238
+ function asNumber(v) {
239
+ if (typeof v === "number" && Number.isFinite(v))
240
+ return v;
241
+ return undefined;
242
+ }
243
+ function asString(v) {
244
+ if (typeof v === "string")
245
+ return v;
246
+ return undefined;
247
+ }
248
+ function mapToSummary(filePath, raw, scope) {
249
+ const summary = { configPath: filePath };
250
+ summary.testDir = asString(raw.testDir);
251
+ summary.timeoutMs = asNumber(raw.timeout);
252
+ summary.workers = raw.workers !== undefined ? String(raw.workers) : undefined;
253
+ summary.retries = raw.retries !== undefined ? String(raw.retries) : undefined;
254
+ summary.globalSetup = asString(raw.globalSetup);
255
+ // expect block
256
+ if (raw.expect && typeof raw.expect === "object") {
257
+ summary.expectTimeoutMs = asNumber(raw.expect.timeout);
258
+ }
259
+ // use block
260
+ if (raw.use && typeof raw.use === "object") {
261
+ const u = raw.use;
262
+ summary.use = {
263
+ headless: u.headless !== undefined ? String(u.headless) : undefined,
264
+ actionTimeoutMs: asNumber(u.actionTimeout),
265
+ trace: asString(u.trace),
266
+ video: asString(u.video),
267
+ screenshot: asString(u.screenshot),
268
+ ignoreHTTPSErrors: u.ignoreHTTPSErrors === true ? true :
269
+ u.ignoreHTTPSErrors === false ? false : undefined,
270
+ baseURL: asString(u.baseURL),
271
+ };
272
+ // Parse launchOptions.args if present
273
+ if (u.launchOptions && typeof u.launchOptions === "object" && Array.isArray(u.launchOptions.args)) {
274
+ summary.use.launchArgs = u.launchOptions.args.filter((a) => typeof a === "string");
275
+ }
276
+ }
277
+ // reporters
278
+ if (Array.isArray(raw.reporter)) {
279
+ summary.reporters = raw.reporter
280
+ .map((r) => {
281
+ if (typeof r === "string")
282
+ return { name: r };
283
+ if (Array.isArray(r) && typeof r[0] === "string") {
284
+ return { name: r[0], details: r[1] && typeof r[1] === "object" ? r[1] : undefined };
285
+ }
286
+ return undefined;
287
+ })
288
+ .filter((x) => x != null);
289
+ }
290
+ // projects
291
+ if (Array.isArray(raw.projects)) {
292
+ summary.projects = raw.projects
293
+ .map((p) => {
294
+ if (!p || typeof p !== "object")
295
+ return undefined;
296
+ return {
297
+ name: asString(p.name) ?? "unknown",
298
+ grep: asString(p.grep),
299
+ browserName: asString(p.browserName),
300
+ isMobile: p.isMobile === true ? true : p.isMobile === false ? false : undefined,
301
+ };
302
+ })
303
+ .filter((x) => x != null);
304
+ }
305
+ // env hints — detect from raw source text patterns
306
+ // (these are derived from text patterns, not AST; we keep them for compatibility)
307
+ return summary;
308
+ }