@noctcore/lint-meta-rules 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ # `github-actions-runner-pinned`
2
+
3
+ > Workflow jobs run on a named runner image, never a `*-latest` label.
4
+
5
+ ## Why
6
+
7
+ GitHub repoints `ubuntu-latest` (and `macos-latest`, `windows-latest`) to a new OS image on its own
8
+ schedule, so a green workflow can turn red, or quietly change what it tests, with no commit in your
9
+ repo. Pinning a named image makes the move a reviewed diff.
10
+
11
+ ## What it flags
12
+
13
+ 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. An expression (`${{ matrix.os }}`) cannot be judged from the text and
15
+ is left alone; a commented-out `runs-on:` is ignored.
16
+
17
+ ```yaml
18
+ # Bad
19
+ runs-on: ubuntu-latest
20
+ runs-on: [self-hosted, ubuntu-latest]
21
+
22
+ # Good
23
+ runs-on: ubuntu-24.04
24
+ runs-on: ${{ matrix.os }}
25
+ ```
26
+
27
+ ## Factory
28
+
29
+ ```ts
30
+ createGithubActionsRunnerPinnedRule(options?: GithubActionsRunnerPinnedOptions): IMetaRule
31
+ ```
32
+
33
+ | Option | Type | Default | Meaning |
34
+ | --- | --- | --- | --- |
35
+ | `workflowGlobs` | `string[]` | `['.github/workflows/*.yml', '.github/workflows/*.yaml']` | Workflow files to scan. |
36
+ | `floatingLabel` | `RegExp` | `/^[\w.-]+-latest$/u` | A label matching this is floating. Do not pass a `g`-flagged regex. |
37
+ | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
38
+
39
+ ## Limits
40
+
41
+ Line-based text, not a YAML parse. A matrix value such as `os: [ubuntu-latest]` that feeds
42
+ `runs-on: ${{ matrix.os }}` is not checked.
@@ -0,0 +1,46 @@
1
+ # `github-actions-sha-pinned`
2
+
3
+ > GitHub Actions `uses:` refs are pinned to a 40-character commit SHA with a `# vN` comment.
4
+
5
+ ## Why
6
+
7
+ A tag or branch ref is a moving target: whoever controls the action's repository can repoint it, and
8
+ the new code runs with your workflow's token. A full commit SHA cannot move. The trailing `# vN`
9
+ comment keeps the pin readable and is what Dependabot rewrites on a bump, so a bare SHA is rejected
10
+ too.
11
+
12
+ ## What it flags
13
+
14
+ Every `uses:` line (step-level and job-level reusable workflow calls) in the scanned workflow files:
15
+
16
+ - a ref whose pin is not a 40-character hex SHA (tags, branches, short SHAs, no `@` at all),
17
+ - a SHA-pinned ref with no `# vN` comment,
18
+ - a `docker://` ref with no `@sha256:<digest>`.
19
+
20
+ Local actions (`uses: ./path`) are exempt. Each violation carries the 1-indexed line.
21
+
22
+ ```yaml
23
+ # Bad
24
+ - uses: actions/checkout@v6
25
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
26
+
27
+ # Good
28
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
29
+ - uses: ./.github/actions/setup
30
+ ```
31
+
32
+ ## Factory
33
+
34
+ ```ts
35
+ createGithubActionsShaPinnedRule(options?: GithubActionsShaPinnedOptions): IMetaRule
36
+ ```
37
+
38
+ | Option | Type | Default | Meaning |
39
+ | --- | --- | --- | --- |
40
+ | `workflowGlobs` | `string[]` | `['.github/workflows/*.yml', '.github/workflows/*.yaml']` | Workflow files to scan. |
41
+ | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
42
+
43
+ ## Limits
44
+
45
+ The check is line-based text, not a YAML parse. A `uses:` value split across lines, or built from an
46
+ expression, is not seen.
@@ -0,0 +1,61 @@
1
+ # `security-scanner-version-parity`
2
+
3
+ > CI and the local pre-push hook pin the same secret-scanner version, and the hook checks it at run
4
+ > time.
5
+
6
+ ## Why
7
+
8
+ Different scanner versions ship different rulesets, so a push can pass the local hook and fail CI (or
9
+ the reverse) with no code difference. Pinning one version on both sides, and having the hook refuse a
10
+ native binary of another version, keeps the two scans the same scan.
11
+
12
+ ## What it flags
13
+
14
+ With the defaults (gitleaks):
15
+
16
+ - workflows that pin different `GITLEAKS_VERSION` values,
17
+ - a hook that runs gitleaks when no workflow pins `GITLEAKS_VERSION`, or workflows that pin it when the
18
+ hook never runs gitleaks,
19
+ - a hook with no `GITLEAKS_VERSION="x.y.z"` declaration, or one that differs from CI,
20
+ - a hook `GITLEAKS_IMAGE="...gitleaks:vX.Y.Z"` tag that differs from CI,
21
+ - a hook that never runs `gitleaks version`.
22
+
23
+ Dormant when neither the workflows nor the hook mention the scanner.
24
+
25
+ ```yaml
26
+ # .github/workflows/security.yml
27
+ env:
28
+ GITLEAKS_VERSION: '8.30.1'
29
+ ```
30
+
31
+ ```bash
32
+ # Good: scripts/ci/pre-push.sh
33
+ GITLEAKS_VERSION="8.30.1"
34
+ GITLEAKS_IMAGE="ghcr.io/gitleaks/gitleaks:v${GITLEAKS_VERSION}"
35
+ [ "$(gitleaks version)" = "$GITLEAKS_VERSION" ] || exit 1
36
+
37
+ # Bad: a drifted pin and no run-time check
38
+ GITLEAKS_VERSION="8.29.0"
39
+ gitleaks git .
40
+ ```
41
+
42
+ ## Factory
43
+
44
+ ```ts
45
+ createSecurityScannerVersionParityRule(options?: SecurityScannerVersionParityOptions): IMetaRule
46
+ ```
47
+
48
+ | Option | Type | Default | Meaning |
49
+ | --- | --- | --- | --- |
50
+ | `scanner` | `string` | `'gitleaks'` | The scanner's binary name, as it appears in the hook and its image name. |
51
+ | `versionVariable` | `string` | `'GITLEAKS_VERSION'` | The variable both sides pin the version in (`KEY: x.y.z` in workflows, `KEY=x.y.z` in the hook). |
52
+ | `imageVariable` | `string` | `'GITLEAKS_IMAGE'` | The hook's optional image variable, whose `<scanner>:vX.Y.Z` tag must agree. |
53
+ | `hookFile` | `string` | `'scripts/ci/pre-push.sh'` | The local hook script, repo-relative. |
54
+ | `workflowGlobs` | `string[]` | `['.github/workflows/*.yml', '.github/workflows/*.yaml']` | Workflow files to scan. |
55
+ | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
56
+
57
+ ## Limits
58
+
59
+ It checks that the hook's text runs `<scanner> version`, not that the comparison is correct, and it
60
+ reads only `x.y.z` versions. One hook file is checked; a repo with several hooks needs one rule
61
+ instance per hook.
@@ -0,0 +1,52 @@
1
+ # `service-image-digest-pin`
2
+
3
+ > Workflow service/container images and docker-compose images are pinned by `@sha256:` digest.
4
+
5
+ ## Why
6
+
7
+ A tag (`postgres:17-alpine`) or `latest` names whatever the registry serves today, so a green build
8
+ can change under an unchanged commit, and a local run cannot promise the bytes CI ran.
9
+ `tag@sha256:<digest>` keeps the readable tag and fixes the content.
10
+
11
+ ## What it flags
12
+
13
+ - In workflow files: `image:` under a job's `services:`, and a job's `container:` in scalar or mapping
14
+ form. An action step's `with: image:` input is not an image pull and is ignored.
15
+ - In compose files: every service's `image:`, except a service that also has `build:` (there `image:`
16
+ tags an image built locally, which has no upstream digest).
17
+
18
+ Each violation carries the 1-indexed line.
19
+
20
+ ```yaml
21
+ # Bad
22
+ services:
23
+ db:
24
+ image: postgres:17-alpine
25
+
26
+ # Good
27
+ services:
28
+ db:
29
+ image: postgres:17-alpine@sha256:<digest>
30
+ api:
31
+ build: .
32
+ image: app-api:local
33
+ ```
34
+
35
+ ## Factory
36
+
37
+ ```ts
38
+ createServiceImageDigestPinRule(options?: ServiceImageDigestPinOptions): IMetaRule
39
+ ```
40
+
41
+ | Option | Type | Default | Meaning |
42
+ | --- | --- | --- | --- |
43
+ | `workflowGlobs` | `string[]` | `['.github/workflows/*.yml', '.github/workflows/*.yaml']` | Workflow files to scan. |
44
+ | `composeGlobs` | `string[]` | `compose` / `docker-compose` with optional `.x` / `-x` infix, `.yml` / `.yaml`, at any depth including dot-directories | Compose files to scan. |
45
+ | `skipDirs` | `string[]` | `['node_modules', '.git', 'dist', '.turbo', 'coverage']` | A compose path with any of these segments is skipped. |
46
+ | `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
+ | `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
48
+
49
+ ## Limits
50
+
51
+ Line-based text, not a YAML parse. An image behind an env default (`${REDIS_IMAGE:-redis:7}`) is
52
+ reported as written, since the text does not show a digest.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noctcore/lint-meta-rules",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Portable, parameterized lint-meta rules — whole-repo / cross-file invariants ESLint cannot reach — for the @noctcore/harness lint-meta runner.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -46,7 +46,7 @@
46
46
  "test": "bun test"
47
47
  },
48
48
  "dependencies": {
49
- "@noctcore/harness": "^0.1.0"
49
+ "@noctcore/harness": "^0.3.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^22.0.0",