@methiu/raigal 0.2.6 → 0.2.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/LICENSE +6 -1
- package/README.md +18 -23
- package/THIRD_PARTY_NOTICES.md +70 -0
- package/dist/cli.js +169 -70
- package/dist/index.js +168 -69
- package/dist/{json-DxIA7CCt.js → json-B2Kc7_rw.js} +1 -1
- package/dist/mcp.js +150 -53
- package/dist/{sarif-Cu7Rm1Ma.js → sarif-CpMse0iL.js} +1 -1
- package/dist/{version-DtOeK-2T.js → version-CXMYu17z.js} +1 -1
- package/package.json +7 -4
- package/scripts/check-tarball-keys.mjs +26 -0
package/LICENSE
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
Copyright (c) 2026 Raigal. All rights reserved.
|
|
2
|
+
|
|
3
|
+
PROPRIETARY. Use is permitted only under a written agreement with Raigal.
|
|
4
|
+
|
|
5
|
+
This software includes third-party components licensed under their own terms.
|
|
6
|
+
See THIRD_PARTY_NOTICES.md for those components and their licences.
|
package/README.md
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@methiu/raigal) [](https://www.npmjs.com/package/@methiu/raigal) [](https://github.com/meethiu/raigal/actions/workflows/ci.yml) [](https://nodejs.org)
|
|
6
6
|
The patterns Claude Code, Cursor, Codex, and OpenCode leave behind: narrative comments above self-explanatory code, swallowed exceptions, hidden fallbacks, `as any` casts, hallucinated imports, duplicated helpers, dead code, todo stubs, oversized functions. Tests pass. Lint passes. The code rots anyway.
|
|
7
7
|
|
|
8
|
-
Raigal catches them. 50+ rules across 10 language targets (TypeScript, JavaScript, Expo / React Native, Python, Go, Rust, Ruby, PHP, C#, C/C++). Scores every change 0-100.
|
|
8
|
+
Raigal catches them. 50+ rules across 10 language targets (TypeScript, JavaScript, Expo / React Native, Python, Go, Rust, Ruby, PHP, C#, C/C++). Scores every change 0-100. No LLM in the scoring path: the same code and tool versions give the same score.
|
|
9
9
|
|
|
10
10
|
## Quick start
|
|
11
11
|
|
|
12
|
-
Raigal is
|
|
12
|
+
Raigal is licensed to organizations on an admin-managed allowlist. Runs require authentication via organization credentials (`raigal login` or `RAIGAL_TOKEN`).
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
# 1. Sign in with your organization account
|
|
@@ -21,7 +21,7 @@ raigal scan
|
|
|
21
21
|
|
|
22
22
|
In CI and automated workflows, provide your organization API key via the `RAIGAL_TOKEN` environment variable.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Install it with npm, pnpm, Yarn, or Bun:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
npm install -g @methiu/raigal
|
|
@@ -53,7 +53,7 @@ raigal hook install --claude # per-edit hook
|
|
|
53
53
|
|
|
54
54
|
## Installation
|
|
55
55
|
|
|
56
|
-
The
|
|
56
|
+
The CLI is published to npm as `@methiu/raigal`. The unscoped `raigal` package is an official alias that installs the same CLI.
|
|
57
57
|
|
|
58
58
|
**Node / npm**
|
|
59
59
|
|
|
@@ -79,22 +79,6 @@ npm install -g @methiu/raigal
|
|
|
79
79
|
|
|
80
80
|
Package installation does not run dependency lifecycle scripts. After installing, run `raigal-tools` once if you want bundled Ruff and golangci-lint coverage; the core scanner works without it.
|
|
81
81
|
|
|
82
|
-
**Homebrew** (macOS / Linux)
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
brew install meethiu/tap/raigal
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Homebrew installs Node.js as a dependency if it isn't already present. Details: [homebrew-tap](https://github.com/meethiu/homebrew-tap).
|
|
89
|
-
|
|
90
|
-
**Python / pipx**
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
pipx install raigal
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
`pipx` keeps `raigal` in its own isolated environment. Needs Node.js on `PATH`.
|
|
97
|
-
|
|
98
82
|
Full reference for every channel, optional bundled tooling, and external tools: [Installation Docs](https://docs.raigal.dev/getting-started/installation).
|
|
99
83
|
|
|
100
84
|
---
|
|
@@ -286,7 +270,7 @@ Expose Raigal as MCP tools for Claude Desktop, Cursor, Codex:
|
|
|
286
270
|
"mcpServers": {
|
|
287
271
|
"raigal": {
|
|
288
272
|
"command": "npx",
|
|
289
|
-
"args": ["-y", "@methiu/raigal
|
|
273
|
+
"args": ["-y", "@methiu/raigal@latest", "mcp"]
|
|
290
274
|
}
|
|
291
275
|
}
|
|
292
276
|
}
|
|
@@ -310,7 +294,7 @@ raigal ci --sarif # SARIF output for code scanning
|
|
|
310
294
|
```bash
|
|
311
295
|
raigal # interactive menu
|
|
312
296
|
raigal init # interactive setup: engines, threshold, and CI pipeline (GitHub, GitLab, Bitbucket)
|
|
313
|
-
raigal init --strict #
|
|
297
|
+
raigal init --strict # strict gate: all engines, typecheck, failBelow 85
|
|
314
298
|
raigal doctor # check which engines can run here
|
|
315
299
|
raigal rules # list rules with severity, fixability, and score impact
|
|
316
300
|
raigal rules --search # searchable rule explorer
|
|
@@ -485,9 +469,20 @@ AI coding tools generate code that compiles and passes tests but ships with patt
|
|
|
485
469
|
|
|
486
470
|
- **One score**: 0-100, enforced in CI. Weighted so sloppy patterns hit harder than style noise.
|
|
487
471
|
- **Auto-fix first**: Clears formatters, unused imports, dead code mechanically. Hands off the rest to your agent with full context.
|
|
488
|
-
- **
|
|
472
|
+
- **No LLM in scoring**: Regex + AST + standard tooling. The same code and tool versions give the same score. See [Network access](#network-access) for the calls a run does make.
|
|
489
473
|
- **Fast onboarding**: Runs with your organization session (`raigal login`) or CI token (`RAIGAL_TOKEN`). Add `.raigal/config.yml` to tune.
|
|
490
474
|
|
|
475
|
+
## Network access
|
|
476
|
+
|
|
477
|
+
Raigal scores code on your machine, but a run is not offline. It contacts:
|
|
478
|
+
|
|
479
|
+
- **Raigal Cloud** (`app.raigal.dev`): sign-in, the organization licence check, and the upload of run results while you are signed in. In GitHub Actions it also requests an OIDC token from GitHub.
|
|
480
|
+
- **Package registries**, through dependency audits (`npm audit`, `pnpm audit`, `bun audit`, `pip-audit` when installed). On by default; set `security.audit: false` to turn them off.
|
|
481
|
+
- **npm registry**, to check for a newer CLI version. Turn off with `RAIGAL_NO_UPDATE_NOTIFIER=1`.
|
|
482
|
+
- **Usage telemetry** (PostHog, EU host). Off in CI; turn off with `RAIGAL_NO_TELEMETRY=1` or `DO_NOT_TRACK=1`.
|
|
483
|
+
|
|
484
|
+
`raigal-tools` downloads Ruff and golangci-lint only when you run it.
|
|
485
|
+
|
|
491
486
|
## What it catches
|
|
492
487
|
|
|
493
488
|
Six deterministic engines run in parallel:
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Third-Party Software Notices and Information
|
|
2
|
+
|
|
3
|
+
This product contains components subject to open source licenses.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## aislop
|
|
8
|
+
|
|
9
|
+
Portions of this software are derived from aislop (https://github.com/scanaislop/aislop),
|
|
10
|
+
used under the MIT License. Raigal's own code is not covered by this notice; see `LICENSE`.
|
|
11
|
+
|
|
12
|
+
MIT License
|
|
13
|
+
|
|
14
|
+
Copyright (c) 2025 heavykenny
|
|
15
|
+
|
|
16
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
17
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
18
|
+
in the Software without restriction, including without limitation the rights
|
|
19
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
20
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
21
|
+
furnished to do so, subject to the following conditions:
|
|
22
|
+
|
|
23
|
+
The above copyright notice and this permission notice shall be included in all
|
|
24
|
+
copies or substantial portions of the Software.
|
|
25
|
+
|
|
26
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
27
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
28
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
29
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
30
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
31
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
32
|
+
SOFTWARE.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Third-Party Package Dependencies
|
|
37
|
+
|
|
38
|
+
The following third-party packages may be bundled into or used by distributions of this software:
|
|
39
|
+
|
|
40
|
+
- `@biomejs/biome` (MIT OR Apache-2.0) - https://github.com/biomejs/biome
|
|
41
|
+
- `@clack/prompts` (MIT) - https://github.com/natemoo-re/clack
|
|
42
|
+
- `@modelcontextprotocol/sdk` (MIT) - https://github.com/modelcontextprotocol/typescript-sdk
|
|
43
|
+
- `adm-zip` (MIT) - https://github.com/cthackers/adm-zip
|
|
44
|
+
- `commander` (MIT) - https://github.com/tj/commander.js
|
|
45
|
+
- `expo-doctor` (MIT) - https://github.com/expo/expo
|
|
46
|
+
- `ink` (MIT) - https://github.com/vadimdemedes/ink
|
|
47
|
+
- `ink-select-input` (MIT) - https://github.com/vadimdemedes/ink-select-input
|
|
48
|
+
- `knip` (ISC) - https://github.com/webpro-nl/knip
|
|
49
|
+
- `micromatch` (MIT) - https://github.com/micromatch/micromatch
|
|
50
|
+
- `oxlint` (MIT) - https://github.com/oxc-project/oxc
|
|
51
|
+
- `picocolors` (ISC) - https://github.com/alexeyraspopov/picocolors
|
|
52
|
+
- `react` (MIT) - https://github.com/facebook/react
|
|
53
|
+
- `smol-toml` (BSD-3-Clause) - https://github.com/cyyynthia/smol-toml
|
|
54
|
+
- `tar` (BlueOak-1.0.0 / ISC) - https://github.com/isaacs/node-tar
|
|
55
|
+
- `typescript` (Apache-2.0) - https://github.com/microsoft/TypeScript
|
|
56
|
+
- `wcwidth` (MIT) - https://github.com/timoxley/wcwidth
|
|
57
|
+
- `yaml` (ISC) - https://github.com/eemeli/yaml
|
|
58
|
+
- `zod` (MIT) - https://github.com/colinhacks/zod
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## External Tools (Downloaded Standalone Binaries)
|
|
63
|
+
|
|
64
|
+
The following external tools are downloaded separately as standalone binaries by `raigal-tools` and executed as separate child processes. They are NOT bundled or redistributed inside the Raigal JavaScript library:
|
|
65
|
+
|
|
66
|
+
- `ruff` (MIT / Apache-2.0) - https://github.com/astral-sh/ruff
|
|
67
|
+
- `golangci-lint` (GPL-3.0) - https://github.com/golangci/golangci-lint
|
|
68
|
+
- `AsyncFixer` (Apache-2.0) - https://github.com/semihokur/AsyncFixer
|
|
69
|
+
- `Meziantou.Analyzer` (MIT) - https://github.com/meziantou/Meziantou.Analyzer
|
|
70
|
+
- `IDisposableAnalyzers` (MIT) - https://github.com/DotNetAnalyzers/IDisposableAnalyzers
|
package/dist/cli.js
CHANGED
|
@@ -923,7 +923,7 @@ const createLogger = (deps = {}) => {
|
|
|
923
923
|
const log = createLogger();
|
|
924
924
|
//#endregion
|
|
925
925
|
//#region src/version.ts
|
|
926
|
-
const APP_VERSION = "0.2.
|
|
926
|
+
const APP_VERSION = "0.2.7";
|
|
927
927
|
//#endregion
|
|
928
928
|
//#region src/commands/agent-connect.ts
|
|
929
929
|
const commandText = (status) => [status.provider.loginCommand.command, ...status.provider.loginCommand.args].join(" ");
|
|
@@ -1209,7 +1209,7 @@ const AllowedOwner = z.object({
|
|
|
1209
1209
|
login: z.string().max(100)
|
|
1210
1210
|
});
|
|
1211
1211
|
const EntitlementClaims = z.object({
|
|
1212
|
-
iss: z.literal(
|
|
1212
|
+
iss: z.literal("https://app.raigal.dev"),
|
|
1213
1213
|
sub: z.string(),
|
|
1214
1214
|
org_name: z.string(),
|
|
1215
1215
|
plan: z.enum(["trial", "active"]),
|
|
@@ -1356,14 +1356,6 @@ const revokeSessionApi = async (token) => {
|
|
|
1356
1356
|
const EMBEDDED_PUBLIC_KEYS = { rgl_key_2026_1: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA69B+1w8c4yJpEURW4Ps2b4BZcoIzeBg3jkCfIo2n2eM=\n-----END PUBLIC KEY-----" };
|
|
1357
1357
|
const keyObjectCache = /* @__PURE__ */ new Map();
|
|
1358
1358
|
const getPublicKey = (kid) => {
|
|
1359
|
-
if (process.env.RAIGAL_KEY_ID === kid && process.env.RAIGAL_PUBLIC_KEY) {
|
|
1360
|
-
const customKey = process.env.RAIGAL_PUBLIC_KEY.replace(/\\n/g, "\n");
|
|
1361
|
-
try {
|
|
1362
|
-
return createPublicKey(customKey);
|
|
1363
|
-
} catch {
|
|
1364
|
-
return null;
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
1359
|
const cached = keyObjectCache.get(kid);
|
|
1368
1360
|
if (cached) return cached;
|
|
1369
1361
|
const pem = EMBEDDED_PUBLIC_KEYS[kid];
|
|
@@ -1423,9 +1415,8 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
|
|
|
1423
1415
|
const parsed = EntitlementClaims.safeParse(rawPayload);
|
|
1424
1416
|
if (!parsed.success) throw new JwtVerificationError(`Invalid entitlement claims: ${parsed.error.message}`, "invalid_claims");
|
|
1425
1417
|
const claims = parsed.data;
|
|
1426
|
-
|
|
1427
|
-
if (claims.
|
|
1428
|
-
if (!isVersionAtLeast("0.2.6", claims.min_cli_version)) throw new JwtVerificationError(`CLI version ${APP_VERSION} is below required minimum ${claims.min_cli_version}. Run "raigal update" or "npm install -g @methiu/raigal@latest".`, "cli_version_unsupported");
|
|
1418
|
+
if (claims.iss !== "https://app.raigal.dev") throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
|
|
1419
|
+
if (!isVersionAtLeast("0.2.7", claims.min_cli_version)) throw new JwtVerificationError(`CLI version ${APP_VERSION} is below required minimum ${claims.min_cli_version}. Run "raigal update" or "npm install -g @methiu/raigal@latest".`, "cli_version_unsupported");
|
|
1429
1420
|
const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
1430
1421
|
const grace = options.allowGraceSeconds ?? 0;
|
|
1431
1422
|
if (now > claims.exp + grace) {
|
|
@@ -8942,7 +8933,28 @@ const isSafeRegularProjectFile = (rootDirectory, absolutePath) => {
|
|
|
8942
8933
|
return false;
|
|
8943
8934
|
}
|
|
8944
8935
|
};
|
|
8945
|
-
const
|
|
8936
|
+
const SECRET_CONFIG_EXTENSIONS = [
|
|
8937
|
+
".env",
|
|
8938
|
+
".yaml",
|
|
8939
|
+
".yml",
|
|
8940
|
+
".json",
|
|
8941
|
+
".toml",
|
|
8942
|
+
".ini",
|
|
8943
|
+
".properties",
|
|
8944
|
+
".tfvars"
|
|
8945
|
+
];
|
|
8946
|
+
const LOCKFILE_RE = /(?:^|\/)(?:package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|packages\.lock\.json|composer\.lock|bun\.lock)$/i;
|
|
8947
|
+
const isDotenvFile = (filePath) => {
|
|
8948
|
+
const base = path.basename(filePath).toLowerCase();
|
|
8949
|
+
return base === ".env" || base.startsWith(".env.");
|
|
8950
|
+
};
|
|
8951
|
+
const hasAllowedSourceExtension = (filePath, extraExtensions) => {
|
|
8952
|
+
const extension = path.extname(filePath);
|
|
8953
|
+
if (SOURCE_EXTENSIONS$1.has(extension)) return true;
|
|
8954
|
+
if (LOCKFILE_RE.test(filePath)) return false;
|
|
8955
|
+
if (extraExtensions.has(".env") && isDotenvFile(filePath)) return true;
|
|
8956
|
+
return extraExtensions.has(extension);
|
|
8957
|
+
};
|
|
8946
8958
|
const isInExcludedDirectory = (filePath, excludedDirectories) => {
|
|
8947
8959
|
const normalized = filePath.toLowerCase();
|
|
8948
8960
|
return excludedDirectories.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`) || normalized.includes(`/${directory}/`));
|
|
@@ -21314,13 +21326,19 @@ const SECRET_PATTERNS = [
|
|
|
21314
21326
|
},
|
|
21315
21327
|
{
|
|
21316
21328
|
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
21317
|
-
name: "AWS Access Key"
|
|
21329
|
+
name: "AWS Access Key",
|
|
21330
|
+
specific: true
|
|
21318
21331
|
},
|
|
21319
21332
|
{
|
|
21320
|
-
pattern: /(?:aws[_-]?secret|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
|
|
21333
|
+
pattern: /(?:aws[_-]?secret(?:[_-]?access)?[_-]?key|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
|
|
21321
21334
|
name: "AWS Secret Key",
|
|
21322
21335
|
keywordPrefixed: true
|
|
21323
21336
|
},
|
|
21337
|
+
{
|
|
21338
|
+
pattern: /\b(?:sk|rk)_live_[A-Za-z0-9]{20,}/g,
|
|
21339
|
+
name: "Stripe secret key",
|
|
21340
|
+
specific: true
|
|
21341
|
+
},
|
|
21324
21342
|
{
|
|
21325
21343
|
pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
|
|
21326
21344
|
name: "Hardcoded password/secret",
|
|
@@ -21328,11 +21346,13 @@ const SECRET_PATTERNS = [
|
|
|
21328
21346
|
},
|
|
21329
21347
|
{
|
|
21330
21348
|
pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
|
|
21331
|
-
name: "Private key"
|
|
21349
|
+
name: "Private key",
|
|
21350
|
+
specific: true
|
|
21332
21351
|
},
|
|
21333
21352
|
{
|
|
21334
21353
|
pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
21335
|
-
name: "JWT token"
|
|
21354
|
+
name: "JWT token",
|
|
21355
|
+
specific: true
|
|
21336
21356
|
},
|
|
21337
21357
|
{
|
|
21338
21358
|
pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
|
|
@@ -21341,17 +21361,51 @@ const SECRET_PATTERNS = [
|
|
|
21341
21361
|
},
|
|
21342
21362
|
{
|
|
21343
21363
|
pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
|
|
21344
|
-
name: "GitHub token"
|
|
21364
|
+
name: "GitHub token",
|
|
21365
|
+
specific: true
|
|
21345
21366
|
},
|
|
21346
21367
|
{
|
|
21347
21368
|
pattern: /xox[baprs]-[A-Za-z0-9-]+/g,
|
|
21348
|
-
name: "Slack token"
|
|
21369
|
+
name: "Slack token",
|
|
21370
|
+
specific: true
|
|
21349
21371
|
},
|
|
21350
21372
|
{
|
|
21351
21373
|
pattern: /(?:mongodb(?:\+srv)?|postgres|mysql|redis):\/\/[^:@/"'`\s]+:[^@"'`\s]+@[^"'`\s]+/gi,
|
|
21352
|
-
name: "Database connection string with credentials"
|
|
21374
|
+
name: "Database connection string with credentials",
|
|
21375
|
+
specific: true
|
|
21353
21376
|
}
|
|
21354
21377
|
];
|
|
21378
|
+
const FORMAT_PATTERNS = SECRET_PATTERNS.filter((p) => p.specific);
|
|
21379
|
+
const CONFIG_ASSIGNMENT_RE = /([A-Za-z0-9_.-]*(?:password|passwd|pwd|secret|token|api[_-]?key|apikey)[A-Za-z0-9_.-]*)["']?[ \t]*[:=][ \t]*["']?([^\s"'#,]+)/gi;
|
|
21380
|
+
const CONFIG_REFERENCE_KEY_RE = /(?:name|ref|path|file|arn|id|type|version|url|uri|length|count|env|header|field|prefix|policy|manager|store|provider)$/i;
|
|
21381
|
+
const CONFIG_REFERENCE_VALUE_RE = /^[<{$%]/;
|
|
21382
|
+
const CONFIG_PLACEHOLDER_VALUE_RE = /^(?:your|example|placeholder|redacted|dummy|xxx)/i;
|
|
21383
|
+
const VERSION_VALUE_RE = /^[~^<>=v]*\d+(?:\.\d+)+/;
|
|
21384
|
+
const CONFIG_LITERAL_VALUES = /* @__PURE__ */ new Set([
|
|
21385
|
+
"true",
|
|
21386
|
+
"false",
|
|
21387
|
+
"null",
|
|
21388
|
+
"none",
|
|
21389
|
+
"nil"
|
|
21390
|
+
]);
|
|
21391
|
+
const isConfigFile = (filePath) => isDotenvFile(filePath) || SECRET_CONFIG_EXTENSIONS.includes(path.extname(filePath).toLowerCase());
|
|
21392
|
+
const DOTENV_TEMPLATE_RE = /^\.env\.(?:example|sample|template|dist|defaults)$/i;
|
|
21393
|
+
const isLocalDatabaseUrl = (value) => {
|
|
21394
|
+
try {
|
|
21395
|
+
return LOCAL_DB_HOSTS.has(new URL(value).hostname.toLowerCase());
|
|
21396
|
+
} catch {
|
|
21397
|
+
return false;
|
|
21398
|
+
}
|
|
21399
|
+
};
|
|
21400
|
+
const configAssignmentName = (key) => {
|
|
21401
|
+
const lower = key.toLowerCase();
|
|
21402
|
+
if (/aws[_-]?secret/.test(lower)) return "AWS Secret Key";
|
|
21403
|
+
if (/api[_-]?key|apikey/.test(lower)) return "API key";
|
|
21404
|
+
if (/token/.test(lower) && !/password|passwd|pwd|secret/.test(lower)) return "Authentication token";
|
|
21405
|
+
return "Hardcoded password/secret";
|
|
21406
|
+
};
|
|
21407
|
+
const URL_VALUE_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
21408
|
+
const isConfigValueIgnorable = (key, value) => value.length < 8 || URL_VALUE_RE.test(value) || CONFIG_REFERENCE_KEY_RE.test(key) || CONFIG_REFERENCE_VALUE_RE.test(value) || CONFIG_PLACEHOLDER_VALUE_RE.test(value) || VERSION_VALUE_RE.test(value) || CONFIG_LITERAL_VALUES.has(value.toLowerCase()) || isNonProductionCredentialValue(value);
|
|
21355
21409
|
const isInsideStringLiteral = (content, matchIndex) => {
|
|
21356
21410
|
const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
|
|
21357
21411
|
const prefix = content.slice(lineStart, matchIndex);
|
|
@@ -21496,16 +21550,69 @@ const shouldSkipSecretFinding = (relativePath, name, lineText, matchedText) => {
|
|
|
21496
21550
|
if ((name === "Hardcoded password/secret" || name === "Authentication token") && isSymbolicConstantValue(matchedText)) return true;
|
|
21497
21551
|
return false;
|
|
21498
21552
|
};
|
|
21553
|
+
const collectSourceCandidates = (content, relativePath) => {
|
|
21554
|
+
const candidates = [];
|
|
21555
|
+
for (const { pattern, name, keywordPrefixed, specific } of SECRET_PATTERNS) for (const match of content.matchAll(new RegExp(pattern.source, pattern.flags))) {
|
|
21556
|
+
const matchedText = match[1] ?? match[0];
|
|
21557
|
+
const lineText = lineForMatch(content, match.index);
|
|
21558
|
+
if (isPlaceholderValue(matchedText)) continue;
|
|
21559
|
+
if (shouldSkipSecretFinding(relativePath, name, lineText, matchedText)) continue;
|
|
21560
|
+
if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
|
|
21561
|
+
candidates.push({
|
|
21562
|
+
name,
|
|
21563
|
+
start: match.index,
|
|
21564
|
+
end: match.index + match[0].length,
|
|
21565
|
+
specific: Boolean(specific)
|
|
21566
|
+
});
|
|
21567
|
+
}
|
|
21568
|
+
return candidates;
|
|
21569
|
+
};
|
|
21570
|
+
const collectConfigCandidates = (content, relativePath) => {
|
|
21571
|
+
const candidates = [];
|
|
21572
|
+
const template = DOTENV_TEMPLATE_RE.test(path.posix.basename(relativePath));
|
|
21573
|
+
for (const { pattern, name } of FORMAT_PATTERNS) for (const match of content.matchAll(new RegExp(pattern.source, pattern.flags))) {
|
|
21574
|
+
const lineText = lineForMatch(content, match.index);
|
|
21575
|
+
if (isPlaceholderValue(match[0])) continue;
|
|
21576
|
+
if (template && isLocalDatabaseUrl(match[0])) continue;
|
|
21577
|
+
if (shouldSkipSecretFinding(relativePath, name, lineText, match[0])) continue;
|
|
21578
|
+
candidates.push({
|
|
21579
|
+
name,
|
|
21580
|
+
start: match.index,
|
|
21581
|
+
end: match.index + match[0].length,
|
|
21582
|
+
specific: true
|
|
21583
|
+
});
|
|
21584
|
+
}
|
|
21585
|
+
for (const match of content.matchAll(new RegExp(CONFIG_ASSIGNMENT_RE.source, CONFIG_ASSIGNMENT_RE.flags))) {
|
|
21586
|
+
const [, key, value] = match;
|
|
21587
|
+
const name = configAssignmentName(key);
|
|
21588
|
+
const lineText = lineForMatch(content, match.index);
|
|
21589
|
+
if (isConfigValueIgnorable(key, value) || isPlaceholderValue(value)) continue;
|
|
21590
|
+
if (shouldSkipSecretFinding(relativePath, name, lineText, value)) continue;
|
|
21591
|
+
candidates.push({
|
|
21592
|
+
name,
|
|
21593
|
+
start: match.index,
|
|
21594
|
+
end: match.index + match[0].length,
|
|
21595
|
+
specific: false
|
|
21596
|
+
});
|
|
21597
|
+
}
|
|
21598
|
+
return candidates;
|
|
21599
|
+
};
|
|
21600
|
+
const dedupeCandidates = (candidates) => {
|
|
21601
|
+
const ordered = [...candidates].sort((a, b) => Number(b.specific) - Number(a.specific) || a.start - b.start);
|
|
21602
|
+
const kept = [];
|
|
21603
|
+
for (const candidate of ordered) {
|
|
21604
|
+
if (kept.some((k) => candidate.start < k.end && k.start < candidate.end)) continue;
|
|
21605
|
+
kept.push(candidate);
|
|
21606
|
+
}
|
|
21607
|
+
return kept.sort((a, b) => a.start - b.start);
|
|
21608
|
+
};
|
|
21609
|
+
const resolveSecretFiles = (context) => {
|
|
21610
|
+
const extras = [...SECRET_CONFIG_EXTENSIONS];
|
|
21611
|
+
return context.secretFiles ? filterExplicitFiles(context.rootDirectory, context.secretFiles, extras) : getSourceFilesWithExtras(context, extras);
|
|
21612
|
+
};
|
|
21499
21613
|
const scanSecrets = async (context) => {
|
|
21500
|
-
const files = getSourceFilesWithExtras(context, [
|
|
21501
|
-
".env",
|
|
21502
|
-
".yaml",
|
|
21503
|
-
".yml",
|
|
21504
|
-
".json",
|
|
21505
|
-
".toml"
|
|
21506
|
-
]);
|
|
21507
21614
|
const diagnostics = [];
|
|
21508
|
-
for (const filePath of
|
|
21615
|
+
for (const filePath of resolveSecretFiles(context)) {
|
|
21509
21616
|
let content;
|
|
21510
21617
|
try {
|
|
21511
21618
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -21514,30 +21621,20 @@ const scanSecrets = async (context) => {
|
|
|
21514
21621
|
}
|
|
21515
21622
|
content = maskComments(content, path.extname(filePath));
|
|
21516
21623
|
const relativePath = relativePosix(context.rootDirectory, filePath);
|
|
21517
|
-
|
|
21518
|
-
|
|
21519
|
-
|
|
21520
|
-
|
|
21521
|
-
|
|
21522
|
-
|
|
21523
|
-
|
|
21524
|
-
|
|
21525
|
-
|
|
21526
|
-
|
|
21527
|
-
|
|
21528
|
-
|
|
21529
|
-
|
|
21530
|
-
|
|
21531
|
-
message: `Possible ${name} detected in source code`,
|
|
21532
|
-
help: "Move secrets to environment variables or a secrets manager",
|
|
21533
|
-
line,
|
|
21534
|
-
column: 0,
|
|
21535
|
-
category: "Security",
|
|
21536
|
-
fixable: false,
|
|
21537
|
-
redactSource: true
|
|
21538
|
-
});
|
|
21539
|
-
}
|
|
21540
|
-
}
|
|
21624
|
+
const candidates = isConfigFile(filePath) ? collectConfigCandidates(content, relativePath) : collectSourceCandidates(content, relativePath);
|
|
21625
|
+
for (const { name, start } of dedupeCandidates(candidates)) diagnostics.push({
|
|
21626
|
+
filePath: relativePath,
|
|
21627
|
+
engine: "security",
|
|
21628
|
+
rule: "security/hardcoded-secret",
|
|
21629
|
+
severity: "error",
|
|
21630
|
+
message: `Possible ${name} detected in source code`,
|
|
21631
|
+
help: "Move secrets to environment variables or a secrets manager",
|
|
21632
|
+
line: content.slice(0, start).split("\n").length,
|
|
21633
|
+
column: 0,
|
|
21634
|
+
category: "Security",
|
|
21635
|
+
fixable: false,
|
|
21636
|
+
redactSource: true
|
|
21637
|
+
});
|
|
21541
21638
|
}
|
|
21542
21639
|
return diagnostics;
|
|
21543
21640
|
};
|
|
@@ -25231,7 +25328,7 @@ const maybeNotifyUpdate = async (now = Date.now()) => {
|
|
|
25231
25328
|
if (!process.stderr.isTTY) return;
|
|
25232
25329
|
const cachePath = resolveUpdateCachePath();
|
|
25233
25330
|
const cache = readCache(cachePath);
|
|
25234
|
-
if (cache && isOutdated("0.2.
|
|
25331
|
+
if (cache && isOutdated("0.2.7", cache.latest)) process.stderr.write(formatUpdateNotice(APP_VERSION, cache.latest));
|
|
25235
25332
|
if (!cache || now - cache.checkedAt > CHECK_INTERVAL_MS) {
|
|
25236
25333
|
const latest = await fetchLatestVersion$1();
|
|
25237
25334
|
if (latest) writeCache(cachePath, {
|
|
@@ -25756,7 +25853,7 @@ const renderCommandGroup = (group, commandWidth) => {
|
|
|
25756
25853
|
return lines;
|
|
25757
25854
|
};
|
|
25758
25855
|
const renderCommandReference = (input = {}) => {
|
|
25759
|
-
const version = input.version ?? "0.2.
|
|
25856
|
+
const version = input.version ?? "0.2.7";
|
|
25760
25857
|
const commandWidth = Math.max(...COMMAND_REFERENCE.map((item) => item.command.length));
|
|
25761
25858
|
const lines = [renderHeader({
|
|
25762
25859
|
version,
|
|
@@ -25979,7 +26076,7 @@ const renderHelpDetails = () => [
|
|
|
25979
26076
|
].map((line) => highlightRaigal(line, theme)).join("\n");
|
|
25980
26077
|
const renderHome = (input = {}) => {
|
|
25981
26078
|
let out = renderHeader({
|
|
25982
|
-
version: input.version ?? "0.2.
|
|
26079
|
+
version: input.version ?? "0.2.7",
|
|
25983
26080
|
command: "--bare",
|
|
25984
26081
|
context: []
|
|
25985
26082
|
});
|
|
@@ -26726,22 +26823,10 @@ const getChangedLineMap = (cwd, base) => {
|
|
|
26726
26823
|
return map;
|
|
26727
26824
|
};
|
|
26728
26825
|
//#endregion
|
|
26729
|
-
//#region src/commands/scan-hook-nudge.ts
|
|
26730
|
-
const shouldShowHookNudge = (ctx) => ctx.isTty && !ctx.isCi && ctx.installedAgentCount === 0;
|
|
26731
|
-
const buildHookNudge = (ctx) => {
|
|
26732
|
-
if (!shouldShowHookNudge(ctx)) return null;
|
|
26733
|
-
return [
|
|
26734
|
-
"",
|
|
26735
|
-
"Next: install the per-edit hook so your coding agent fixes slop before it lands.",
|
|
26736
|
-
ctx.invocation === "raigal" ? " raigal hook install" : " npm install -g @methiu/raigal && raigal hook install",
|
|
26737
|
-
""
|
|
26738
|
-
].join("\n");
|
|
26739
|
-
};
|
|
26740
|
-
//#endregion
|
|
26741
26826
|
//#region src/commands/scan-coverage.ts
|
|
26742
26827
|
const coverageReason = (c) => {
|
|
26743
26828
|
if (c.supportedFiles === 0 && c.dominantUnsupported) return `This repository is ${c.dominantUnsupported} (${c.unsupportedFiles} files), which Raigal does not analyze. No score — it would not reflect this code.`;
|
|
26744
|
-
if (c.supportedFiles === 0) return "No files in a language Raigal analyzes (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP,
|
|
26829
|
+
if (c.supportedFiles === 0) return "No files in a language Raigal analyzes (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, C#, C/C++). Nothing to score.";
|
|
26745
26830
|
const lang = c.dominantUnsupported ?? "an unsupported language";
|
|
26746
26831
|
const files = `${c.supportedFiles} supported file${c.supportedFiles === 1 ? "" : "s"}`;
|
|
26747
26832
|
return `This repository is mostly ${lang} (${c.unsupportedFiles} files); Raigal analyzed only ${files}. Score withheld — it would represent a sliver of the codebase.`;
|
|
@@ -26980,10 +27065,23 @@ const collectScanFileScope = (request) => {
|
|
|
26980
27065
|
projectFiles: [.../* @__PURE__ */ new Set([...projectSourceFiles, ...declarations])],
|
|
26981
27066
|
scoreFileCount: projectSourceFiles.length + projectTestFiles.length,
|
|
26982
27067
|
scopeLabel: candidates.label,
|
|
27068
|
+
secretFiles: filterEnumeratedProjectFiles(request.rootDirectory, candidates.files, [...SECRET_CONFIG_EXTENSIONS], request.excludePatterns, request.includePatterns),
|
|
26983
27069
|
testFiles
|
|
26984
27070
|
};
|
|
26985
27071
|
};
|
|
26986
27072
|
//#endregion
|
|
27073
|
+
//#region src/commands/scan-hook-nudge.ts
|
|
27074
|
+
const shouldShowHookNudge = (ctx) => ctx.isTty && !ctx.isCi && ctx.installedAgentCount === 0;
|
|
27075
|
+
const buildHookNudge = (ctx) => {
|
|
27076
|
+
if (!shouldShowHookNudge(ctx)) return null;
|
|
27077
|
+
return [
|
|
27078
|
+
"",
|
|
27079
|
+
"Next: install the per-edit hook so your coding agent fixes slop before it lands.",
|
|
27080
|
+
ctx.invocation === "raigal" ? " raigal hook install" : " npm install -g @methiu/raigal && raigal hook install",
|
|
27081
|
+
""
|
|
27082
|
+
].join("\n");
|
|
27083
|
+
};
|
|
27084
|
+
//#endregion
|
|
26987
27085
|
//#region src/commands/scan-options.ts
|
|
26988
27086
|
const isMachineOutput = (options) => Boolean(options.json) || Boolean(options.sarif);
|
|
26989
27087
|
const isHistoryComparableScan = (options) => !options.staged && !options.changes && options.command !== "ci";
|
|
@@ -27428,7 +27526,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
27428
27526
|
const projectName = projectInfo.projectName ?? "project";
|
|
27429
27527
|
const language = projectInfo.languages[0] ?? "unknown";
|
|
27430
27528
|
const printedHumanHeader = !machineOutput && showHeader;
|
|
27431
|
-
const { dependencyAuditFiles, dependencyAuditScope, files, projectFiles, scoreFileCount, scopeLabel, testFiles } = scanScope;
|
|
27529
|
+
const { dependencyAuditFiles, dependencyAuditScope, files, projectFiles, scoreFileCount, scopeLabel, secretFiles, testFiles } = scanScope;
|
|
27432
27530
|
const excludePatterns = [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)];
|
|
27433
27531
|
const scanCoverage = deriveScanCoverage(projectInfo.coverage, scoreFileCount);
|
|
27434
27532
|
const reportProjectInfo = {
|
|
@@ -27469,6 +27567,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
27469
27567
|
excludePatterns,
|
|
27470
27568
|
testFiles,
|
|
27471
27569
|
projectFiles,
|
|
27570
|
+
secretFiles,
|
|
27472
27571
|
installedTools: projectInfo.installedTools,
|
|
27473
27572
|
config: engineConfig
|
|
27474
27573
|
}, config.engines, machineOutput)).map((result) => ({
|