@noctcore/lint-meta-rules 0.6.0 → 0.6.1
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 +83 -73
- package/docs/rules/agents-doc-presence.md +13 -4
- package/docs/rules/canonical-helpers-single-home.md +13 -3
- package/docs/rules/dockerfile-base-image-digest-pin.md +17 -4
- package/docs/rules/eslint-config-no-warn.md +22 -7
- package/docs/rules/file-size-ratchet.md +19 -9
- package/docs/rules/github-actions-least-privilege-permissions.md +20 -11
- package/docs/rules/github-actions-no-template-injection.md +21 -12
- package/docs/rules/github-actions-runner-pinned.md +19 -6
- package/docs/rules/github-actions-sha-pinned.md +19 -5
- package/docs/rules/idempotency-key-parity.md +13 -5
- package/docs/rules/layer-rank.md +17 -6
- package/docs/rules/no-cloned-component-folders.md +13 -4
- package/docs/rules/no-warn-severity.md +14 -3
- package/docs/rules/package-shape.md +13 -6
- package/docs/rules/prisma-method-surface.md +19 -4
- package/docs/rules/security-scanner-version-parity.md +16 -7
- package/docs/rules/service-image-digest-pin.md +21 -7
- package/docs/rules/session-epoch-captured.md +15 -9
- package/docs/rules/session-kind-stamped.md +15 -7
- package/docs/rules/session-landing-declared.md +13 -5
- package/docs/rules/session-mint-callers.md +11 -3
- package/docs/rules/tenant-model-registry-parity.md +18 -4
- package/docs/rules/test-runner-segregation.md +15 -5
- package/docs/rules/test-sibling-enforcement.md +12 -5
- package/docs/rules/test-workspace-enrollment.md +14 -6
- package/docs/rules/translation-dead-keys.md +33 -20
- package/docs/rules/ui-primitive-shape.md +13 -4
- package/docs/rules/workspace-graph-parity.md +15 -5
- package/package.json +4 -4
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> Workflow jobs run on a named runner image, never a `*-latest` label.
|
|
4
4
|
|
|
5
|
+
<!-- begin generated rule header -->
|
|
6
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createGithubActionsRunnerPinnedRule` from `@noctcore/lint-meta-rules` · Category `ci` · Fails CI by default: yes
|
|
7
|
+
<!-- end generated rule header -->
|
|
8
|
+
|
|
5
9
|
## Why
|
|
6
10
|
|
|
7
11
|
GitHub repoints `ubuntu-latest` (and `macos-latest`, `windows-latest`) to a new OS image on its own
|
|
@@ -11,8 +15,7 @@ repo. Pinning a named image makes the move a reviewed diff.
|
|
|
11
15
|
## What it flags
|
|
12
16
|
|
|
13
17
|
Every floating label in a `runs-on:` value, read as a scalar, a flow list (`[a, b]`), a block list, or
|
|
14
|
-
a `group:` / `labels:` mapping.
|
|
15
|
-
is left alone; a commented-out `runs-on:` is ignored.
|
|
18
|
+
a `group:` / `labels:` mapping.
|
|
16
19
|
|
|
17
20
|
```yaml
|
|
18
21
|
# Bad
|
|
@@ -24,7 +27,17 @@ runs-on: ubuntu-24.04
|
|
|
24
27
|
runs-on: ${{ matrix.os }}
|
|
25
28
|
```
|
|
26
29
|
|
|
27
|
-
##
|
|
30
|
+
## What it does not flag
|
|
31
|
+
|
|
32
|
+
- A named image (`ubuntu-24.04`), quoted or with a trailing comment, and a custom label such as
|
|
33
|
+
`self-hosted`.
|
|
34
|
+
- An expression (`${{ matrix.os }}`): it cannot be judged from the text.
|
|
35
|
+
- A commented-out `runs-on:`, and a later key in the job (`name: ubuntu-latest`) after the label list.
|
|
36
|
+
|
|
37
|
+
Line-based text, not a YAML parse. A matrix value such as `os: [ubuntu-latest]` that feeds
|
|
38
|
+
`runs-on: ${{ matrix.os }}` is not checked.
|
|
39
|
+
|
|
40
|
+
## Options
|
|
28
41
|
|
|
29
42
|
```ts
|
|
30
43
|
createGithubActionsRunnerPinnedRule(options?: GithubActionsRunnerPinnedOptions): IMetaRule
|
|
@@ -36,7 +49,7 @@ createGithubActionsRunnerPinnedRule(options?: GithubActionsRunnerPinnedOptions):
|
|
|
36
49
|
| `floatingLabel` | `RegExp` | `/^[\w.-]+-latest$/u` | A label matching this is floating. Do not pass a `g`-flagged regex. |
|
|
37
50
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
38
51
|
|
|
39
|
-
##
|
|
52
|
+
## When not to use it
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
If you want your CI to track GitHub's newest runner images automatically and accept the unreviewed
|
|
55
|
+
moves, skip it.
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> GitHub Actions `uses:` refs are pinned to a 40-character commit SHA with a `# vN` comment.
|
|
4
4
|
|
|
5
|
+
<!-- begin generated rule header -->
|
|
6
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createGithubActionsShaPinnedRule` from `@noctcore/lint-meta-rules` · Category `ci` · Fails CI by default: yes
|
|
7
|
+
<!-- end generated rule header -->
|
|
8
|
+
|
|
5
9
|
## Why
|
|
6
10
|
|
|
7
11
|
A tag or branch ref is a moving target: whoever controls the action's repository can repoint it, and
|
|
@@ -17,7 +21,7 @@ Every `uses:` line (step-level and job-level reusable workflow calls) in the sca
|
|
|
17
21
|
- a SHA-pinned ref with no `# vN` comment,
|
|
18
22
|
- a `docker://` ref with no `@sha256:<digest>`.
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
Each violation carries the 1-indexed line.
|
|
21
25
|
|
|
22
26
|
```yaml
|
|
23
27
|
# Bad
|
|
@@ -29,7 +33,17 @@ Local actions (`uses: ./path`) are exempt. Each violation carries the 1-indexed
|
|
|
29
33
|
- uses: ./.github/actions/setup
|
|
30
34
|
```
|
|
31
35
|
|
|
32
|
-
##
|
|
36
|
+
## What it does not flag
|
|
37
|
+
|
|
38
|
+
- Local actions (`uses: ./path`).
|
|
39
|
+
- A full 40-character SHA with a `# vN` comment, quoted or not, including a path inside the action
|
|
40
|
+
repo (`github/codeql-action/analyze@<sha> # v3`).
|
|
41
|
+
- A `docker://` ref pinned by `@sha256:<digest>`.
|
|
42
|
+
|
|
43
|
+
The check is line-based text, not a YAML parse. A `uses:` value split across lines, or built from an
|
|
44
|
+
expression, is not seen.
|
|
45
|
+
|
|
46
|
+
## Options
|
|
33
47
|
|
|
34
48
|
```ts
|
|
35
49
|
createGithubActionsShaPinnedRule(options?: GithubActionsShaPinnedOptions): IMetaRule
|
|
@@ -40,7 +54,7 @@ createGithubActionsShaPinnedRule(options?: GithubActionsShaPinnedOptions): IMeta
|
|
|
40
54
|
| `workflowGlobs` | `string[]` | `['.github/workflows/*.yml', '.github/workflows/*.yaml']` | Workflow files to scan. |
|
|
41
55
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
42
56
|
|
|
43
|
-
##
|
|
57
|
+
## When not to use it
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
If your repo has no GitHub Actions workflows, or you accept tag refs and review action updates some
|
|
60
|
+
other way, skip it.
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
> A procedure guarded by an idempotency middleware has a client caller that sends the key, or no
|
|
4
4
|
> client caller at all.
|
|
5
5
|
|
|
6
|
+
<!-- begin generated rule header -->
|
|
7
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createIdempotencyKeyParityRule` from `@noctcore/lint-meta-rules/trpc` · Category `source-text` · Fails CI by default: yes
|
|
8
|
+
<!-- end generated rule header -->
|
|
9
|
+
|
|
6
10
|
Import it from the `trpc` entry point:
|
|
7
11
|
|
|
8
12
|
```ts
|
|
@@ -14,8 +18,8 @@ import { createIdempotencyKeyParityRule } from '@noctcore/lint-meta-rules/trpc';
|
|
|
14
18
|
An idempotency middleware only de-duplicates when the client sends a key, and neither half fails when
|
|
15
19
|
the other is missing. The server passes the request straight through, the client gets a normal
|
|
16
20
|
response, and both test suites pass. A guard nobody sends a key to is decoration advertising
|
|
17
|
-
double-submit protection that does not exist.
|
|
18
|
-
|
|
21
|
+
double-submit protection that does not exist. One audit of a production API found 14 of 30 guarded
|
|
22
|
+
procedures whose only web caller had never sent a key.
|
|
19
23
|
|
|
20
24
|
The gap is only visible by holding the two lists side by side, which is what this rule does.
|
|
21
25
|
|
|
@@ -30,7 +34,7 @@ reported.
|
|
|
30
34
|
The method is read from the decorator to the next `async <name>(`, the shape `nestjs-trpc` routers
|
|
31
35
|
take.
|
|
32
36
|
|
|
33
|
-
## What it
|
|
37
|
+
## What it does not flag
|
|
34
38
|
|
|
35
39
|
- A guarded procedure with no client caller: the guard is correct in advance of the screen that will
|
|
36
40
|
use it, and demanding a caller would be demanding the screen.
|
|
@@ -44,7 +48,7 @@ file, if `clientGlobs` match it: the check is per procedure, across the client t
|
|
|
44
48
|
|
|
45
49
|
With no `middleware` the rule is inert.
|
|
46
50
|
|
|
47
|
-
##
|
|
51
|
+
## Options
|
|
48
52
|
|
|
49
53
|
```ts
|
|
50
54
|
createIdempotencyKeyParityRule(options?: IdempotencyKeyParityOptions): IMetaRule
|
|
@@ -66,7 +70,11 @@ createIdempotencyKeyParityRule(options?: IdempotencyKeyParityOptions): IMetaRule
|
|
|
66
70
|
| `hint` | `string` | none | Appended to every message: how this project threads the key. |
|
|
67
71
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
### Worked example: an app with an idempotency middleware
|
|
74
|
+
|
|
75
|
+
A NestJS API guards its mutations with an `IdempotencyMiddleware` in `*.router.ts` files, and a web app
|
|
76
|
+
calls them through tRPC hooks. The `hint` tells whoever hits a violation how the web app threads the
|
|
77
|
+
key:
|
|
70
78
|
|
|
71
79
|
```ts
|
|
72
80
|
createIdempotencyKeyParityRule({
|
package/docs/rules/layer-rank.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
> A module may import only strictly-lower-ranked `<scope>` packages — equal (sideways) or higher
|
|
4
4
|
> (upward) is forbidden.
|
|
5
5
|
|
|
6
|
+
<!-- begin generated rule header -->
|
|
7
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createLayerRankRule` from `@noctcore/lint-meta-rules` · Category `source-text` · Fails CI by default: yes
|
|
8
|
+
<!-- end generated rule header -->
|
|
9
|
+
|
|
6
10
|
## Why
|
|
7
11
|
|
|
8
12
|
Layered architectures encode a fixed dependency direction (e.g. `contracts → shared → storage/skills
|
|
@@ -15,11 +19,19 @@ are caught before they calcify.
|
|
|
15
19
|
Each source file is assigned an **importer rank**: surface files (matched by `surfacePrefix`) get
|
|
16
20
|
`surfaceRank`; otherwise the `packageDirPattern` capture is looked up in `ranks`. For each
|
|
17
21
|
`<scope>/<pkg>` import whose target is also ranked, the rule flags it when the target rank is `>=` the
|
|
18
|
-
importer rank (sideways when equal, upward when greater).
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
importer rank (sideways when equal, upward when greater).
|
|
23
|
+
|
|
24
|
+
## What it does not flag
|
|
25
|
+
|
|
26
|
+
- A downward import: the target's rank is strictly lower than the importer's.
|
|
27
|
+
- Unranked importers and unranked targets, so a package outside the documented spine never produces a
|
|
28
|
+
false positive. Surface files are unranked unless `surfaceRank` is set.
|
|
29
|
+
- Test files (`.test.ts`, `.test.tsx`).
|
|
30
|
+
- Imports without a `from` clause (`import '<scope>/x'`, `import()`, `require()`): only
|
|
31
|
+
`from '<scope>/<pkg>'` is read.
|
|
32
|
+
- Anything at all while `ranks` is empty.
|
|
21
33
|
|
|
22
|
-
##
|
|
34
|
+
## Options
|
|
23
35
|
|
|
24
36
|
```ts
|
|
25
37
|
createLayerRankRule(options?: LayerRankOptions): IMetaRule
|
|
@@ -35,8 +47,7 @@ createLayerRankRule(options?: LayerRankOptions): IMetaRule
|
|
|
35
47
|
| `sourceGlobs` | `string[]` | packages + apps `src/**` `.ts`/`.tsx` | Source files to scan. |
|
|
36
48
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
defaults to `{}` so the rule is inert until a consumer supplies its own layering.
|
|
50
|
+
`ranks` defaults to `{}`, so the rule is inert until you supply your own layering.
|
|
40
51
|
|
|
41
52
|
## When not to use it
|
|
42
53
|
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> A component folder name may exist under only one feature.
|
|
4
4
|
|
|
5
|
+
<!-- begin generated rule header -->
|
|
6
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createNoClonedComponentFoldersRule` from `@noctcore/lint-meta-rules` · Category `source-text` · Fails CI by default: yes
|
|
7
|
+
<!-- end generated rule header -->
|
|
8
|
+
|
|
5
9
|
## Why
|
|
6
10
|
|
|
7
11
|
Same-named component folders across features are how sibling drift starts: a component cloned into a
|
|
@@ -16,7 +20,15 @@ A `<feature>/<Name>/<barrelFile>` path marks a component folder. Grouping by `<N
|
|
|
16
20
|
`allowedClones` — is flagged. The allowlist freezes today's clone groups and only shrinks: an
|
|
17
21
|
`allowedClones` entry whose clone group no longer exists is itself flagged as stale.
|
|
18
22
|
|
|
19
|
-
##
|
|
23
|
+
## What it does not flag
|
|
24
|
+
|
|
25
|
+
- A component folder name that exists under only one feature.
|
|
26
|
+
- Folders under `excludedFeatures` (`ui` and `app` by default).
|
|
27
|
+
- Names in `allowedClones` while their clone group still exists.
|
|
28
|
+
- Folders without the `barrelFile`, and folders nested deeper than `<feature>/<Name>/` under
|
|
29
|
+
`componentsRoot`.
|
|
30
|
+
|
|
31
|
+
## Options
|
|
20
32
|
|
|
21
33
|
```ts
|
|
22
34
|
createNoClonedComponentFoldersRule(options?: NoClonedComponentFoldersOptions): IMetaRule
|
|
@@ -31,9 +43,6 @@ createNoClonedComponentFoldersRule(options?: NoClonedComponentFoldersOptions): I
|
|
|
31
43
|
| `sharedDest` | `string` | `'components/ui'` | Where a shared surface should be hoisted (used in the message). |
|
|
32
44
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
33
45
|
|
|
34
|
-
De-projected from nightcore, which hardcoded `apps/web/src/components`, the `ui`/`app` excluded
|
|
35
|
-
features, and a fixed `ALLOWED_CLONES` set.
|
|
36
|
-
|
|
37
46
|
## When not to use it
|
|
38
47
|
|
|
39
48
|
If your components are not organized as `<feature>/<Component>/` folders under one root, or you
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> ESLint severity is `error` or `off`, never `warn`.
|
|
4
4
|
|
|
5
|
+
<!-- begin generated rule header -->
|
|
6
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createNoWarnSeverityRule` from `@noctcore/lint-meta-rules` · Category `config` · Fails CI by default: yes
|
|
7
|
+
<!-- end generated rule header -->
|
|
8
|
+
|
|
5
9
|
## Why
|
|
6
10
|
|
|
7
11
|
Agents iterate by reading CI failures. A `warn` severity is a silent miss — it neither fails the
|
|
@@ -14,7 +18,16 @@ Reads each configured flat-config file (skipping any that do not exist), strips
|
|
|
14
18
|
reports any line containing a `'warn'` / `"warn"` severity literal. The violation carries the
|
|
15
19
|
1-indexed line number.
|
|
16
20
|
|
|
17
|
-
##
|
|
21
|
+
## What it does not flag
|
|
22
|
+
|
|
23
|
+
- A `'warn'` inside a `//` line comment.
|
|
24
|
+
- The numeric severity `1`: only the quoted `'warn'` / `"warn"` literal is matched.
|
|
25
|
+
- A `warn` a spread preset injects, since no file the project owns spells it. Use
|
|
26
|
+
[`eslint-config-no-warn`](./eslint-config-no-warn.md) for the resolved config.
|
|
27
|
+
- Config files not listed in `configFiles` (the defaults are the root flat configs), and listed ones that
|
|
28
|
+
do not exist.
|
|
29
|
+
|
|
30
|
+
## Options
|
|
18
31
|
|
|
19
32
|
```ts
|
|
20
33
|
createNoWarnSeverityRule(options?: NoWarnSeverityOptions): IMetaRule
|
|
@@ -25,8 +38,6 @@ createNoWarnSeverityRule(options?: NoWarnSeverityOptions): IMetaRule
|
|
|
25
38
|
| `configFiles` | `string[]` | `['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs']` | Flat-config files to scan; each is read only if present. |
|
|
26
39
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
27
40
|
|
|
28
|
-
De-projected from nightcore, which hardcoded `eslint.config.mjs`.
|
|
29
|
-
|
|
30
41
|
## When not to use it
|
|
31
42
|
|
|
32
43
|
If your project deliberately uses `warn` as an in-editor nudge that is not meant to gate CI, this rule
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
> Every workspace is named `<scope>/<dir>`; library packages expose a barrel and point their build
|
|
4
4
|
> fields at the built output.
|
|
5
5
|
|
|
6
|
+
<!-- begin generated rule header -->
|
|
7
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createPackageShapeRule` from `@noctcore/lint-meta-rules` · Category `config` · Fails CI by default: yes
|
|
8
|
+
<!-- end generated rule header -->
|
|
9
|
+
|
|
6
10
|
## Why
|
|
7
11
|
|
|
8
12
|
A monorepo stays navigable when a package's identity is mechanical: its npm name matches its folder,
|
|
@@ -20,10 +24,16 @@ For each `package.json` matched by `libraryGlobs` (full checks) or `appGlobs` (n
|
|
|
20
24
|
`distMarker`, and `exports` must reference it.
|
|
21
25
|
- **invalid JSON** — reported as a violation.
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
## What it does not flag
|
|
28
|
+
|
|
29
|
+
- Barrel and build-field problems in app/surface packages: they are deployable entrypoints
|
|
30
|
+
(vite/tauri/bun), so only the name check applies to them.
|
|
31
|
+
- A package whose directory has an `externalNames` entry and whose name equals it.
|
|
32
|
+
- A missing `distFields` entry or a missing `exports`: only fields that are present are checked, and
|
|
33
|
+
`exports` passes when any of its targets contains `distMarker`.
|
|
34
|
+
- `package.json` files outside `libraryGlobs` and `appGlobs`.
|
|
25
35
|
|
|
26
|
-
##
|
|
36
|
+
## Options
|
|
27
37
|
|
|
28
38
|
```ts
|
|
29
39
|
createPackageShapeRule(options?: PackageShapeOptions): IMetaRule
|
|
@@ -40,9 +50,6 @@ createPackageShapeRule(options?: PackageShapeOptions): IMetaRule
|
|
|
40
50
|
| `distFields` | `string[]` | `['main','module','types']` | `package.json` string fields that must point at built output. |
|
|
41
51
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
42
52
|
|
|
43
|
-
De-projected from nightcore, which hardcoded the `@nightcore` scope, the `packages/*` vs `apps/*`
|
|
44
|
-
split, the `src/index.ts` barrel and the `dist/` marker.
|
|
45
|
-
|
|
46
53
|
## When not to use it
|
|
47
54
|
|
|
48
55
|
If your packages are not scope-named after their folders, or libraries publish raw source (no build
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
> The Prisma reads and writes your rules police partition the generated client's `<Model>Delegate`
|
|
4
4
|
> method surface exactly, so a Prisma upgrade cannot add an unguarded method.
|
|
5
5
|
|
|
6
|
+
<!-- begin generated rule header -->
|
|
7
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createPrismaMethodSurfaceRule` from `@noctcore/lint-meta-rules/prisma` · Category `config` · Fails CI by default: yes
|
|
8
|
+
<!-- end generated rule header -->
|
|
9
|
+
|
|
6
10
|
Import it from the `prisma` entry point:
|
|
7
11
|
|
|
8
12
|
```ts
|
|
@@ -30,10 +34,16 @@ configured reads and writes are exactly its delegate surface.
|
|
|
30
34
|
It **fails closed**: no generated client under `clientGlobs` is a violation, because a checkout that
|
|
31
35
|
never ran `prisma generate` is exactly where drift hides. Run `prisma generate` before lint-meta.
|
|
32
36
|
|
|
37
|
+
## What it does not flag
|
|
38
|
+
|
|
39
|
+
- A generated client whose delegates expose exactly the configured reads and writes, in either the
|
|
40
|
+
per-model (`prisma-client`) or the single-file (`prisma-client-js`) layout.
|
|
41
|
+
- Members that are not query methods: `fields`, the symbol brand and `$`-prefixed members.
|
|
42
|
+
|
|
33
43
|
Members are read line by line: a delegate method is a generic, `name<T ...>(...)`, at the interface's
|
|
34
|
-
member indentation
|
|
44
|
+
member indentation, so a non-generic member is not counted.
|
|
35
45
|
|
|
36
|
-
##
|
|
46
|
+
## Options
|
|
37
47
|
|
|
38
48
|
```ts
|
|
39
49
|
createPrismaMethodSurfaceRule(options?: PrismaMethodSurfaceOptions): IMetaRule
|
|
@@ -50,9 +60,9 @@ createPrismaMethodSurfaceRule(options?: PrismaMethodSurfaceOptions): IMetaRule
|
|
|
50
60
|
With the defaults, the rule checks the exact lists `@noctcore/eslint-plugin-prisma`'s rules read.
|
|
51
61
|
If your own rules read their own lists, pass those.
|
|
52
62
|
|
|
53
|
-
|
|
63
|
+
### Worked example: a monorepo with a database package
|
|
54
64
|
|
|
55
|
-
|
|
65
|
+
A schema that generates into `packages/database/generated/prisma` with the `prisma-client`
|
|
56
66
|
generator:
|
|
57
67
|
|
|
58
68
|
```ts
|
|
@@ -63,3 +73,8 @@ createPrismaMethodSurfaceRule({
|
|
|
63
73
|
|
|
64
74
|
Adding a method to every generated delegate reports it as unguarded; adding it to one model only
|
|
65
75
|
reports that delegate as divergent; removing the generated folder reports that no client was found.
|
|
76
|
+
|
|
77
|
+
## When not to use it
|
|
78
|
+
|
|
79
|
+
If no lint rule of yours reads a list of Prisma method names (no tenant, writer or transaction fence),
|
|
80
|
+
there is no list to keep in step, so skip it.
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
> CI and the local pre-push hook pin the same secret-scanner version, and the hook checks it at run
|
|
4
4
|
> time.
|
|
5
5
|
|
|
6
|
+
<!-- begin generated rule header -->
|
|
7
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createSecurityScannerVersionParityRule` from `@noctcore/lint-meta-rules` · Category `ci` · Fails CI by default: yes
|
|
8
|
+
<!-- end generated rule header -->
|
|
9
|
+
|
|
6
10
|
## Why
|
|
7
11
|
|
|
8
12
|
Different scanner versions ship different rulesets, so a push can pass the local hook and fail CI (or
|
|
@@ -20,8 +24,6 @@ With the defaults (gitleaks):
|
|
|
20
24
|
- a hook `GITLEAKS_IMAGE="...gitleaks:vX.Y.Z"` tag that differs from CI,
|
|
21
25
|
- a hook that never runs `gitleaks version`.
|
|
22
26
|
|
|
23
|
-
Dormant when neither the workflows nor the hook mention the scanner.
|
|
24
|
-
|
|
25
27
|
```yaml
|
|
26
28
|
# .github/workflows/security.yml
|
|
27
29
|
env:
|
|
@@ -39,7 +41,15 @@ GITLEAKS_VERSION="8.29.0"
|
|
|
39
41
|
gitleaks git .
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
##
|
|
44
|
+
## What it does not flag
|
|
45
|
+
|
|
46
|
+
- A repo where neither the workflows nor the hook mention the scanner: the rule is dormant.
|
|
47
|
+
- Whether the hook's version comparison is correct: it checks only that the hook's text runs
|
|
48
|
+
`<scanner> version`.
|
|
49
|
+
- Versions not written as `x.y.z`, and any hook other than `hookFile`. A repo with several hooks needs
|
|
50
|
+
one rule instance per hook.
|
|
51
|
+
|
|
52
|
+
## Options
|
|
43
53
|
|
|
44
54
|
```ts
|
|
45
55
|
createSecurityScannerVersionParityRule(options?: SecurityScannerVersionParityOptions): IMetaRule
|
|
@@ -54,8 +64,7 @@ createSecurityScannerVersionParityRule(options?: SecurityScannerVersionParityOpt
|
|
|
54
64
|
| `workflowGlobs` | `string[]` | `['.github/workflows/*.yml', '.github/workflows/*.yaml']` | Workflow files to scan. |
|
|
55
65
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
56
66
|
|
|
57
|
-
##
|
|
67
|
+
## When not to use it
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
instance per hook.
|
|
69
|
+
If you run the secret scanner in only one place (CI or a local hook, not both), there is no second pin
|
|
70
|
+
to keep in step.
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> Workflow service/container images and docker-compose images are pinned by `@sha256:` digest.
|
|
4
4
|
|
|
5
|
+
<!-- begin generated rule header -->
|
|
6
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createServiceImageDigestPinRule` from `@noctcore/lint-meta-rules` · Category `ci` · Fails CI by default: yes
|
|
7
|
+
<!-- end generated rule header -->
|
|
8
|
+
|
|
5
9
|
## Why
|
|
6
10
|
|
|
7
11
|
A tag (`postgres:17-alpine`) or `latest` names whatever the registry serves today, so a green build
|
|
@@ -11,9 +15,9 @@ can change under an unchanged commit, and a local run cannot promise the bytes C
|
|
|
11
15
|
## What it flags
|
|
12
16
|
|
|
13
17
|
- In workflow files: `image:` under a job's `services:`, and a job's `container:` in scalar or mapping
|
|
14
|
-
form.
|
|
15
|
-
- In compose files: every service's `image:`,
|
|
16
|
-
|
|
18
|
+
form.
|
|
19
|
+
- In compose files: every service's `image:`, including one behind an env default
|
|
20
|
+
(`${REDIS_IMAGE:-redis:7}`), which is reported as written since the text does not show a digest.
|
|
17
21
|
|
|
18
22
|
Each violation carries the 1-indexed line.
|
|
19
23
|
|
|
@@ -32,7 +36,17 @@ services:
|
|
|
32
36
|
image: app-api:local
|
|
33
37
|
```
|
|
34
38
|
|
|
35
|
-
##
|
|
39
|
+
## What it does not flag
|
|
40
|
+
|
|
41
|
+
- An image pinned as `tag@sha256:<digest>`.
|
|
42
|
+
- An action step's `with: image:` input: it is not an image pull.
|
|
43
|
+
- A compose service that also has `build:`: there `image:` tags an image built locally, which has no
|
|
44
|
+
upstream digest.
|
|
45
|
+
- An exact ref listed in `allowUnpinned`, and compose files under a `skipDirs` segment.
|
|
46
|
+
|
|
47
|
+
The check is line-based text, not a YAML parse.
|
|
48
|
+
|
|
49
|
+
## Options
|
|
36
50
|
|
|
37
51
|
```ts
|
|
38
52
|
createServiceImageDigestPinRule(options?: ServiceImageDigestPinOptions): IMetaRule
|
|
@@ -46,7 +60,7 @@ createServiceImageDigestPinRule(options?: ServiceImageDigestPinOptions): IMetaRu
|
|
|
46
60
|
| `allowUnpinned` | `string[]` | `[]` | Exact image refs allowed unpinned, for a ref the registry cannot serve a digest for. Only that exact ref passes; a bumped tag is checked again. |
|
|
47
61
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
48
62
|
|
|
49
|
-
##
|
|
63
|
+
## When not to use it
|
|
50
64
|
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
If your workflows and compose files only run throwaway local services where you accept whatever a tag
|
|
66
|
+
serves today, skip it.
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
> Every call into the sign-in seam passes the session epoch it captured before reading the
|
|
4
4
|
> credential.
|
|
5
5
|
|
|
6
|
+
<!-- begin generated rule header -->
|
|
7
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createSessionEpochCapturedRule` from `@noctcore/lint-meta-rules/session` · Category `source-text` · Fails CI by default: yes
|
|
8
|
+
<!-- end generated rule header -->
|
|
9
|
+
|
|
6
10
|
Import it from the `session` entry point:
|
|
7
11
|
|
|
8
12
|
```ts
|
|
@@ -25,9 +29,11 @@ never mention the epoch.
|
|
|
25
29
|
## What it flags
|
|
26
30
|
|
|
27
31
|
Each `.<call>(...)` call in a file matched by `sourceGlobs` whose argument text (found by balancing
|
|
28
|
-
parentheses) does not mention `field` as a whole word.
|
|
32
|
+
parentheses) does not mention `field` as a whole word. The parenthesis scanner reads each call on its
|
|
33
|
+
own, so a call written on one line is checked too, and an unstamped call cannot borrow the epoch of
|
|
34
|
+
the next one.
|
|
29
35
|
|
|
30
|
-
## What it
|
|
36
|
+
## What it does not flag
|
|
31
37
|
|
|
32
38
|
- `epochless: true`, or any identifier that merely contains the field: whole words only.
|
|
33
39
|
- Files in `exempt` (the seam's own home, which defines the method and its default).
|
|
@@ -35,11 +41,7 @@ parentheses) does not mention `field` as a whole word.
|
|
|
35
41
|
|
|
36
42
|
With no `call` the rule is inert.
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
call. That missed a call written on one line, and let an unstamped one-line call borrow the epoch of
|
|
40
|
-
the next multi-line call. The parenthesis scanner here reads each call on its own.
|
|
41
|
-
|
|
42
|
-
## Factory
|
|
44
|
+
## Options
|
|
43
45
|
|
|
44
46
|
```ts
|
|
45
47
|
createSessionEpochCapturedRule(options?: SessionEpochCapturedOptions): IMetaRule
|
|
@@ -58,7 +60,11 @@ createSessionEpochCapturedRule(options?: SessionEpochCapturedOptions): IMetaRule
|
|
|
58
60
|
| `hint` | `string` | none | Appended to every message. |
|
|
59
61
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
### Worked example: an app with a post-credential sign-in seam
|
|
64
|
+
|
|
65
|
+
Every sign-in entry point (password, OAuth, second factor) ends by calling one seam,
|
|
66
|
+
`beginOrEstablish`, which a two-factor challenge service defines. Each entry point reads the epoch with
|
|
67
|
+
`sessionService.readEpoch(userId)` before it checks the credential, and passes it in:
|
|
62
68
|
|
|
63
69
|
```ts
|
|
64
70
|
createSessionEpochCapturedRule({
|
|
@@ -74,7 +80,7 @@ createSessionEpochCapturedRule({
|
|
|
74
80
|
});
|
|
75
81
|
```
|
|
76
82
|
|
|
77
|
-
Two
|
|
83
|
+
Two flows in such an app mint without a captured epoch and are outside the rule by construction, because they
|
|
78
84
|
never call the seam: signup (the account did not exist when the request began) and password change (it
|
|
79
85
|
revokes first and then re-issues, so the current counter is the right one).
|
|
80
86
|
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
> Every call that mints a session stamps the principal's kind onto it, or sits in an allowlisted,
|
|
4
4
|
> provably single-kind flow.
|
|
5
5
|
|
|
6
|
+
<!-- begin generated rule header -->
|
|
7
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createSessionKindStampedRule` from `@noctcore/lint-meta-rules/session` · Category `source-text` · Fails CI by default: yes
|
|
8
|
+
<!-- end generated rule header -->
|
|
9
|
+
|
|
6
10
|
Import it from the `session` entry point:
|
|
7
11
|
|
|
8
12
|
```ts
|
|
@@ -17,7 +21,7 @@ mint that wrote it. A session minted without the field reads as whatever the rea
|
|
|
17
21
|
fence between the two kinds that is a silent promotion: a portal account whose session lost its stamp
|
|
18
22
|
is a staff caller inside its own tenant, and the tenant boundary waves it through as legitimate.
|
|
19
23
|
|
|
20
|
-
This is not hypothetical. In
|
|
24
|
+
This is not hypothetical. In one production codebase, two re-issue paths (a password
|
|
21
25
|
change and a revoke-other-sessions action, both reachable by a portal account) shipped without the
|
|
22
26
|
stamp while every gate was green, because nothing mechanical looked.
|
|
23
27
|
|
|
@@ -32,10 +36,7 @@ parentheses, so a multi-line options object with nested calls is read whole):
|
|
|
32
36
|
somewhere. Coarser, but no silent hole.
|
|
33
37
|
3. Otherwise the file must be in `allowUnstamped`.
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
it on a second delegated mint passes clause 2.
|
|
37
|
-
|
|
38
|
-
## What it leaves alone
|
|
39
|
+
## What it does not flag
|
|
39
40
|
|
|
40
41
|
- `.<mintCall>Something(`, and the method's definition.
|
|
41
42
|
- `kindless` or `kinds`: the field must appear as a whole word.
|
|
@@ -43,7 +44,10 @@ it on a second delegated mint passes clause 2.
|
|
|
43
44
|
|
|
44
45
|
With no `mintCall` the rule is inert.
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
Residual gap, stated rather than hidden: a file that stamps the field on one delegated mint and forgets
|
|
48
|
+
it on a second delegated mint passes clause 2.
|
|
49
|
+
|
|
50
|
+
## Options
|
|
47
51
|
|
|
48
52
|
```ts
|
|
49
53
|
createSessionKindStampedRule(options?: SessionKindStampedOptions): IMetaRule
|
|
@@ -62,7 +66,11 @@ createSessionKindStampedRule(options?: SessionKindStampedOptions): IMetaRule
|
|
|
62
66
|
| `hint` | `string` | none | Appended to every message. |
|
|
63
67
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
### Worked example: an app with staff and portal accounts
|
|
70
|
+
|
|
71
|
+
One sign-in serves staff accounts and invite-only portal accounts, and every session mint goes
|
|
72
|
+
through `establishSession`. Self-signup only ever creates staff accounts, so its file is allowed to
|
|
73
|
+
mint without a stamp:
|
|
66
74
|
|
|
67
75
|
```ts
|
|
68
76
|
createSessionKindStampedRule({
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
> Every file that opens a door into a session declares where it leaves the caller, and a door whose
|
|
4
4
|
> landing demands a return shape has it.
|
|
5
5
|
|
|
6
|
+
<!-- begin generated rule header -->
|
|
7
|
+
Runs under `@noctcore/harness`, not ESLint · Factory `createSessionLandingDeclaredRule` from `@noctcore/lint-meta-rules/session` · Category `source-text` · Fails CI by default: yes
|
|
8
|
+
<!-- end generated rule header -->
|
|
9
|
+
|
|
6
10
|
Import it from the `session` entry point:
|
|
7
11
|
|
|
8
12
|
```ts
|
|
@@ -15,8 +19,8 @@ With two shells behind one sign-in (staff and a customer portal, say), the clien
|
|
|
15
19
|
account to the right one if the response that ends the sign-in says which kind it is. The first door
|
|
16
20
|
(the password sign-in) usually does. A second door that finishes a sign-in from a different service (a
|
|
17
21
|
second-factor challenge) can return the same union and still have its landing unwired, and nothing
|
|
18
|
-
fails: the account signs in, lands in the wrong shell, and bounces off its guard. In
|
|
19
|
-
|
|
22
|
+
fails: the account signs in, lands in the wrong shell, and bounces off its guard. In one production
|
|
23
|
+
codebase, every portal account with 2FA enabled did exactly that.
|
|
20
24
|
|
|
21
25
|
Two sibling rules fence the same seam, [`session-mint-callers`](./session-mint-callers.md) (who may
|
|
22
26
|
mint) and [`session-kind-stamped`](./session-kind-stamped.md) (the session carries the kind). Neither
|
|
@@ -34,7 +38,7 @@ can see this, because both are about the session and this is about the RESPONSE.
|
|
|
34
38
|
3. **The declarations.** A door whose `landing` is not a key of `landings`, or whose `because` is empty.
|
|
35
39
|
4. **Staleness.** A door whose file no longer exists, or that `sourceGlobs` do not reach.
|
|
36
40
|
|
|
37
|
-
## What it
|
|
41
|
+
## What it does not flag
|
|
38
42
|
|
|
39
43
|
- A file that only defines a door method (the leading dot is required).
|
|
40
44
|
- A door whose landing maps to `null`: a re-issue that replaces the cookie of a caller already inside a
|
|
@@ -43,7 +47,7 @@ can see this, because both are about the session and this is about the RESPONSE.
|
|
|
43
47
|
|
|
44
48
|
With no `doorCalls` the rule is inert.
|
|
45
49
|
|
|
46
|
-
##
|
|
50
|
+
## Options
|
|
47
51
|
|
|
48
52
|
```ts
|
|
49
53
|
createSessionLandingDeclaredRule(options?: SessionLandingDeclaredOptions): IMetaRule
|
|
@@ -61,7 +65,11 @@ createSessionLandingDeclaredRule(options?: SessionLandingDeclaredOptions): IMeta
|
|
|
61
65
|
| `hint` | `string` | none | Appended to every message. |
|
|
62
66
|
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
63
67
|
|
|
64
|
-
|
|
68
|
+
### Worked example: an app with two sign-in shells
|
|
69
|
+
|
|
70
|
+
Staff and portal accounts share one sign-in and land in different shells. Sessions open through
|
|
71
|
+
`establishSession` or the second-factor gate `beginOrEstablish`. The two doors either kind can reach
|
|
72
|
+
must return `Promise<ILoginResult>`; the rest are staff-only or re-issues:
|
|
65
73
|
|
|
66
74
|
```ts
|
|
67
75
|
const AUTH = 'apps/api/src/modules/auth';
|