@intentius/chant-lexicon-github 0.1.23 → 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.
Files changed (111) hide show
  1. package/dist/integrity.json +34 -4
  2. package/dist/manifest.json +1 -1
  3. package/dist/rules/{gha020.ts → gha013.ts} +4 -4
  4. package/dist/rules/gha029.ts +68 -0
  5. package/dist/rules/gha030.ts +41 -0
  6. package/dist/rules/gha031.ts +77 -0
  7. package/dist/rules/gha032.ts +39 -0
  8. package/dist/rules/gha033.ts +47 -0
  9. package/dist/rules/gha034.ts +41 -0
  10. package/dist/rules/gha035.ts +66 -0
  11. package/dist/rules/gha036.ts +42 -0
  12. package/dist/rules/gha037.ts +46 -0
  13. package/dist/rules/gha038.ts +41 -0
  14. package/dist/rules/gha039.ts +41 -0
  15. package/dist/rules/gha040.ts +43 -0
  16. package/dist/rules/gha041.ts +38 -0
  17. package/dist/rules/gha042.ts +40 -0
  18. package/dist/rules/gha043.ts +41 -0
  19. package/dist/rules/gha044.ts +41 -0
  20. package/dist/rules/gha045.ts +38 -0
  21. package/dist/rules/gha046.ts +56 -0
  22. package/dist/rules/gha047.ts +51 -0
  23. package/dist/rules/gha048.ts +41 -0
  24. package/dist/rules/gha049.ts +42 -0
  25. package/dist/rules/gha050.ts +44 -0
  26. package/dist/rules/gha051.ts +45 -0
  27. package/dist/rules/gha052.ts +42 -0
  28. package/dist/rules/gha053.ts +41 -0
  29. package/dist/rules/gha054.ts +41 -0
  30. package/dist/rules/gha055.ts +50 -0
  31. package/dist/rules/gha056.ts +37 -0
  32. package/dist/rules/gha057.ts +40 -0
  33. package/dist/rules/gha058.ts +48 -0
  34. package/dist/rules/yaml-helpers.ts +335 -0
  35. package/package.json +1 -1
  36. package/src/codegen/docs.ts +188 -0
  37. package/src/composites/dependabot.test.ts +48 -0
  38. package/src/composites/dependabot.ts +91 -0
  39. package/src/composites/index.ts +2 -0
  40. package/src/index.ts +2 -0
  41. package/src/lint/post-synth/{gha020.test.ts → gha013.test.ts} +7 -7
  42. package/src/lint/post-synth/{gha020.ts → gha013.ts} +4 -4
  43. package/src/lint/post-synth/gha029.test.ts +108 -0
  44. package/src/lint/post-synth/gha029.ts +68 -0
  45. package/src/lint/post-synth/gha030.test.ts +87 -0
  46. package/src/lint/post-synth/gha030.ts +41 -0
  47. package/src/lint/post-synth/gha031.test.ts +75 -0
  48. package/src/lint/post-synth/gha031.ts +77 -0
  49. package/src/lint/post-synth/gha032.test.ts +65 -0
  50. package/src/lint/post-synth/gha032.ts +39 -0
  51. package/src/lint/post-synth/gha033.test.ts +68 -0
  52. package/src/lint/post-synth/gha033.ts +47 -0
  53. package/src/lint/post-synth/gha034.test.ts +86 -0
  54. package/src/lint/post-synth/gha034.ts +41 -0
  55. package/src/lint/post-synth/gha035.test.ts +85 -0
  56. package/src/lint/post-synth/gha035.ts +66 -0
  57. package/src/lint/post-synth/gha036.test.ts +68 -0
  58. package/src/lint/post-synth/gha036.ts +42 -0
  59. package/src/lint/post-synth/gha037.test.ts +66 -0
  60. package/src/lint/post-synth/gha037.ts +46 -0
  61. package/src/lint/post-synth/gha038.test.ts +64 -0
  62. package/src/lint/post-synth/gha038.ts +41 -0
  63. package/src/lint/post-synth/gha039.test.ts +66 -0
  64. package/src/lint/post-synth/gha039.ts +41 -0
  65. package/src/lint/post-synth/gha040.test.ts +77 -0
  66. package/src/lint/post-synth/gha040.ts +43 -0
  67. package/src/lint/post-synth/gha041.test.ts +48 -0
  68. package/src/lint/post-synth/gha041.ts +38 -0
  69. package/src/lint/post-synth/gha042.test.ts +53 -0
  70. package/src/lint/post-synth/gha042.ts +40 -0
  71. package/src/lint/post-synth/gha043.test.ts +69 -0
  72. package/src/lint/post-synth/gha043.ts +41 -0
  73. package/src/lint/post-synth/gha044.test.ts +56 -0
  74. package/src/lint/post-synth/gha044.ts +41 -0
  75. package/src/lint/post-synth/gha045.test.ts +51 -0
  76. package/src/lint/post-synth/gha045.ts +38 -0
  77. package/src/lint/post-synth/gha046.test.ts +59 -0
  78. package/src/lint/post-synth/gha046.ts +56 -0
  79. package/src/lint/post-synth/gha047.test.ts +66 -0
  80. package/src/lint/post-synth/gha047.ts +51 -0
  81. package/src/lint/post-synth/gha048.test.ts +51 -0
  82. package/src/lint/post-synth/gha048.ts +41 -0
  83. package/src/lint/post-synth/gha049.test.ts +72 -0
  84. package/src/lint/post-synth/gha049.ts +42 -0
  85. package/src/lint/post-synth/gha050.test.ts +55 -0
  86. package/src/lint/post-synth/gha050.ts +44 -0
  87. package/src/lint/post-synth/gha051.test.ts +72 -0
  88. package/src/lint/post-synth/gha051.ts +45 -0
  89. package/src/lint/post-synth/gha052.test.ts +52 -0
  90. package/src/lint/post-synth/gha052.ts +42 -0
  91. package/src/lint/post-synth/gha053.test.ts +65 -0
  92. package/src/lint/post-synth/gha053.ts +41 -0
  93. package/src/lint/post-synth/gha054.test.ts +49 -0
  94. package/src/lint/post-synth/gha054.ts +41 -0
  95. package/src/lint/post-synth/gha055.test.ts +49 -0
  96. package/src/lint/post-synth/gha055.ts +50 -0
  97. package/src/lint/post-synth/gha056.test.ts +47 -0
  98. package/src/lint/post-synth/gha056.ts +37 -0
  99. package/src/lint/post-synth/gha057.test.ts +61 -0
  100. package/src/lint/post-synth/gha057.ts +40 -0
  101. package/src/lint/post-synth/gha058.test.ts +67 -0
  102. package/src/lint/post-synth/gha058.ts +48 -0
  103. package/src/lint/post-synth/yaml-helpers.ts +335 -0
  104. package/src/lint/rules/data/flagged-actions.ts +36 -0
  105. package/src/lint/rules/data/known-action-slugs.ts +38 -0
  106. package/src/lint/rules/data/preinstalled-tools.ts +36 -0
  107. package/src/lint/rules/data/risky-features.ts +39 -0
  108. package/src/lint/rules/data/trusted-action-owners.ts +12 -0
  109. package/src/lint/rules/data/untrusted-contexts.ts +44 -0
  110. package/src/plugin.test.ts +47 -1
  111. package/src/serializer.ts +38 -5
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "342371d6e50839e96a0e26116644681538de0a1990904009ba9853c64c938a1e",
4
+ "manifest.json": "9bfb1e2f89ab306d4369dbd5df0a9cb0fc22cc8d282f37a1c6195ce86b812410",
5
5
  "meta.json": "798c5f5f37b0174756d2451299acfdc95c014f11d06d32b9153bbdfa9f3a580c",
