@intentius/chant-lexicon-github 0.49.0 → 0.50.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.
- package/dist/components/generate-op-pipeline.d.ts +81 -0
- package/dist/components/generate-op-pipeline.d.ts.map +1 -0
- package/dist/integrity.json +15 -5
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/gha059.d.ts +40 -0
- package/dist/lint/post-synth/gha059.d.ts.map +1 -0
- package/dist/lint/post-synth/gha060.d.ts +34 -0
- package/dist/lint/post-synth/gha060.d.ts.map +1 -0
- package/dist/lint/post-synth/gha061.d.ts +27 -0
- package/dist/lint/post-synth/gha061.d.ts.map +1 -0
- package/dist/lint/post-synth/gha062.d.ts +24 -0
- package/dist/lint/post-synth/gha062.d.ts.map +1 -0
- package/dist/lint/post-synth/gha063.d.ts +12 -0
- package/dist/lint/post-synth/gha063.d.ts.map +1 -0
- package/dist/lint/post-synth/gha064.d.ts +12 -0
- package/dist/lint/post-synth/gha064.d.ts.map +1 -0
- package/dist/lint/post-synth/gha065.d.ts +19 -0
- package/dist/lint/post-synth/gha065.d.ts.map +1 -0
- package/dist/lint/post-synth/gha066.d.ts +12 -0
- package/dist/lint/post-synth/gha066.d.ts.map +1 -0
- package/dist/lint/post-synth/gha067.d.ts +12 -0
- package/dist/lint/post-synth/gha067.d.ts.map +1 -0
- package/dist/lint/post-synth/gha068.d.ts +13 -0
- package/dist/lint/post-synth/gha068.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/yaml-helpers.d.ts +20 -0
- package/dist/lint/post-synth/yaml-helpers.d.ts.map +1 -1
- package/dist/lint/rules/data/action-usage-policy.d.ts +30 -0
- package/dist/lint/rules/data/action-usage-policy.d.ts.map +1 -0
- package/dist/lint/rules/data/advisory-feed.d.ts +36 -0
- package/dist/lint/rules/data/advisory-feed.d.ts.map +1 -0
- package/dist/manifest.json +109 -1
- package/dist/meta.json +5 -0
- package/dist/okf/index.md +10 -0
- package/dist/okf/rules/GHA059.md +15 -0
- package/dist/okf/rules/GHA060.md +15 -0
- package/dist/okf/rules/GHA061.md +15 -0
- package/dist/okf/rules/GHA062.md +15 -0
- package/dist/okf/rules/GHA063.md +15 -0
- package/dist/okf/rules/GHA064.md +15 -0
- package/dist/okf/rules/GHA065.md +15 -0
- package/dist/okf/rules/GHA066.md +15 -0
- package/dist/okf/rules/GHA067.md +15 -0
- package/dist/okf/rules/GHA068.md +15 -0
- package/dist/okf/types/Job.md +9 -0
- package/dist/okf/types/Workflow.md +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/gha059.ts +132 -0
- package/dist/rules/gha060.ts +146 -0
- package/dist/rules/gha061.ts +85 -0
- package/dist/rules/gha062.ts +68 -0
- package/dist/rules/gha063.ts +60 -0
- package/dist/rules/gha064.ts +55 -0
- package/dist/rules/gha065.ts +129 -0
- package/dist/rules/gha066.ts +47 -0
- package/dist/rules/gha067.ts +76 -0
- package/dist/rules/gha068.ts +51 -0
- package/dist/rules/yaml-helpers.ts +37 -2
- package/dist/types/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/codegen/docs.ts +1 -1
- package/src/components/generate-op-pipeline.test.ts +146 -0
- package/src/components/generate-op-pipeline.ts +183 -0
- package/src/generated/index.d.ts +1 -0
- package/src/generated/lexicon-github.json +5 -0
- package/src/lint/audit-catalog.ts +16 -1
- package/src/lint/post-synth/gha059.test.ts +147 -0
- package/src/lint/post-synth/gha059.ts +132 -0
- package/src/lint/post-synth/gha060.test.ts +144 -0
- package/src/lint/post-synth/gha060.ts +146 -0
- package/src/lint/post-synth/gha061.test.ts +85 -0
- package/src/lint/post-synth/gha061.ts +85 -0
- package/src/lint/post-synth/gha062.test.ts +88 -0
- package/src/lint/post-synth/gha062.ts +68 -0
- package/src/lint/post-synth/gha063.test.ts +86 -0
- package/src/lint/post-synth/gha063.ts +60 -0
- package/src/lint/post-synth/gha064.test.ts +73 -0
- package/src/lint/post-synth/gha064.ts +55 -0
- package/src/lint/post-synth/gha065.test.ts +85 -0
- package/src/lint/post-synth/gha065.ts +129 -0
- package/src/lint/post-synth/gha066.test.ts +66 -0
- package/src/lint/post-synth/gha066.ts +47 -0
- package/src/lint/post-synth/gha067.test.ts +85 -0
- package/src/lint/post-synth/gha067.ts +76 -0
- package/src/lint/post-synth/gha068.test.ts +71 -0
- package/src/lint/post-synth/gha068.ts +51 -0
- package/src/lint/post-synth/index.ts +20 -0
- package/src/lint/post-synth/yaml-helpers.ts +37 -2
- package/src/lint/rules/data/action-usage-policy.ts +31 -0
- package/src/lint/rules/data/advisory-feed.ts +38 -0
- package/src/plugin.test.ts +24 -3
- package/src/plugin.ts +28 -0
package/dist/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"chantVersion": ">=0.1.0",
|
|
5
5
|
"namespace": "GitHub",
|
|
6
6
|
"intrinsics": [
|
|
@@ -9,6 +9,114 @@
|
|
|
9
9
|
"description": "Typed class wrapping a raw GitHub Actions expression, serializing to `${{ }}` (chant #1069 — was registered as \"expression\", which src/index.ts never exported)",
|
|
10
10
|
"outputKey": "expression",
|
|
11
11
|
"isTag": false
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "secrets",
|
|
15
|
+
"description": "Access a secret by name.",
|
|
16
|
+
"isTag": false,
|
|
17
|
+
"foldsEagerly": true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "matrix",
|
|
21
|
+
"description": "Access a matrix value by key.",
|
|
22
|
+
"isTag": false,
|
|
23
|
+
"foldsEagerly": true
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "steps",
|
|
27
|
+
"description": "Access a step output.",
|
|
28
|
+
"isTag": false,
|
|
29
|
+
"foldsEagerly": true
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "needs",
|
|
33
|
+
"description": "Access a job output from a needed job.",
|
|
34
|
+
"isTag": false,
|
|
35
|
+
"foldsEagerly": true
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "inputs",
|
|
39
|
+
"description": "Access a workflow input.",
|
|
40
|
+
"isTag": false,
|
|
41
|
+
"foldsEagerly": true
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "vars",
|
|
45
|
+
"description": "Access a configuration variable.",
|
|
46
|
+
"isTag": false,
|
|
47
|
+
"foldsEagerly": true
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "env",
|
|
51
|
+
"description": "Access an environment variable.",
|
|
52
|
+
"isTag": false,
|
|
53
|
+
"foldsEagerly": true
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "always",
|
|
57
|
+
"description": "Always run, regardless of status.",
|
|
58
|
+
"isTag": false,
|
|
59
|
+
"foldsEagerly": true
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "failure",
|
|
63
|
+
"description": "Run only if a previous step has failed.",
|
|
64
|
+
"isTag": false,
|
|
65
|
+
"foldsEagerly": true
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "success",
|
|
69
|
+
"description": "Run only if all previous steps succeeded.",
|
|
70
|
+
"isTag": false,
|
|
71
|
+
"foldsEagerly": true
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "cancelled",
|
|
75
|
+
"description": "Run only if the workflow was cancelled.",
|
|
76
|
+
"isTag": false,
|
|
77
|
+
"foldsEagerly": true
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "contains",
|
|
81
|
+
"description": "Check if a string contains a substring.",
|
|
82
|
+
"isTag": false,
|
|
83
|
+
"foldsEagerly": true
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "startsWith",
|
|
87
|
+
"description": "Check if a string starts with a prefix.",
|
|
88
|
+
"isTag": false,
|
|
89
|
+
"foldsEagerly": true
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "toJSON",
|
|
93
|
+
"description": "Convert a value to JSON.",
|
|
94
|
+
"isTag": false,
|
|
95
|
+
"foldsEagerly": true
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "fromJSON",
|
|
99
|
+
"description": "Parse a JSON string.",
|
|
100
|
+
"isTag": false,
|
|
101
|
+
"foldsEagerly": true
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "format",
|
|
105
|
+
"description": "Format a string with placeholders.",
|
|
106
|
+
"isTag": false,
|
|
107
|
+
"foldsEagerly": true
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"name": "branch",
|
|
111
|
+
"description": "Check if the ref matches a branch name.",
|
|
112
|
+
"isTag": false,
|
|
113
|
+
"foldsEagerly": true
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "tag",
|
|
117
|
+
"description": "Check if the ref matches a tag prefix.",
|
|
118
|
+
"isTag": false,
|
|
119
|
+
"foldsEagerly": true
|
|
12
120
|
}
|
|
13
121
|
],
|
|
14
122
|
"pseudoParameters": {}
|
package/dist/meta.json
CHANGED
package/dist/okf/index.md
CHANGED
|
@@ -68,3 +68,13 @@ okf_version: '0.2'
|
|
|
68
68
|
* [GHA056](/rules/GHA056.md) - Workflow without a name
|
|
69
69
|
* [GHA057](/rules/GHA057.md) - Dependency update allows executing untrusted external code
|
|
70
70
|
* [GHA058](/rules/GHA058.md) - Dependency update has no cooldown window
|
|
71
|
+
* [GHA059](/rules/GHA059.md) - SHA-pinned action reference has a missing or internally-inconsistent version annotation
|
|
72
|
+
* [GHA060](/rules/GHA060.md) - Generated GitHub App token granted broader scope than its consuming steps evidence
|
|
73
|
+
* [GHA061](/rules/GHA061.md) - Action reference outside the configured usage policy (opt-in)
|
|
74
|
+
* [GHA062](/rules/GHA062.md) - Pinned action reference matches a known-vulnerability advisory (feed-driven)
|
|
75
|
+
* [GHA063](/rules/GHA063.md) - Dependency setup action without caching enabled
|
|
76
|
+
* [GHA064](/rules/GHA064.md) - Job hardcoded onto an expensive runner with no sign it needs that OS
|
|
77
|
+
* [GHA065](/rules/GHA065.md) - Matrix combines dimensions into an uncapped, large fan-out
|
|
78
|
+
* [GHA066](/rules/GHA066.md) - Uploaded artifact has no explicit retention-days
|
|
79
|
+
* [GHA067](/rules/GHA067.md) - Unconditional docker build with no path filter or guard
|
|
80
|
+
* [GHA068](/rules/GHA068.md) - Pull-request workflow missing a concurrency group
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: post-synth-check
|
|
3
|
+
title: GHA059
|
|
4
|
+
description: SHA-pinned action reference has a missing or internally-inconsistent version annotation
|
|
5
|
+
id: GHA059
|
|
6
|
+
severity: error
|
|
7
|
+
category: post-synth
|
|
8
|
+
lexicon: github
|
|
9
|
+
docs: https://intentius.io/chant/lexicons/github/rules/
|
|
10
|
+
---
|
|
11
|
+
SHA-pinned action reference has a missing or internally-inconsistent version annotation
|
|
12
|
+
|
|
13
|
+
## Applies to
|
|
14
|
+
|
|
15
|
+
- [Job](/types/Job.md)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: post-synth-check
|
|
3
|
+
title: GHA060
|
|
4
|
+
description: Generated GitHub App token granted broader scope than its consuming steps evidence
|
|
5
|
+
id: GHA060
|
|
6
|
+
severity: error
|
|
7
|
+
category: post-synth
|
|
8
|
+
lexicon: github
|
|
9
|
+
docs: https://intentius.io/chant/lexicons/github/rules/
|
|
10
|
+
---
|
|
11
|
+
Generated GitHub App token granted broader scope than its consuming steps evidence
|
|
12
|
+
|
|
13
|
+
## Applies to
|
|
14
|
+
|
|
15
|
+
- [Job](/types/Job.md)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: post-synth-check
|
|
3
|
+
title: GHA061
|
|
4
|
+
description: Action reference outside the configured usage policy (opt-in)
|
|
5
|
+
id: GHA061
|
|
6
|
+
severity: error
|
|
7
|
+
category: post-synth
|
|
8
|
+
lexicon: github
|
|
9
|
+
docs: https://intentius.io/chant/lexicons/github/rules/
|
|
10
|
+
---
|
|
11
|
+
Action reference outside the configured usage policy (opt-in)
|
|
12
|
+
|
|
13
|
+
## Applies to
|
|
14
|
+
|
|
15
|
+
- [Job](/types/Job.md)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: post-synth-check
|
|
3
|
+
title: GHA062
|
|
4
|
+
description: Pinned action reference matches a known-vulnerability advisory (feed-driven)
|
|
5
|
+
id: GHA062
|
|
6
|
+
severity: error
|
|
7
|
+
category: post-synth
|
|
8
|
+
lexicon: github
|
|
9
|
+
docs: https://intentius.io/chant/lexicons/github/rules/
|
|
10
|
+
---
|
|
11
|
+
Pinned action reference matches a known-vulnerability advisory (feed-driven)
|
|
12
|
+
|
|
13
|
+
## Applies to
|
|
14
|
+
|
|
15
|
+
- [Job](/types/Job.md)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: post-synth-check
|
|
3
|
+
title: GHA063
|
|
4
|
+
description: Dependency setup action without caching enabled
|
|
5
|
+
id: GHA063
|
|
6
|
+
severity: error
|
|
7
|
+
category: post-synth
|
|
8
|
+
lexicon: github
|
|
9
|
+
docs: https://intentius.io/chant/lexicons/github/rules/
|
|
10
|
+
---
|
|
11
|
+
Dependency setup action without caching enabled
|
|
12
|
+
|
|
13
|
+
## Applies to
|
|
14
|
+
|
|
15
|
+
- [Job](/types/Job.md)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: post-synth-check
|
|
3
|
+
title: GHA064
|
|
4
|
+
description: Job hardcoded onto an expensive runner with no sign it needs that OS
|
|
5
|
+
id: GHA064
|
|
6
|
+
severity: error
|
|
7
|
+
category: post-synth
|
|
8
|
+
lexicon: github
|
|
9
|
+
docs: https://intentius.io/chant/lexicons/github/rules/
|
|
10
|
+
---
|
|
11
|
+
Job hardcoded onto an expensive runner with no sign it needs that OS
|
|
12
|
+
|
|
13
|
+
## Applies to
|
|
14
|
+
|
|
15
|
+
- [Job](/types/Job.md)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: post-synth-check
|
|
3
|
+
title: GHA065
|
|
4
|
+
description: Matrix combines dimensions into an uncapped, large fan-out
|
|
5
|
+
id: GHA065
|
|
6
|
+
severity: error
|
|
7
|
+
category: post-synth
|
|
8
|
+
lexicon: github
|
|
9
|
+
docs: https://intentius.io/chant/lexicons/github/rules/
|
|
10
|
+
---
|
|
11
|
+
Matrix combines dimensions into an uncapped, large fan-out
|
|
12
|
+
|
|
13
|
+
## Applies to
|
|
14
|
+
|
|
15
|
+
- [Job](/types/Job.md)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: post-synth-check
|
|
3
|
+
title: GHA066
|
|
4
|
+
description: Uploaded artifact has no explicit retention-days
|
|
5
|
+
id: GHA066
|
|
6
|
+
severity: error
|
|
7
|
+
category: post-synth
|
|
8
|
+
lexicon: github
|
|
9
|
+
docs: https://intentius.io/chant/lexicons/github/rules/
|
|
10
|
+
---
|
|
11
|
+
Uploaded artifact has no explicit retention-days
|
|
12
|
+
|
|
13
|
+
## Applies to
|
|
14
|
+
|
|
15
|
+
- [Job](/types/Job.md)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: post-synth-check
|
|
3
|
+
title: GHA067
|
|
4
|
+
description: Unconditional docker build with no path filter or guard
|
|
5
|
+
id: GHA067
|
|
6
|
+
severity: error
|
|
7
|
+
category: post-synth
|
|
8
|
+
lexicon: github
|
|
9
|
+
docs: https://intentius.io/chant/lexicons/github/rules/
|
|
10
|
+
---
|
|
11
|
+
Unconditional docker build with no path filter or guard
|
|
12
|
+
|
|
13
|
+
## Applies to
|
|
14
|
+
|
|
15
|
+
- [Job](/types/Job.md)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: post-synth-check
|
|
3
|
+
title: GHA068
|
|
4
|
+
description: Pull-request workflow missing a concurrency group
|
|
5
|
+
id: GHA068
|
|
6
|
+
severity: error
|
|
7
|
+
category: post-synth
|
|
8
|
+
lexicon: github
|
|
9
|
+
docs: https://intentius.io/chant/lexicons/github/rules/
|
|
10
|
+
---
|
|
11
|
+
Pull-request workflow missing a concurrency group
|
|
12
|
+
|
|
13
|
+
## Applies to
|
|
14
|
+
|
|
15
|
+
- [Workflow](/types/Workflow.md)
|
package/dist/okf/types/Job.md
CHANGED
|
@@ -67,3 +67,12 @@ resource_type: GitHub::Actions::Job
|
|
|
67
67
|
- [GHA053](/rules/GHA053.md): Re-enables unsafe set-env / add-path workflow commands
|
|
68
68
|
- [GHA054](/rules/GHA054.md): Use of a feature with a known security footgun
|
|
69
69
|
- [GHA055](/rules/GHA055.md): Runtime install of a tool already present on the runner
|
|
70
|
+
- [GHA059](/rules/GHA059.md): SHA-pinned action reference has a missing or internally-inconsistent version annotation
|
|
71
|
+
- [GHA060](/rules/GHA060.md): Generated GitHub App token granted broader scope than its consuming steps evidence
|
|
72
|
+
- [GHA061](/rules/GHA061.md): Action reference outside the configured usage policy (opt-in)
|
|
73
|
+
- [GHA062](/rules/GHA062.md): Pinned action reference matches a known-vulnerability advisory (feed-driven)
|
|
74
|
+
- [GHA063](/rules/GHA063.md): Dependency setup action without caching enabled
|
|
75
|
+
- [GHA064](/rules/GHA064.md): Job hardcoded onto an expensive runner with no sign it needs that OS
|
|
76
|
+
- [GHA065](/rules/GHA065.md): Matrix combines dimensions into an uncapped, large fan-out
|
|
77
|
+
- [GHA066](/rules/GHA066.md): Uploaded artifact has no explicit retention-days
|
|
78
|
+
- [GHA067](/rules/GHA067.md): Unconditional docker build with no path filter or guard
|
|
@@ -34,3 +34,4 @@ resource_type: GitHub::Actions::Workflow
|
|
|
34
34
|
- [GHA041](/rules/GHA041.md): Blanket secrets: inherit into a reusable workflow
|
|
35
35
|
- [GHA053](/rules/GHA053.md): Re-enables unsafe set-env / add-path workflow commands
|
|
36
36
|
- [GHA056](/rules/GHA056.md): Workflow without a name
|
|
37
|
+
- [GHA068](/rules/GHA068.md): Pull-request workflow missing a concurrency group
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;AA6B7F,eAAO,MAAM,YAAY,EAAE,aA0U1B,CAAC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA059: Stale or Missing Pin Annotation
|
|
3
|
+
*
|
|
4
|
+
* A ref pinned to a commit SHA is only as trustworthy as the label a reviewer
|
|
5
|
+
* reads next to it — `uses: actions/setup-node@1a2b…9a0b # v4.0.2` lets a
|
|
6
|
+
* reviewer sanity-check the digest without resolving it themselves. This check
|
|
7
|
+
* flags two ways that label goes stale:
|
|
8
|
+
*
|
|
9
|
+
* 1. Missing — a SHA-pinned ref with no trailing version comment at all.
|
|
10
|
+
* 2. Mismatched — inferred without any network call, from internal
|
|
11
|
+
* inconsistency in the workflow itself: the same action is pinned to the
|
|
12
|
+
* same commit SHA in one place and a *different* label in another, or the
|
|
13
|
+
* same label is attached to two *different* commit SHAs. Either shape
|
|
14
|
+
* proves at least one of the labels is wrong — a real digest has exactly
|
|
15
|
+
* one correct human-readable name.
|
|
16
|
+
*
|
|
17
|
+
* Cross-referencing a label against the actual git history (to catch a lone,
|
|
18
|
+
* internally-consistent but still-wrong annotation) needs a network call and
|
|
19
|
+
* is out of scope here — see GHA062 for the feed-based advisory check, which
|
|
20
|
+
* takes the same "no network in the rule itself" stance via an injected feed.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
24
|
+
import { getPrimaryOutput, extractActionRefs, extractUsesComment, parseActionUses } from "./yaml-helpers";
|
|
25
|
+
|
|
26
|
+
const SHA_RE = /^[0-9a-f]{40}$/;
|
|
27
|
+
|
|
28
|
+
export interface StalePinFinding {
|
|
29
|
+
job: string;
|
|
30
|
+
ref: string;
|
|
31
|
+
slug: string;
|
|
32
|
+
sha: string;
|
|
33
|
+
kind: "missing" | "mismatched";
|
|
34
|
+
/** For "mismatched": the other label/SHA this one conflicts with. */
|
|
35
|
+
conflict?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Find every SHA-pinned `uses:` with a missing or internally-inconsistent
|
|
40
|
+
* version annotation. Scoped to one workflow document — cross-file
|
|
41
|
+
* consistency isn't attempted since two independent workflows pinning the
|
|
42
|
+
* same action differently isn't evidence of drift.
|
|
43
|
+
*/
|
|
44
|
+
export function findStalePinAnnotations(yaml: string): StalePinFinding[] {
|
|
45
|
+
const refs = extractActionRefs(yaml)
|
|
46
|
+
.map(({ job, ref }) => ({ job, ref, parsed: parseActionUses(ref) }))
|
|
47
|
+
.filter((r): r is { job: string; ref: string; parsed: NonNullable<ReturnType<typeof parseActionUses>> } => !!r.parsed && SHA_RE.test(r.parsed.gitRef));
|
|
48
|
+
|
|
49
|
+
const findings: StalePinFinding[] = [];
|
|
50
|
+
|
|
51
|
+
// First pass: missing annotations.
|
|
52
|
+
const withComment: Array<{ job: string; ref: string; slug: string; sha: string; comment: string }> = [];
|
|
53
|
+
for (const { job, ref, parsed } of refs) {
|
|
54
|
+
const comment = extractUsesComment(ref);
|
|
55
|
+
if (!comment) {
|
|
56
|
+
findings.push({ job, ref, slug: parsed.slug, sha: parsed.gitRef, kind: "missing" });
|
|
57
|
+
} else {
|
|
58
|
+
withComment.push({ job, ref, slug: parsed.slug, sha: parsed.gitRef, comment });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Second pass: internal mismatch. Group by slug — the same action's
|
|
63
|
+
// commit-SHA↔label mapping must be one-to-one across the whole file.
|
|
64
|
+
const bySlug = new Map<string, Array<{ job: string; ref: string; slug: string; sha: string; comment: string }>>();
|
|
65
|
+
for (const entry of withComment) {
|
|
66
|
+
const list = bySlug.get(entry.slug) ?? [];
|
|
67
|
+
list.push(entry);
|
|
68
|
+
bySlug.set(entry.slug, list);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const [, entries] of bySlug) {
|
|
72
|
+
const shaToComments = new Map<string, Set<string>>();
|
|
73
|
+
const commentToShas = new Map<string, Set<string>>();
|
|
74
|
+
for (const e of entries) {
|
|
75
|
+
(shaToComments.get(e.sha) ?? shaToComments.set(e.sha, new Set()).get(e.sha)!).add(e.comment);
|
|
76
|
+
(commentToShas.get(e.comment) ?? commentToShas.set(e.comment, new Set()).get(e.comment)!).add(e.sha);
|
|
77
|
+
}
|
|
78
|
+
for (const e of entries) {
|
|
79
|
+
const otherComments = [...(shaToComments.get(e.sha) ?? [])].filter((c) => c !== e.comment);
|
|
80
|
+
const otherShas = [...(commentToShas.get(e.comment) ?? [])].filter((s) => s !== e.sha);
|
|
81
|
+
if (otherComments.length > 0) {
|
|
82
|
+
findings.push({
|
|
83
|
+
job: e.job,
|
|
84
|
+
ref: e.ref,
|
|
85
|
+
slug: e.slug,
|
|
86
|
+
sha: e.sha,
|
|
87
|
+
kind: "mismatched",
|
|
88
|
+
conflict: `commit ${e.sha} is also annotated "# ${otherComments[0]}" elsewhere`,
|
|
89
|
+
});
|
|
90
|
+
} else if (otherShas.length > 0) {
|
|
91
|
+
findings.push({
|
|
92
|
+
job: e.job,
|
|
93
|
+
ref: e.ref,
|
|
94
|
+
slug: e.slug,
|
|
95
|
+
sha: e.sha,
|
|
96
|
+
kind: "mismatched",
|
|
97
|
+
conflict: `label "# ${e.comment}" is also attached to commit ${otherShas[0]} elsewhere`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return findings;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const gha059: PostSynthCheck = {
|
|
107
|
+
id: "GHA059",
|
|
108
|
+
description: "SHA-pinned action reference has a missing or internally-inconsistent version annotation",
|
|
109
|
+
|
|
110
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
111
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
112
|
+
|
|
113
|
+
for (const [, output] of ctx.outputs) {
|
|
114
|
+
const yaml = getPrimaryOutput(output);
|
|
115
|
+
for (const finding of findStalePinAnnotations(yaml)) {
|
|
116
|
+
const message =
|
|
117
|
+
finding.kind === "missing"
|
|
118
|
+
? `Job "${finding.job}" pins ${finding.ref} to a commit SHA with no trailing version comment (e.g. "# v4.0.2") — add one so reviewers can sanity-check the digest.`
|
|
119
|
+
: `Job "${finding.job}" pins ${finding.ref} whose annotation is internally inconsistent (${finding.conflict}) — one of these labels no longer matches the digest it's attached to.`;
|
|
120
|
+
diagnostics.push({
|
|
121
|
+
checkId: "GHA059",
|
|
122
|
+
severity: "warning",
|
|
123
|
+
message,
|
|
124
|
+
entity: finding.job,
|
|
125
|
+
lexicon: "github",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return diagnostics;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA060: Over-Scoped Generated Token
|
|
3
|
+
*
|
|
4
|
+
* Flags a GitHub App installation token minted with a `permission-<scope>:
|
|
5
|
+
* write`/`admin` input broader than what the job's consuming steps evidence.
|
|
6
|
+
* Targets the well-known GitHub App token actions (`actions/create-github-app-
|
|
7
|
+
* token`, `tibdex/github-app-token`, `getsentry/action-github-app-token`),
|
|
8
|
+
* which take `permission-<resource>: read|write|admin` inputs and hand back
|
|
9
|
+
* the minted token as a step output.
|
|
10
|
+
*
|
|
11
|
+
* Two ways a generated token is over-scoped:
|
|
12
|
+
* 1. Unused — the token's output (`steps.<id>.outputs.token`) is never
|
|
13
|
+
* referenced by any other step in the job, so every write/admin scope
|
|
14
|
+
* granted is pure waste.
|
|
15
|
+
* 2. No signal — the token IS consumed, but no other step in the job shows
|
|
16
|
+
* any evidence (a `gh`/`git` invocation, an API path, a keyword) of
|
|
17
|
+
* exercising a scope granted `write`/`admin`.
|
|
18
|
+
*
|
|
19
|
+
* "No signal" is a heuristic, not a proof — a step could exercise a scope
|
|
20
|
+
* through machinery this doesn't recognize (a custom script calling `octokit`
|
|
21
|
+
* directly, say). It reports `guidance`, not a `deterministic` fix, for
|
|
22
|
+
* exactly that reason: a human confirms before narrowing the grant.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
26
|
+
import { getPrimaryOutput, extractStepsByJob, parseActionUses } from "./yaml-helpers";
|
|
27
|
+
|
|
28
|
+
const APP_TOKEN_SLUGS = new Set([
|
|
29
|
+
"actions/create-github-app-token",
|
|
30
|
+
"tibdex/github-app-token",
|
|
31
|
+
"getsentry/action-github-app-token",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const WRITE_LEVELS = new Set(["write", "admin"]);
|
|
35
|
+
|
|
36
|
+
/** Textual evidence that a step exercises a given permission scope. Not exhaustive — a fallback covers unlisted scopes. */
|
|
37
|
+
const SCOPE_SIGNALS: Record<string, RegExp> = {
|
|
38
|
+
contents: /git\s+push|gh\s+release|\/(?:git\/)?contents\b/i,
|
|
39
|
+
issues: /gh\s+issue|\/issues\b/i,
|
|
40
|
+
"pull-requests": /gh\s+pr\b|\/pulls\b/i,
|
|
41
|
+
packages: /npm\s+publish|docker\s+push|\/packages\b/i,
|
|
42
|
+
administration: /\/admin\b|administration/i,
|
|
43
|
+
actions: /\/actions\/runs|gh\s+run\b/i,
|
|
44
|
+
checks: /check-runs|\bchecks\b/i,
|
|
45
|
+
deployments: /\/deployments\b/i,
|
|
46
|
+
statuses: /\/statuses\b/i,
|
|
47
|
+
workflows: /\.github\/workflows/i,
|
|
48
|
+
environments: /\/environments\b/i,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function scopeSignalMatches(scope: string, text: string): boolean {
|
|
52
|
+
const signal = SCOPE_SIGNALS[scope];
|
|
53
|
+
if (signal) return signal.test(text);
|
|
54
|
+
return new RegExp(scope.replace(/-/g, "[-_ ]?"), "i").test(text);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function stepText(step: Record<string, unknown>): string {
|
|
58
|
+
const parts: string[] = [];
|
|
59
|
+
if (typeof step.run === "string") parts.push(step.run);
|
|
60
|
+
if (step.with && typeof step.with === "object") parts.push(JSON.stringify(step.with));
|
|
61
|
+
if (step.env && typeof step.env === "object") parts.push(JSON.stringify(step.env));
|
|
62
|
+
if (typeof step.uses === "string") parts.push(step.uses);
|
|
63
|
+
return parts.join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface OverScopedTokenFinding {
|
|
67
|
+
job: string;
|
|
68
|
+
stepId: string;
|
|
69
|
+
scopes: string[];
|
|
70
|
+
reason: "unused" | "no-signal";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Find every generated GitHub App token whose granted write/admin scopes exceed what the job's other steps evidence. */
|
|
74
|
+
export function findOverScopedTokens(yaml: string): OverScopedTokenFinding[] {
|
|
75
|
+
const findings: OverScopedTokenFinding[] = [];
|
|
76
|
+
|
|
77
|
+
for (const [job, steps] of extractStepsByJob(yaml)) {
|
|
78
|
+
steps.forEach((step, idx) => {
|
|
79
|
+
const uses = typeof step.uses === "string" ? step.uses : undefined;
|
|
80
|
+
if (!uses) return;
|
|
81
|
+
const parsed = parseActionUses(uses);
|
|
82
|
+
if (!parsed || !APP_TOKEN_SLUGS.has(parsed.slug)) return;
|
|
83
|
+
|
|
84
|
+
const withBlock = step.with && typeof step.with === "object" ? (step.with as Record<string, unknown>) : {};
|
|
85
|
+
const stepId = typeof step.id === "string" ? step.id : `#${idx}`;
|
|
86
|
+
|
|
87
|
+
const writeScopes: string[] = [];
|
|
88
|
+
for (const [key, value] of Object.entries(withBlock)) {
|
|
89
|
+
const m = /^permission-([a-z-]+)$/i.exec(key);
|
|
90
|
+
if (!m) continue;
|
|
91
|
+
if (WRITE_LEVELS.has(String(value).toLowerCase())) writeScopes.push(m[1]);
|
|
92
|
+
}
|
|
93
|
+
if (writeScopes.length === 0) return;
|
|
94
|
+
|
|
95
|
+
// Evidence comes only from OTHER steps — the minting step's own `with:`
|
|
96
|
+
// block names the scopes being granted, so it can't also count as
|
|
97
|
+
// evidence they're used.
|
|
98
|
+
const otherStepsText = steps
|
|
99
|
+
.filter((_, i) => i !== idx)
|
|
100
|
+
.map(stepText)
|
|
101
|
+
.join("\n");
|
|
102
|
+
|
|
103
|
+
const outputRef = `steps.${stepId}.outputs.token`;
|
|
104
|
+
if (!otherStepsText.includes(outputRef)) {
|
|
105
|
+
findings.push({ job, stepId, scopes: writeScopes, reason: "unused" });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const unsignaled = writeScopes.filter((scope) => !scopeSignalMatches(scope, otherStepsText));
|
|
110
|
+
if (unsignaled.length > 0) {
|
|
111
|
+
findings.push({ job, stepId, scopes: unsignaled, reason: "no-signal" });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return findings;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const gha060: PostSynthCheck = {
|
|
120
|
+
id: "GHA060",
|
|
121
|
+
description: "Generated GitHub App token granted broader scope than its consuming steps evidence",
|
|
122
|
+
|
|
123
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
124
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
125
|
+
|
|
126
|
+
for (const [, output] of ctx.outputs) {
|
|
127
|
+
const yaml = getPrimaryOutput(output);
|
|
128
|
+
for (const finding of findOverScopedTokens(yaml)) {
|
|
129
|
+
const scopeList = finding.scopes.map((s) => `permission-${s}`).join(", ");
|
|
130
|
+
const message =
|
|
131
|
+
finding.reason === "unused"
|
|
132
|
+
? `Job "${finding.job}" step "${finding.stepId}" mints a GitHub App token with ${scopeList} but no other step references its output — remove the unused scope(s) or the token step entirely.`
|
|
133
|
+
: `Job "${finding.job}" step "${finding.stepId}" mints a GitHub App token with ${scopeList}, but no consuming step shows evidence of exercising that scope — narrow the token to the scopes actually used.`;
|
|
134
|
+
diagnostics.push({
|
|
135
|
+
checkId: "GHA060",
|
|
136
|
+
severity: "warning",
|
|
137
|
+
message,
|
|
138
|
+
entity: finding.job,
|
|
139
|
+
lexicon: "github",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return diagnostics;
|
|
145
|
+
},
|
|
146
|
+
};
|