@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 CHANGED
@@ -1 +1,6 @@
1
- PROPRIETARY. All rights reserved. Use is permitted only under a written agreement with Raigal.
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
  [![npm version](https://img.shields.io/npm/v/@methiu/raigal.svg)](https://www.npmjs.com/package/@methiu/raigal) [![npm downloads](https://img.shields.io/npm/dm/@methiu/raigal.svg)](https://www.npmjs.com/package/@methiu/raigal) [![CI](https://github.com/meethiu/raigal/actions/workflows/ci.yml/badge.svg)](https://github.com/meethiu/raigal/actions/workflows/ci.yml) [![Node >= 20](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](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. Sub-second. Deterministic: no LLM in the runtime path, same code in, same score out.
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 demo software licensed to organizations on an admin-managed allowlist. Runs require authentication via organization credentials (`raigal login` or `RAIGAL_TOKEN`).
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
- Also available on npm, Yarn, Bun, and Homebrew:
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 same CLI is published to npm and Homebrew. Pick whichever fits your stack.
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-mcp"]
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 # enterprise-grade gate: all engines, typecheck, failBelow 85
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
- - **Deterministic**: Regex + AST + standard tooling. No LLMs, no API calls. Same code in, same score out.
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.6";
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(process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev"),
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
- const expectedIssuer = process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev";
1427
- if (claims.iss !== expectedIssuer) throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
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 hasAllowedSourceExtension = (filePath, extraExtensions) => SOURCE_EXTENSIONS$1.has(path.extname(filePath)) || extraExtensions.has(path.extname(filePath));
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 files) {
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
- for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
21518
- const regex = new RegExp(pattern.source, pattern.flags);
21519
- for (const match of content.matchAll(regex)) {
21520
- const matchedText = match[1] ?? match[0];
21521
- const lineText = lineForMatch(content, match.index);
21522
- if (isPlaceholderValue(matchedText)) continue;
21523
- if (shouldSkipSecretFinding(relativePath, name, lineText, matchedText)) continue;
21524
- if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
21525
- const line = content.slice(0, match.index).split("\n").length;
21526
- diagnostics.push({
21527
- filePath: relativePath,
21528
- engine: "security",
21529
- rule: "security/hardcoded-secret",
21530
- severity: "error",
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.6", cache.latest)) process.stderr.write(formatUpdateNotice(APP_VERSION, cache.latest));
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.6";
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.6",
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, Java). Nothing to score.";
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) => ({