6
6
  "types/index.d.ts": "39c0791f0e58025bcef82e0f7a92240508f899fc2771d5dd51b2369b4d330f13",
7
7
  "rules/deprecated-action-version.ts": "d41e6e532ab7f623af1bee4ac5279fcb2baada7defa1c5d022a5bc71983e8797",
@@ -20,10 +20,10 @@
20
20
  "rules/gha006.ts": "7e2977d34851ce2ed9f91c4dd78ecda8d4de099ceaac1cd9ab74960a193133a3",
21
21
  "rules/gha009.ts": "0f9a261e886309679494da32b72a0d9ea20017e5da75a9d59a04846a0e4d78db",
22
22
  "rules/gha011.ts": "53b7ca79b5bf56e5793a9b7a44ae0ac766eb5acf0669005e7810cb7762bab79c",
23
+ "rules/gha013.ts": "47c1b28565aced3370b85d00a03b6110a32e451661e34badd731ffbb24b45d9a",
23
24
  "rules/gha017.ts": "ec62dac7f9d69f36978cb461c4ba391a7e445d410bbfa497cddcb1b12cfd5904",
24
25
  "rules/gha018.ts": "2c71c5accb96bccfa01075e7f8f4ad66e6d9a68a81b86f89a68cabb9c144e140",
25
26
  "rules/gha019.ts": "d0b33d63812c9cba2ebf9f2d85d8cf22068ea96460cd2143bdfb4d2a4d58fe2e",
26
- "rules/gha020.ts": "72dc68a1eae98e0e4611dfe62eecda6900c22f286825bc3c5133f1f5e2e62d43",
27
27
  "rules/gha021.ts": "01463240871326c2798abb3a3cdccba95b44efa1e0083e54a81824e8875dcc37",
28
28
  "rules/gha022.ts": "7bfb5e280ace280f06b8b64645863210320cd38380a2fc7b95a3455753a50f9c",
29
29
  "rules/gha023.ts": "708468e098e434739bd62573d8559318c08a2830aa8ac39d2bf465e7da012b38",
@@ -32,10 +32,40 @@
32
32
  "rules/gha026.ts": "1c0278cb453dad28f417fd11fd30d83402a64ccb809f366a5b46ffceb3466da6",
33
33
  "rules/gha027.ts": "5a266987c65dc92ca77c1d4c9b8fa29d6435275d399819dec5027be3f2d96239",
34
34
  "rules/gha028.ts": "5d1f06b635354b67557f0dbdc2f11ca4c5b8e8541b85b1c135b6f02ef147d9a0",
35
- "rules/yaml-helpers.ts": "4729afd9ff287dbef38b84298b130f5ab8822a351d9d37111a7ed4cb589ced58",
35
+ "rules/gha029.ts": "6e2c3262761fd2d21a1c8b6e85a8b77df9fb0ef049c8856e62ff6aef6670c870",
36
+ "rules/gha030.ts": "310fd8e8acc7ea6ceba4dfb8fb133080cf91fd5fc90f7f1d9dfb11527a7ed0ca",
37
+ "rules/gha031.ts": "a5c702ab22b7031eddb21406bc4e92ada4a835a27a7469b2e0db2b1c0965a9b1",
38
+ "rules/gha032.ts": "ee2c9da89457765671cab774ecd9de2202dbf8a6832e83f2d1678ea091596ca8",
39
+ "rules/gha033.ts": "0e8608c22fe9e263c867b75aa000d83583cfd1ad1fe1ef177fe9cc8dcbbf7157",
40
+ "rules/gha034.ts": "afce9e7d49a78d6684e6f361a76bf909b9303aef18cc284c0737252e5eaf4a46",
41
+ "rules/gha035.ts": "078fb87dcfc3fa902085291ff35fa1adadda77e5e261cfb6cea4cb0672150b11",
42
+ "rules/gha036.ts": "4dd0594d22a3318bb7140549811709814fb44641a632ca20cf8a1da674e37a0f",
43
+ "rules/gha037.ts": "46aa9c12e6f65d64a7bb62894427c8c22b9a994c336acecb4d179936be907287",
44
+ "rules/gha038.ts": "1b589319db7635927e9489e3e6590d4e41b87dc56214a8a2daab789f381497e1",
45
+ "rules/gha039.ts": "cad2d726386d6a36b0af3958aa9c3decc7789049b4b44003680d75f5964b1603",
46
+ "rules/gha040.ts": "b56203cda4519fcc0a10c7f5e07998fdd448eae6b2e884458c27a4822162fdc9",
47
+ "rules/gha041.ts": "030d0fba36b72dbdbfa1cd976d733f1cd29c132b79efa0842392e77d66cd54bc",
48
+ "rules/gha042.ts": "bdf719250ed6749712755d96a4aab4ebdd94ac468b9c132b514751dc838445c7",
49
+ "rules/gha043.ts": "5f2d7dc63f4de13f4ee316d2b1ce1efc52d4fa3599d47d4cd91af344cb73abc1",
50
+ "rules/gha044.ts": "9c80a0aed056ec11f17ae0ddff38a2c481f417352cbda55f01ab1e81825eb5b5",
51
+ "rules/gha045.ts": "93b0de6cb6206992674c58cdf50a734209e7eb6cc1ae84ef1b846426e9021172",
52
+ "rules/gha046.ts": "bb57f4b6664043c02ce20f0773dcb9e59b1134e36f50645b2c4d79aab49e7fb7",
53
+ "rules/gha047.ts": "26e7dd636d815c27f627cf6d51aa60fd1ce198c87b83dd6fef9d7f6929636301",
54
+ "rules/gha048.ts": "f26c3f95c4710621b58bee7b867cb6e83dc95edbca7cef97fdfa8034bdc322bb",
55
+ "rules/gha049.ts": "3d35beee07777532f4bccf940d6e34cfb541986429419d50ce75d65faf7fac70",
56
+ "rules/gha050.ts": "da3263efc790ce790456152ebbe900502b2516901028662e710b17c27efb8e90",
57
+ "rules/gha051.ts": "40ca97d7f861f2acb2af54ccd33aa3b11a53a07a7188b3023ca67c831ae73b99",
58
+ "rules/gha052.ts": "f7d1d44b695b02462030e209cbd7610f29fabc53eda21eca74ccbdd8f6dfa8e6",
59
+ "rules/gha053.ts": "a0b42e53b0b6894b6f17f4394cb9a5d864af07ca7f32dac44519dc1bc6541476",
60
+ "rules/gha054.ts": "daede5dbd2bb42985d48dbe1c43c2806924272e701f109f30c1e590fc52255e0",
61
+ "rules/gha055.ts": "fa335dcded664147a468d11c54c18393571df7efdc36d64ada029ac8994651b5",
62
+ "rules/gha056.ts": "73fa2eea80504522cb53db1e48b36e97ede0eeb7b60ed531a476d2f43d3746ae",
63
+ "rules/gha057.ts": "78e896232d6f5c3e4c1bab873eae97960dbb8a0c108f1546f32d943aafc41a91",
64
+ "rules/gha058.ts": "cdcb60916e3ce091b0e6c73295f24b77fca1480fb0a5907ae6d1fc5b1805f794",
65
+ "rules/yaml-helpers.ts": "74b430ec4810135be1e5dcd198b907c2cd179a9cc5a339041b6dab31edb4d2a0",
36
66
  "skills/chant-github.md": "f3707f09634be4bbb45a6be279f17bb27f275b98e6d3337cc8872c1623ba050f",
37
67
  "skills/chant-github-patterns.md": "bb3abef289a8fdfcf07d6bb2d7289dcb2f38bc0cb0321ea320b78b45a6f548c0",
38
68
  "skills/chant-github-security.md": "aab111cb0871cad30281ce48d7da23663689619351029219e2be019a1a61e394"
39
69
  },
40
- "composite": "c22cd8e0736c58805457ba2afd209376bc095117797f7bac07145de793f0dd61"
70
+ "composite": "c655b1bc501ac68358351aa669e0eeca83ba58bf483d5503949bb1dad5410586"
41
71
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github",
3
- "version": "0.1.23",
3
+ "version": "0.2.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GitHub",
6
6
  "intrinsics": [
@@ -1,5 +1,5 @@
1
1
  /**
2
- * GHA020: Missing Job-Level Permissions for Sensitive Triggers
2
+ * GHA013: Missing Job-Level Permissions for Sensitive Triggers
3
3
  *
4
4
  * Flags jobs without explicit `permissions:` when the workflow uses
5
5
  * `pull_request_target` or `workflow_dispatch` triggers.
@@ -8,8 +8,8 @@
8
8
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
9
  import { getPrimaryOutput, extractJobs, extractTriggers } from "./yaml-helpers";
10
10
 
11
- export const gha020: PostSynthCheck = {
12
- id: "GHA020",
11
+ export const gha013: PostSynthCheck = {
12
+ id: "GHA013",
13
13
  description: "Missing job-level permissions for sensitive triggers",
14
14
 
15
15
  check(ctx: PostSynthContext): PostSynthDiagnostic[] {
@@ -25,7 +25,7 @@ export const gha020: PostSynthCheck = {
25
25
  for (const [jobName, job] of jobs) {
26
26
  if (!job.permissions) {
27
27
  diagnostics.push({
28
- checkId: "GHA020",
28
+ checkId: "GHA013",
29
29
  severity: "warning",
30
30
  message: `Job "${jobName}" lacks explicit permissions but workflow uses a sensitive trigger. Add job-level permissions for least-privilege security.`,
31
31
  entity: jobName,
@@ -0,0 +1,68 @@
1
+ /**
2
+ * GHA029: Unpinned Action or Reusable-Workflow Reference
3
+ *
4
+ * Flags any `uses:` (step action or job-level reusable workflow) that is pinned
5
+ * to a mutable tag or branch instead of a full 40-character commit SHA. A tag
6
+ * can be repointed to malicious code after review, so every external reference
7
+ * is a supply-chain trust decision.
8
+ *
9
+ * `actions/checkout` is intentionally left to the more specific GHA021. Local
10
+ * references (`./`, `../`) and `docker://` images (GHA030) are out of scope.
11
+ * Owners in the trusted allowlist are exempt.
12
+ */
13
+
14
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
15
+ import { getPrimaryOutput, extractActionRefs, parseActionUses } from "./yaml-helpers";
16
+ import { TRUSTED_ACTION_OWNERS } from "../rules/data/trusted-action-owners";
17
+
18
+ const SHA_RE = /^[0-9a-f]{40}$/;
19
+
20
+ export interface UnpinnedRef {
21
+ job: string;
22
+ ref: string;
23
+ slug: string;
24
+ }
25
+
26
+ /**
27
+ * Find every action/reusable-workflow `uses:` that is not pinned to a commit
28
+ * SHA. Exposed as a pure function so the allowlist behaviour is testable.
29
+ */
30
+ export function findUnpinnedActions(
31
+ yaml: string,
32
+ trustedOwners: ReadonlySet<string> = TRUSTED_ACTION_OWNERS,
33
+ ): UnpinnedRef[] {
34
+ const result: UnpinnedRef[] = [];
35
+ for (const { job, ref } of extractActionRefs(yaml)) {
36
+ const parsed = parseActionUses(ref);
37
+ if (!parsed) continue; // local or docker:// reference
38
+ if (parsed.slug === "actions/checkout") continue; // owned by GHA021
39
+ if (trustedOwners.has(parsed.owner)) continue;
40
+ if (SHA_RE.test(parsed.gitRef)) continue; // already pinned
41
+ result.push({ job, ref, slug: parsed.slug });
42
+ }
43
+ return result;
44
+ }
45
+
46
+ export const gha029: PostSynthCheck = {
47
+ id: "GHA029",
48
+ description: "Action or reusable workflow not pinned to a commit SHA",
49
+
50
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
51
+ const diagnostics: PostSynthDiagnostic[] = [];
52
+
53
+ for (const [, output] of ctx.outputs) {
54
+ const yaml = getPrimaryOutput(output);
55
+ for (const { job, ref } of findUnpinnedActions(yaml)) {
56
+ diagnostics.push({
57
+ checkId: "GHA029",
58
+ severity: "warning",
59
+ message: `Job "${job}" uses ${ref} pinned to a tag or branch — pin to a full commit SHA for supply-chain security.`,
60
+ entity: job,
61
+ lexicon: "github",
62
+ });
63
+ }
64
+ }
65
+
66
+ return diagnostics;
67
+ },
68
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * GHA030: Container Image Without an Immutable Digest
3
+ *
4
+ * Flags job `container:` images, `services:` images, and `docker://` step
5
+ * references that are not pinned to an immutable `@sha256:` digest. A mutable
6
+ * tag (`:latest`, `:20`) can be repointed to a different image after review.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, extractImageRefs } from "./yaml-helpers";
11
+
12
+ const SOURCE_LABEL: Record<string, string> = {
13
+ container: "container image",
14
+ service: "service image",
15
+ step: "docker:// image",
16
+ };
17
+
18
+ export const gha030: PostSynthCheck = {
19
+ id: "GHA030",
20
+ description: "Container image not pinned to an immutable digest",
21
+
22
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
23
+ const diagnostics: PostSynthDiagnostic[] = [];
24
+
25
+ for (const [, output] of ctx.outputs) {
26
+ const yaml = getPrimaryOutput(output);
27
+ for (const { job, image, source } of extractImageRefs(yaml)) {
28
+ if (image.includes("@sha256:")) continue;
29
+ diagnostics.push({
30
+ checkId: "GHA030",
31
+ severity: "warning",
32
+ message: `Job "${job}" ${SOURCE_LABEL[source]} "${image}" is not pinned to a digest — reference it by @sha256:... so the image cannot change after review.`,
33
+ entity: job,
34
+ lexicon: "github",
35
+ });
36
+ }
37
+ }
38
+
39
+ return diagnostics;
40
+ },
41
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * GHA031: Action Reference Resembling a Well-Known Action
3
+ *
4
+ * Flags a `uses:` slug that is a near-miss (edit distance 1–2) of a popular
5
+ * action but not an exact match — a likely typo or a deliberate impersonation
6
+ * of the well-known action under different ownership. Advisory only; backed by
7
+ * a vendored reference list.
8
+ */
9
+
10
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
+ import { getPrimaryOutput, extractActionRefs, parseActionUses } from "./yaml-helpers";
12
+ import { KNOWN_ACTION_SLUGS } from "../rules/data/known-action-slugs";
13
+
14
+ const KNOWN = new Set(KNOWN_ACTION_SLUGS);
15
+
16
+ /** Levenshtein edit distance, capped: returns early once it exceeds `max`. */
17
+ export function editDistance(a: string, b: string, max: number): number {
18
+ if (a === b) return 0;
19
+ if (Math.abs(a.length - b.length) > max) return max + 1;
20
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
21
+ for (let i = 1; i <= a.length; i++) {
22
+ const curr = [i];
23
+ let rowMin = i;
24
+ for (let j = 1; j <= b.length; j++) {
25
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
26
+ const val = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
27
+ curr.push(val);
28
+ if (val < rowMin) rowMin = val;
29
+ }
30
+ if (rowMin > max) return max + 1;
31
+ prev = curr;
32
+ }
33
+ return prev[b.length];
34
+ }
35
+
36
+ /** Find the nearest known slug within edit distance 1–2 of `slug`, if any. */
37
+ export function nearestLookAlike(slug: string): string | undefined {
38
+ if (KNOWN.has(slug)) return undefined; // exact match is legitimate
39
+ let best: string | undefined;
40
+ let bestDist = 3;
41
+ for (const known of KNOWN) {
42
+ const d = editDistance(slug, known, 2);
43
+ if (d >= 1 && d <= 2 && d < bestDist) {
44
+ best = known;
45
+ bestDist = d;
46
+ }
47
+ }
48
+ return best;
49
+ }
50
+
51
+ export const gha031: PostSynthCheck = {
52
+ id: "GHA031",
53
+ description: "Action reference resembles a well-known action (possible impersonation)",
54
+
55
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
56
+ const diagnostics: PostSynthDiagnostic[] = [];
57
+
58
+ for (const [, output] of ctx.outputs) {
59
+ const yaml = getPrimaryOutput(output);
60
+ for (const { job, ref } of extractActionRefs(yaml)) {
61
+ const parsed = parseActionUses(ref);
62
+ if (!parsed) continue;
63
+ const lookAlike = nearestLookAlike(parsed.slug);
64
+ if (!lookAlike) continue;
65
+ diagnostics.push({
66
+ checkId: "GHA031",
67
+ severity: "warning",
68
+ message: `Job "${job}" uses "${parsed.slug}", which closely resembles the well-known action "${lookAlike}". Confirm the owner is who you intend — this may be a typo or impersonation.`,
69
+ entity: job,
70
+ lexicon: "github",
71
+ });
72
+ }
73
+ }
74
+
75
+ return diagnostics;
76
+ },
77
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * GHA032: Reference to an Archived or Compromised Action
3
+ *
4
+ * Flags a `uses:` slug that a vendored snapshot marks as archived/abandoned or
5
+ * carrying a disclosed security issue. Advisory only — the dataset is
6
+ * necessarily incomplete and may lag upstream changes.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, extractActionRefs, parseActionUses } from "./yaml-helpers";
11
+ import { FLAGGED_ACTIONS } from "../rules/data/flagged-actions";
12
+
13
+ export const gha032: PostSynthCheck = {
14
+ id: "GHA032",
15
+ description: "Action is archived/abandoned or has a disclosed security issue",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+
20
+ for (const [, output] of ctx.outputs) {
21
+ const yaml = getPrimaryOutput(output);
22
+ for (const { job, ref } of extractActionRefs(yaml)) {
23
+ const parsed = parseActionUses(ref);
24
+ if (!parsed) continue;
25
+ const flagged = FLAGGED_ACTIONS[parsed.slug];
26
+ if (!flagged) continue;
27
+ diagnostics.push({
28
+ checkId: "GHA032",
29
+ severity: "warning",
30
+ message: `Job "${job}" uses "${parsed.slug}" — ${flagged.reason}. ${flagged.remediation}.`,
31
+ entity: job,
32
+ lexicon: "github",
33
+ });
34
+ }
35
+ }
36
+
37
+ return diagnostics;
38
+ },
39
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * GHA033: Blanket `write-all` Token Permissions
3
+ *
4
+ * Flags a `permissions: write-all` grant at the workflow or job level. It hands
5
+ * the `GITHUB_TOKEN` every write scope regardless of what the job uses, so any
6
+ * compromised or injected step becomes a write-capable actor across the repo.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, extractWorkflowPermissions, extractJobPermissions, writeSurface } from "./yaml-helpers";
11
+
12
+ export const gha033: PostSynthCheck = {
13
+ id: "GHA033",
14
+ description: "Blanket write-all token permissions",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [, output] of ctx.outputs) {
20
+ const yaml = getPrimaryOutput(output);
21
+
22
+ const wf = extractWorkflowPermissions(yaml);
23
+ if (wf && writeSurface(wf).writeAll) {
24
+ diagnostics.push({
25
+ checkId: "GHA033",
26
+ severity: "warning",
27
+ message: `Workflow declares permissions: write-all — replace it with the specific scopes the jobs actually need (least privilege).`,
28
+ lexicon: "github",
29
+ });
30
+ }
31
+
32
+ for (const [job, perms] of extractJobPermissions(yaml)) {
33
+ if (writeSurface(perms).writeAll) {
34
+ diagnostics.push({
35
+ checkId: "GHA033",
36
+ severity: "warning",
37
+ message: `Job "${job}" declares permissions: write-all — replace it with the specific scopes this job needs (least privilege).`,
38
+ entity: job,
39
+ lexicon: "github",
40
+ });
41
+ }
42
+ }
43
+ }
44
+
45
+ return diagnostics;
46
+ },
47
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * GHA034: Workflow-Wide Write Scope (Prefer Job-Scoped Permissions)
3
+ *
4
+ * Flags individual write scopes granted at the workflow level. A workflow-wide
5
+ * grant gives every job that scope even when only one needs it. Least privilege
6
+ * prefers declaring the write scope on the single job that uses it.
7
+ *
8
+ * The `write-all` blanket preset is handled by GHA033 and skipped here.
9
+ */
10
+
11
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
12
+ import { getPrimaryOutput, extractWorkflowPermissions, writeSurface } from "./yaml-helpers";
13
+
14
+ export const gha034: PostSynthCheck = {
15
+ id: "GHA034",
16
+ description: "Write permissions granted workflow-wide instead of per-job",
17
+
18
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
19
+ const diagnostics: PostSynthDiagnostic[] = [];
20
+
21
+ for (const [, output] of ctx.outputs) {
22
+ const yaml = getPrimaryOutput(output);
23
+
24
+ const wf = extractWorkflowPermissions(yaml);
25
+ if (!wf) continue;
26
+
27
+ const { writeAll, scopes } = writeSurface(wf);
28
+ if (writeAll) continue; // GHA033 owns write-all
29
+ if (scopes.length === 0) continue;
30
+
31
+ diagnostics.push({
32
+ checkId: "GHA034",
33
+ severity: "warning",
34
+ message: `Workflow grants write scope (${scopes.join(", ")}) for all jobs — move each write scope onto the specific job that needs it for least privilege.`,
35
+ lexicon: "github",
36
+ });
37
+ }
38
+
39
+ return diagnostics;
40
+ },
41
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * GHA035: Elevated Token Scope on an Untrusted-Code Trigger
3
+ *
4
+ * Raises the stakes of GHA033/GHA034: when a workflow can run untrusted code
5
+ * (`pull_request_target`, `workflow_run`) and also grants the token write
6
+ * access, an injected step runs with standing write credentials. This is the
7
+ * combination behind most CI privilege-escalation incidents, so it is an error.
8
+ *
9
+ * Cross-references the trust-boundary checks (GHA018 / GHA025 / GHA038).
10
+ */
11
+
12
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
13
+ import {
14
+ getPrimaryOutput,
15
+ extractTriggers,
16
+ extractWorkflowPermissions,
17
+ extractJobPermissions,
18
+ writeSurface,
19
+ grantsWrite,
20
+ } from "./yaml-helpers";
21
+
22
+ const UNTRUSTED_TRIGGERS = ["pull_request_target", "workflow_run"];
23
+
24
+ function describeWrite(perms: ReturnType<typeof writeSurface>): string {
25
+ return perms.writeAll ? "write-all" : perms.scopes.map((s) => `${s}: write`).join(", ");
26
+ }
27
+
28
+ export const gha035: PostSynthCheck = {
29
+ id: "GHA035",
30
+ description: "Elevated token scope on a trigger that can run untrusted code",
31
+
32
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
33
+ const diagnostics: PostSynthDiagnostic[] = [];
34
+
35
+ for (const [, output] of ctx.outputs) {
36
+ const yaml = getPrimaryOutput(output);
37
+ const triggers = extractTriggers(yaml);
38
+ const untrusted = UNTRUSTED_TRIGGERS.filter((t) => triggers[t]);
39
+ if (untrusted.length === 0) continue;
40
+
41
+ const wf = extractWorkflowPermissions(yaml);
42
+ if (wf && grantsWrite(wf)) {
43
+ diagnostics.push({
44
+ checkId: "GHA035",
45
+ severity: "error",
46
+ message: `Workflow grants write token scope (${describeWrite(writeSurface(wf))}) while using the ${untrusted.join("/")} trigger, which can run untrusted code. Drop write scope or gate the privileged work behind a separate trusted workflow.`,
47
+ lexicon: "github",
48
+ });
49
+ }
50
+
51
+ for (const [job, perms] of extractJobPermissions(yaml)) {
52
+ if (grantsWrite(perms)) {
53
+ diagnostics.push({
54
+ checkId: "GHA035",
55
+ severity: "error",
56
+ message: `Job "${job}" grants write token scope (${describeWrite(writeSurface(perms))}) while the workflow uses the ${untrusted.join("/")} trigger, which can run untrusted code. Drop write scope or isolate the privileged work.`,
57
+ entity: job,
58
+ lexicon: "github",
59
+ });
60
+ }
61
+ }
62
+ }
63
+
64
+ return diagnostics;
65
+ },
66
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * GHA036: Untrusted Input Interpolated into a Shell Command
3
+ *
4
+ * Flags attacker-controllable expression contexts (PR titles, branch names,
5
+ * issue/comment bodies, commit messages) interpolated directly into a `run:`
6
+ * script. The value is expanded into the shell before the script runs, so a
7
+ * crafted payload becomes command execution. Pass untrusted input through an
8
+ * `env:` variable and reference it quoted instead.
9
+ */
10
+
11
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
12
+ import { getPrimaryOutput, extractRunBlocks, extractExpressions } from "./yaml-helpers";
13
+ import { matchUntrustedContext } from "../rules/data/untrusted-contexts";
14
+
15
+ export const gha036: PostSynthCheck = {
16
+ id: "GHA036",
17
+ description: "Untrusted input interpolated into a run: shell command",
18
+
19
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
20
+ const diagnostics: PostSynthDiagnostic[] = [];
21
+
22
+ for (const [, output] of ctx.outputs) {
23
+ const yaml = getPrimaryOutput(output);
24
+ for (const { job, run } of extractRunBlocks(yaml)) {
25
+ for (const expr of extractExpressions(run)) {
26
+ const ctxName = matchUntrustedContext(expr);
27
+ if (!ctxName) continue;
28
+ diagnostics.push({
29
+ checkId: "GHA036",
30
+ severity: "error",
31
+ message: `Job "${job}" interpolates untrusted input \${{ ${ctxName} ... }} into a run: script — this is a script-injection sink. Pass it through an env: variable and reference "$VAR" quoted instead.`,
32
+ entity: job,
33
+ lexicon: "github",
34
+ });
35
+ break; // one diagnostic per run block is enough
36
+ }
37
+ }
38
+ }
39
+
40
+ return diagnostics;
41
+ },
42
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * GHA037: Untrusted Input Written to GITHUB_ENV / GITHUB_PATH
3
+ *
4
+ * Flags a `run:` step that writes attacker-controllable input into the
5
+ * `$GITHUB_ENV` or `$GITHUB_PATH` files. Those files set environment variables
6
+ * and PATH entries for *later* steps, so injecting into them escalates a data
7
+ * value into influence over subsequent privileged steps (a known path to
8
+ * `LD_PRELOAD`/`NODE_OPTIONS`-style takeover).
9
+ */
10
+
11
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
12
+ import { getPrimaryOutput, extractRunBlocks, extractExpressions } from "./yaml-helpers";
13
+ import { matchUntrustedContext } from "../rules/data/untrusted-contexts";
14
+
15
+ const ENV_FILE_RE = /GITHUB_ENV|GITHUB_PATH/;
16
+
17
+ export const gha037: PostSynthCheck = {
18
+ id: "GHA037",
19
+ description: "Untrusted input written to GITHUB_ENV / GITHUB_PATH",
20
+
21
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
22
+ const diagnostics: PostSynthDiagnostic[] = [];
23
+
24
+ for (const [, output] of ctx.outputs) {
25
+ const yaml = getPrimaryOutput(output);
26
+ for (const { job, run } of extractRunBlocks(yaml)) {
27
+ if (!ENV_FILE_RE.test(run)) continue;
28
+ for (const expr of extractExpressions(run)) {
29
+ const ctxName = matchUntrustedContext(expr);
30
+ if (!ctxName) continue;
31
+ const file = run.includes("GITHUB_PATH") ? "GITHUB_PATH" : "GITHUB_ENV";
32
+ diagnostics.push({
33
+ checkId: "GHA037",
34
+ severity: "error",
35
+ message: `Job "${job}" writes untrusted input \${{ ${ctxName} ... }} into $${file}, which sets state for later steps. Sanitize/validate the value or avoid persisting untrusted input across steps.`,
36
+ entity: job,
37
+ lexicon: "github",
38
+ });
39
+ break;
40
+ }
41
+ }
42
+ }
43
+
44
+ return diagnostics;
45
+ },
46
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * GHA038: Privileged `workflow_run` Trigger Checking Out Untrusted Code
3
+ *
4
+ * Generalizes GHA018 (pull_request_target + checkout) to the other privileged
5
+ * trigger that runs in the base-repo context: `workflow_run`. A `workflow_run`
6
+ * workflow has repo write scope and secrets, and checking out the artifact/head
7
+ * of the triggering run pulls untrusted code into that privileged context.
8
+ */
9
+
10
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
+ import { getPrimaryOutput, extractTriggers, extractJobs, hasCheckoutAction } from "./yaml-helpers";
12
+
13
+ export const gha038: PostSynthCheck = {
14
+ id: "GHA038",
15
+ description: "workflow_run trigger with checkout runs untrusted code in a privileged context",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+
20
+ for (const [, output] of ctx.outputs) {
21
+ const yaml = getPrimaryOutput(output);
22
+ const triggers = extractTriggers(yaml);
23
+ if (!triggers["workflow_run"]) continue;
24
+
25
+ const jobs = extractJobs(yaml);
26
+ for (const [jobName, job] of jobs) {
27
+ if (job.steps && hasCheckoutAction(job.steps)) {
28
+ diagnostics.push({
29
+ checkId: "GHA038",
30
+ severity: "warning",
31
+ message: `Job "${jobName}" checks out code under a workflow_run trigger, which runs with repo write scope and secrets. Treat the checked-out code as untrusted — avoid executing it, or move execution to an unprivileged workflow.`,
32
+ entity: jobName,
33
+ lexicon: "github",
34
+ });
35
+ }
36
+ }
37
+ }
38
+
39
+ return diagnostics;
40
+ },
41
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * GHA039: Spoofable Identity Used as an Authorization Gate
3
+ *
4
+ * Flags an `if:` condition that gates execution on a commit-author identity
5
+ * field (`...author.name` / `...author.email`). Those values come straight from
6
+ * git metadata the committer sets freely, so an attacker can forge them to pass
7
+ * the gate. Use a trustworthy signal — environment protection rules, CODEOWNERS,
8
+ * or membership checks against a verified actor.
9
+ */
10
+
11
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
12
+ import { getPrimaryOutput, extractIfConditions } from "./yaml-helpers";
13
+
14
+ const SPOOFABLE_FIELD_RE = /\.author\.(name|email)\b/;
15
+ const COMPARISON_RE = /==|!=|contains\s*\(/;
16
+
17
+ export const gha039: PostSynthCheck = {
18
+ id: "GHA039",
19
+ description: "Authorization gate on a spoofable commit-author identity field",
20
+
21
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
22
+ const diagnostics: PostSynthDiagnostic[] = [];
23
+
24
+ for (const [, output] of ctx.outputs) {
25
+ const yaml = getPrimaryOutput(output);
26
+ for (const { job, expr } of extractIfConditions(yaml)) {
27
+ if (SPOOFABLE_FIELD_RE.test(expr) && COMPARISON_RE.test(expr)) {
28
+ diagnostics.push({
29
+ checkId: "GHA039",
30
+ severity: "warning",
31
+ message: `Job "${job}" gates on a commit-author identity field in an if: condition (${expr}). Author name/email are attacker-controlled and can be spoofed — gate on a verified signal (environment protection, CODEOWNERS, verified actor) instead.`,
32
+ entity: job,
33
+ lexicon: "github",
34
+ });
35
+ }
36
+ }
37
+ }
38
+
39
+ return diagnostics;
40
+ },
41
+ };