@intentius/chant-lexicon-github 0.1.23 → 0.3.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/integrity.json +34 -4
- package/dist/manifest.json +1 -1
- package/dist/rules/{gha020.ts → gha013.ts} +4 -4
- package/dist/rules/gha029.ts +68 -0
- package/dist/rules/gha030.ts +41 -0
- package/dist/rules/gha031.ts +77 -0
- package/dist/rules/gha032.ts +39 -0
- package/dist/rules/gha033.ts +47 -0
- package/dist/rules/gha034.ts +41 -0
- package/dist/rules/gha035.ts +66 -0
- package/dist/rules/gha036.ts +42 -0
- package/dist/rules/gha037.ts +46 -0
- package/dist/rules/gha038.ts +41 -0
- package/dist/rules/gha039.ts +41 -0
- package/dist/rules/gha040.ts +43 -0
- package/dist/rules/gha041.ts +38 -0
- package/dist/rules/gha042.ts +40 -0
- package/dist/rules/gha043.ts +41 -0
- package/dist/rules/gha044.ts +41 -0
- package/dist/rules/gha045.ts +38 -0
- package/dist/rules/gha046.ts +56 -0
- package/dist/rules/gha047.ts +51 -0
- package/dist/rules/gha048.ts +41 -0
- package/dist/rules/gha049.ts +42 -0
- package/dist/rules/gha050.ts +44 -0
- package/dist/rules/gha051.ts +45 -0
- package/dist/rules/gha052.ts +42 -0
- package/dist/rules/gha053.ts +41 -0
- package/dist/rules/gha054.ts +41 -0
- package/dist/rules/gha055.ts +50 -0
- package/dist/rules/gha056.ts +37 -0
- package/dist/rules/gha057.ts +40 -0
- package/dist/rules/gha058.ts +48 -0
- package/dist/rules/yaml-helpers.ts +335 -0
- package/package.json +1 -1
- package/src/codegen/docs.ts +273 -4
- package/src/composites/dependabot.test.ts +48 -0
- package/src/composites/dependabot.ts +91 -0
- package/src/composites/index.ts +2 -0
- package/src/index.ts +2 -0
- package/src/lint/post-synth/{gha020.test.ts → gha013.test.ts} +7 -7
- package/src/lint/post-synth/{gha020.ts → gha013.ts} +4 -4
- package/src/lint/post-synth/gha029.test.ts +108 -0
- package/src/lint/post-synth/gha029.ts +68 -0
- package/src/lint/post-synth/gha030.test.ts +87 -0
- package/src/lint/post-synth/gha030.ts +41 -0
- package/src/lint/post-synth/gha031.test.ts +75 -0
- package/src/lint/post-synth/gha031.ts +77 -0
- package/src/lint/post-synth/gha032.test.ts +65 -0
- package/src/lint/post-synth/gha032.ts +39 -0
- package/src/lint/post-synth/gha033.test.ts +68 -0
- package/src/lint/post-synth/gha033.ts +47 -0
- package/src/lint/post-synth/gha034.test.ts +86 -0
- package/src/lint/post-synth/gha034.ts +41 -0
- package/src/lint/post-synth/gha035.test.ts +85 -0
- package/src/lint/post-synth/gha035.ts +66 -0
- package/src/lint/post-synth/gha036.test.ts +68 -0
- package/src/lint/post-synth/gha036.ts +42 -0
- package/src/lint/post-synth/gha037.test.ts +66 -0
- package/src/lint/post-synth/gha037.ts +46 -0
- package/src/lint/post-synth/gha038.test.ts +64 -0
- package/src/lint/post-synth/gha038.ts +41 -0
- package/src/lint/post-synth/gha039.test.ts +66 -0
- package/src/lint/post-synth/gha039.ts +41 -0
- package/src/lint/post-synth/gha040.test.ts +77 -0
- package/src/lint/post-synth/gha040.ts +43 -0
- package/src/lint/post-synth/gha041.test.ts +48 -0
- package/src/lint/post-synth/gha041.ts +38 -0
- package/src/lint/post-synth/gha042.test.ts +53 -0
- package/src/lint/post-synth/gha042.ts +40 -0
- package/src/lint/post-synth/gha043.test.ts +69 -0
- package/src/lint/post-synth/gha043.ts +41 -0
- package/src/lint/post-synth/gha044.test.ts +56 -0
- package/src/lint/post-synth/gha044.ts +41 -0
- package/src/lint/post-synth/gha045.test.ts +51 -0
- package/src/lint/post-synth/gha045.ts +38 -0
- package/src/lint/post-synth/gha046.test.ts +59 -0
- package/src/lint/post-synth/gha046.ts +56 -0
- package/src/lint/post-synth/gha047.test.ts +66 -0
- package/src/lint/post-synth/gha047.ts +51 -0
- package/src/lint/post-synth/gha048.test.ts +51 -0
- package/src/lint/post-synth/gha048.ts +41 -0
- package/src/lint/post-synth/gha049.test.ts +72 -0
- package/src/lint/post-synth/gha049.ts +42 -0
- package/src/lint/post-synth/gha050.test.ts +55 -0
- package/src/lint/post-synth/gha050.ts +44 -0
- package/src/lint/post-synth/gha051.test.ts +72 -0
- package/src/lint/post-synth/gha051.ts +45 -0
- package/src/lint/post-synth/gha052.test.ts +52 -0
- package/src/lint/post-synth/gha052.ts +42 -0
- package/src/lint/post-synth/gha053.test.ts +65 -0
- package/src/lint/post-synth/gha053.ts +41 -0
- package/src/lint/post-synth/gha054.test.ts +49 -0
- package/src/lint/post-synth/gha054.ts +41 -0
- package/src/lint/post-synth/gha055.test.ts +49 -0
- package/src/lint/post-synth/gha055.ts +50 -0
- package/src/lint/post-synth/gha056.test.ts +47 -0
- package/src/lint/post-synth/gha056.ts +37 -0
- package/src/lint/post-synth/gha057.test.ts +61 -0
- package/src/lint/post-synth/gha057.ts +40 -0
- package/src/lint/post-synth/gha058.test.ts +67 -0
- package/src/lint/post-synth/gha058.ts +48 -0
- package/src/lint/post-synth/yaml-helpers.ts +335 -0
- package/src/lint/rules/data/flagged-actions.ts +36 -0
- package/src/lint/rules/data/known-action-slugs.ts +38 -0
- package/src/lint/rules/data/preinstalled-tools.ts +36 -0
- package/src/lint/rules/data/risky-features.ts +39 -0
- package/src/lint/rules/data/trusted-action-owners.ts +12 -0
- package/src/lint/rules/data/untrusted-contexts.ts +44 -0
- package/src/mcp/context-tools.test.ts +108 -0
- package/src/mcp/context-tools.ts +172 -0
- package/src/plugin.test.ts +58 -3
- package/src/plugin.ts +5 -1
- package/src/serializer.ts +38 -5
package/src/codegen/docs.ts
CHANGED
|
@@ -25,7 +25,7 @@ npm install --save-dev @intentius/chant-lexicon-github
|
|
|
25
25
|
|
|
26
26
|
{{file:docs-snippets/src/quickstart.ts}}
|
|
27
27
|
|
|
28
|
-
The lexicon provides **
|
|
28
|
+
The lexicon provides **3 resources** (Workflow, Job, Dependabot config), **14 composites** (Checkout, SetupNode, SetupGo, SetupPython, CacheAction, UploadArtifact, DownloadArtifact, NodeCI, NodePipeline, PythonCI, DockerBuild, DeployEnvironment, GoCI, Dependabot) + **3 presets** (BunPipeline, PnpmPipeline, YarnPipeline), a typed **Expression** system with 24 GitHub and 5 Runner context variables, and **13 lint rules** + **45 post-synth checks** (including a CI/CD supply-chain security pass, GHA029–058).
|
|
29
29
|
`;
|
|
30
30
|
|
|
31
31
|
const outputFormat = `The GitHub Actions lexicon serializes resources into **\`.github/workflows/*.yml\`** YAML files.
|
|
@@ -184,7 +184,7 @@ GitHub automatically picks up any \`.github/workflows/*.yml\` files and runs the
|
|
|
184
184
|
- [Workflow Concepts](/chant/lexicons/github/workflow-concepts/) — resource types, triggers, permissions
|
|
185
185
|
- [Expressions](/chant/lexicons/github/expressions/) — typed expression system and condition helpers
|
|
186
186
|
- [Composites](/chant/lexicons/github/composites/) — pre-built action wrappers (Checkout, SetupNode, etc.)
|
|
187
|
-
- [Lint Rules](/chant/lexicons/github/lint-rules/) — 13 lint rules and
|
|
187
|
+
- [Lint Rules](/chant/lexicons/github/lint-rules/) — 13 lint rules and 45 post-synth checks`,
|
|
188
188
|
},
|
|
189
189
|
{
|
|
190
190
|
slug: "workflow-concepts",
|
|
@@ -838,7 +838,27 @@ const { workflow, buildJob, testJob, lintJob } = GoCI({
|
|
|
838
838
|
const noLint = GoCI({ lintCommand: null });
|
|
839
839
|
\`\`\`
|
|
840
840
|
|
|
841
|
-
**Props:** \`goVersion?\`, \`testCommand?\`, \`buildCommand?\`, \`lintCommand?\` (null to omit), \`runsOn
|
|
841
|
+
**Props:** \`goVersion?\`, \`testCommand?\`, \`buildCommand?\`, \`lintCommand?\` (null to omit), \`runsOn?\`
|
|
842
|
+
|
|
843
|
+
## Dependabot
|
|
844
|
+
|
|
845
|
+
Models the repository's dependency-update configuration (\`.github/dependabot.yml\`) as a chant resource, so it is emitted and lintable like a workflow. The composite ships **safe defaults**: a cooldown window on every ecosystem (so a version published moments ago — including a compromised one — is not adopted before anyone can react) and external code execution explicitly denied.
|
|
846
|
+
|
|
847
|
+
\`\`\`typescript
|
|
848
|
+
import { Dependabot } from "@intentius/chant-lexicon-github";
|
|
849
|
+
|
|
850
|
+
export const dependabot = Dependabot({
|
|
851
|
+
ecosystems: [
|
|
852
|
+
{ packageEcosystem: "npm", directory: "/" },
|
|
853
|
+
{ packageEcosystem: "github-actions", directory: "/" },
|
|
854
|
+
],
|
|
855
|
+
// cooldownDays defaults to 7, openPullRequestsLimit to 5
|
|
856
|
+
});
|
|
857
|
+
\`\`\`
|
|
858
|
+
|
|
859
|
+
**Props:** \`ecosystems\` (required — each \`{ packageEcosystem, directory?, interval? }\`), \`cooldownDays?\` (default 7), \`openPullRequestsLimit?\` (default 5)
|
|
860
|
+
|
|
861
|
+
For full control, construct the \`DependabotConfig\` resource directly with raw \`updates:\` entries. Two post-synth checks validate the emitted config: **GHA057** (\`insecure-external-code-execution: allow\`) and **GHA058** (no cooldown). See [Lint Rules](/chant/lexicons/github/lint-rules/).`,
|
|
842
862
|
},
|
|
843
863
|
{
|
|
844
864
|
slug: "lint-rules",
|
|
@@ -952,6 +972,12 @@ Flags matrix strategy dimensions with an empty values array. An empty dimension
|
|
|
952
972
|
|
|
953
973
|
Flags jobs whose \`needs:\` entries reference a job not defined in the workflow. This causes a workflow validation error on GitHub.
|
|
954
974
|
|
|
975
|
+
### GHA013 — Missing job-level permissions for sensitive triggers
|
|
976
|
+
|
|
977
|
+
**Severity:** warning
|
|
978
|
+
|
|
979
|
+
Flags jobs without an explicit \`permissions:\` block when the workflow uses a sensitive trigger (\`pull_request_target\` or \`workflow_dispatch\`). Declaring job-level permissions keeps least-privilege scope on workflows that run with elevated context.
|
|
980
|
+
|
|
955
981
|
### GHA017 — Missing permissions block
|
|
956
982
|
|
|
957
983
|
**Severity:** info
|
|
@@ -970,6 +996,242 @@ Flags workflows triggered by \`pull_request_target\` that include \`actions/chec
|
|
|
970
996
|
|
|
971
997
|
Detects cycles in the \`needs:\` dependency graph. If job A needs B and B needs A (directly or transitively), GitHub rejects the workflow. Reports the full cycle chain in the diagnostic message.
|
|
972
998
|
|
|
999
|
+
### GHA021 — Checkout action not pinned to a SHA
|
|
1000
|
+
|
|
1001
|
+
**Severity:** warning
|
|
1002
|
+
|
|
1003
|
+
Flags \`actions/checkout\` referenced by a tag (e.g. \`@v4\`) instead of a pinned commit SHA. The narrower precursor to GHA029, kept because checkout is the most common unpinned action.
|
|
1004
|
+
|
|
1005
|
+
### GHA022 — Job without timeout-minutes
|
|
1006
|
+
|
|
1007
|
+
**Severity:** info
|
|
1008
|
+
|
|
1009
|
+
Flags jobs that omit \`timeout-minutes\`. A hung step otherwise runs to the runner's default cap, burning minutes — set an explicit ceiling.
|
|
1010
|
+
|
|
1011
|
+
### GHA023 — Deprecated set-output command
|
|
1012
|
+
|
|
1013
|
+
**Severity:** warning
|
|
1014
|
+
|
|
1015
|
+
Flags \`::set-output\` in \`run:\` steps. The workflow command is deprecated and disabled on current runners — write to \`$GITHUB_OUTPUT\` instead.
|
|
1016
|
+
|
|
1017
|
+
### GHA024 — Missing concurrency for deploy workflows
|
|
1018
|
+
|
|
1019
|
+
**Severity:** info
|
|
1020
|
+
|
|
1021
|
+
Flags deploy workflows without a \`concurrency:\` block. Without one, two pushes can deploy concurrently and race — add a concurrency group to serialize them.
|
|
1022
|
+
|
|
1023
|
+
### GHA025 — pull_request_target without restrictions
|
|
1024
|
+
|
|
1025
|
+
**Severity:** warning
|
|
1026
|
+
|
|
1027
|
+
Flags \`pull_request_target\` used without a \`types:\` filter. The trigger runs with repository secrets in the base-branch context, so it should be scoped to the specific PR events that need it.
|
|
1028
|
+
|
|
1029
|
+
### GHA026 — Secret used without environment protection
|
|
1030
|
+
|
|
1031
|
+
**Severity:** info
|
|
1032
|
+
|
|
1033
|
+
Flags workflows that reference \`secrets.\` in steps but declare no \`environment:\` on any job, so the secret skips the approval and scoping rules an environment gate provides.
|
|
1034
|
+
|
|
1035
|
+
### GHA027 — Cleanup step missing if: always()
|
|
1036
|
+
|
|
1037
|
+
**Severity:** info
|
|
1038
|
+
|
|
1039
|
+
Flags steps named "cleanup" / "teardown" / "clean up" that lack an \`if:\` condition. Cleanup should run even when a prior step fails — add \`if: always()\`.
|
|
1040
|
+
|
|
1041
|
+
### GHA028 — Workflow with no on: triggers
|
|
1042
|
+
|
|
1043
|
+
**Severity:** error
|
|
1044
|
+
|
|
1045
|
+
Flags a workflow file with no top-level \`on:\` key. Without a trigger the workflow can never run.
|
|
1046
|
+
|
|
1047
|
+
## Supply-chain security pass (GHA029–058)
|
|
1048
|
+
|
|
1049
|
+
GHA029 onward are a CI/CD supply-chain security pass: pin & vet external references, enforce least-privilege token scopes, guard trust boundaries against untrusted input, contain secrets, reject unsound expressions, and keep artifacts/caches honest. They run statically on the emitted YAML — everything answerable without leaving the build.
|
|
1050
|
+
|
|
1051
|
+
The checks that need a *moving external truth* — whether a pinned SHA still maps to a real upstream tag, whether a ref still exists, whether a new advisory now covers an action in use — can't be deterministic, so they live in the operational layer instead. Schedule the [\`WorkflowAuditOp\`](/chant/guide/ops/#audit-supply-chain-drift) (temporal lexicon) for that live, always-fresh half; it reads the same emitted workflow references and reports drift via \`report | issue | pull-request\`.
|
|
1052
|
+
|
|
1053
|
+
### GHA029 — Action or reusable workflow not pinned to a commit SHA
|
|
1054
|
+
|
|
1055
|
+
**Severity:** warning
|
|
1056
|
+
|
|
1057
|
+
Flags any \`uses:\` — step action or job-level reusable workflow — pinned to a mutable tag or branch instead of a full commit SHA. Tags can be repointed to malicious code after review, so every external reference should be pinned. \`actions/checkout\` is covered by the more specific GHA021; local (\`./\`) and \`docker://\` references are out of scope. Owners in the vendored trusted allowlist are exempt.
|
|
1058
|
+
|
|
1059
|
+
### GHA030 — Container image not pinned to a digest
|
|
1060
|
+
|
|
1061
|
+
**Severity:** warning
|
|
1062
|
+
|
|
1063
|
+
Flags job \`container:\` images, \`services:\` images, and \`docker://\` step references that are not pinned to an immutable \`@sha256:\` digest. A mutable tag can be repointed to a different image after review.
|
|
1064
|
+
|
|
1065
|
+
### GHA031 — Action resembles a well-known action
|
|
1066
|
+
|
|
1067
|
+
**Severity:** warning
|
|
1068
|
+
|
|
1069
|
+
Flags a \`uses:\` slug that is a near-miss (edit distance 1–2) of a popular action but not an exact match — a likely typo or impersonation under different ownership. Advisory; backed by a vendored reference list.
|
|
1070
|
+
|
|
1071
|
+
### GHA032 — Archived or compromised action
|
|
1072
|
+
|
|
1073
|
+
**Severity:** warning
|
|
1074
|
+
|
|
1075
|
+
Flags a \`uses:\` slug that a vendored snapshot marks as archived/abandoned or carrying a disclosed security issue, with remediation. Advisory and necessarily incomplete.
|
|
1076
|
+
|
|
1077
|
+
### GHA033 — Blanket write-all permissions
|
|
1078
|
+
|
|
1079
|
+
**Severity:** warning
|
|
1080
|
+
|
|
1081
|
+
Flags \`permissions: write-all\` at the workflow or job level. It grants the \`GITHUB_TOKEN\` every write scope regardless of need — replace it with the specific scopes the job uses.
|
|
1082
|
+
|
|
1083
|
+
### GHA034 — Write permissions granted workflow-wide
|
|
1084
|
+
|
|
1085
|
+
**Severity:** warning
|
|
1086
|
+
|
|
1087
|
+
Flags individual write scopes declared at the workflow level, which apply to every job even when only one needs them. Move each write scope onto the specific job that uses it. (The \`write-all\` preset is covered by GHA033.)
|
|
1088
|
+
|
|
1089
|
+
### GHA035 — Elevated scope on an untrusted-code trigger
|
|
1090
|
+
|
|
1091
|
+
**Severity:** error
|
|
1092
|
+
|
|
1093
|
+
Flags a workflow that grants the token write access while using a trigger that can run untrusted code (\`pull_request_target\`, \`workflow_run\`). An injected step would run with standing write credentials — drop the write scope or isolate the privileged work in a separate trusted workflow.
|
|
1094
|
+
|
|
1095
|
+
### GHA036 — Untrusted input in a run: command
|
|
1096
|
+
|
|
1097
|
+
**Severity:** error
|
|
1098
|
+
|
|
1099
|
+
Flags an attacker-controllable expression context (PR title, branch name, issue/comment body, commit message) interpolated directly into a \`run:\` script — a script-injection sink. Pass the value through an \`env:\` variable and reference it quoted instead.
|
|
1100
|
+
|
|
1101
|
+
### GHA037 — Untrusted input written to GITHUB_ENV / GITHUB_PATH
|
|
1102
|
+
|
|
1103
|
+
**Severity:** error
|
|
1104
|
+
|
|
1105
|
+
Flags a \`run:\` step that writes untrusted input into \`$GITHUB_ENV\` or \`$GITHUB_PATH\`, which set environment/PATH state for later steps and can escalate into takeover of a subsequent privileged step.
|
|
1106
|
+
|
|
1107
|
+
### GHA038 — workflow_run trigger checking out untrusted code
|
|
1108
|
+
|
|
1109
|
+
**Severity:** warning
|
|
1110
|
+
|
|
1111
|
+
Generalizes GHA018 to the \`workflow_run\` trigger, which runs with repo write scope and secrets. Checking out the head/artifact of the triggering run pulls untrusted code into that privileged context.
|
|
1112
|
+
|
|
1113
|
+
### GHA039 — Authorization gate on a spoofable identity
|
|
1114
|
+
|
|
1115
|
+
**Severity:** warning
|
|
1116
|
+
|
|
1117
|
+
Flags an \`if:\` condition that gates on a commit-author identity field (\`author.name\` / \`author.email\`). Those come from git metadata the committer sets freely and can be spoofed — gate on a verified signal (environment protection, CODEOWNERS, verified actor).
|
|
1118
|
+
|
|
1119
|
+
### GHA040 — Self-hosted runner on an untrusted-code trigger
|
|
1120
|
+
|
|
1121
|
+
**Severity:** warning
|
|
1122
|
+
|
|
1123
|
+
Flags a job on a self-hosted runner under a trigger a fork can reach (\`pull_request\`, \`pull_request_target\`, \`workflow_run\`). Self-hosted runners are non-ephemeral and shared, so untrusted code can persist and compromise later jobs.
|
|
1124
|
+
|
|
1125
|
+
### GHA041 — Blanket secrets: inherit into a reusable workflow
|
|
1126
|
+
|
|
1127
|
+
**Severity:** warning
|
|
1128
|
+
|
|
1129
|
+
Flags a reusable-workflow call passing \`secrets: inherit\`, which hands the called workflow every caller secret. Pass through only the specific secrets it needs.
|
|
1130
|
+
|
|
1131
|
+
### GHA042 — Entire secrets context passed
|
|
1132
|
+
|
|
1133
|
+
**Severity:** warning
|
|
1134
|
+
|
|
1135
|
+
Flags \`toJSON(secrets)\` passed into a step or reusable workflow, serializing every secret where one or two specific references would do.
|
|
1136
|
+
|
|
1137
|
+
### GHA043 — Secret consumed without an environment gate
|
|
1138
|
+
|
|
1139
|
+
**Severity:** warning
|
|
1140
|
+
|
|
1141
|
+
Extends GHA026: when a workflow gates some jobs with an \`environment:\`, flags the specific secret-using jobs that have none — the inconsistent-gating case where a job skips the approval/scoping applied elsewhere.
|
|
1142
|
+
|
|
1143
|
+
### GHA044 — Hardcoded registry/container credential
|
|
1144
|
+
|
|
1145
|
+
**Severity:** error
|
|
1146
|
+
|
|
1147
|
+
Flags a \`password:\` / \`token:\` / \`registry-password:\` set to a literal rather than a \`\${{ secrets.* }}\` reference. Move the credential into a secret.
|
|
1148
|
+
|
|
1149
|
+
### GHA045 — Secret interpolated into a run: command
|
|
1150
|
+
|
|
1151
|
+
**Severity:** warning
|
|
1152
|
+
|
|
1153
|
+
Flags \`\${{ secrets.* }}\` expanded directly into a \`run:\` script, where a transform can defeat log masking and the raw value is exposed to argument injection. Pass it through an \`env:\` variable and reference \`"$VAR"\` quoted.
|
|
1154
|
+
|
|
1155
|
+
### GHA046 — Logically unsound guard condition
|
|
1156
|
+
|
|
1157
|
+
**Severity:** warning
|
|
1158
|
+
|
|
1159
|
+
Flags an \`if:\` condition that reads like a gate but evaluates to a constant — \`true\`/\`false\` literals, an \`X == X\` tautology, or a collapse via \`|| true\` / \`&& false\`. A gate that constrains nothing is misleading.
|
|
1160
|
+
|
|
1161
|
+
### GHA047 — Ineffective contains() guard
|
|
1162
|
+
|
|
1163
|
+
**Severity:** warning
|
|
1164
|
+
|
|
1165
|
+
Flags \`contains('literal', <dynamic>)\` — a constant haystack with a dynamic needle. \`contains(search, item)\` tests whether \`item\` is in \`search\`, so reversed arguments make the result depend on a fixed string. Swap them.
|
|
1166
|
+
|
|
1167
|
+
### GHA048 — Obfuscated guard condition
|
|
1168
|
+
|
|
1169
|
+
**Severity:** warning
|
|
1170
|
+
|
|
1171
|
+
Flags an \`if:\` gate whose compared operand is built through \`format()\` / \`join()\` / \`fromJSON()\` indirection. Constructing the operand at evaluation time hides what the gate checks — compare against the value directly.
|
|
1172
|
+
|
|
1173
|
+
### GHA049 — Persisted checkout credentials reachable by an artifact
|
|
1174
|
+
|
|
1175
|
+
**Severity:** warning
|
|
1176
|
+
|
|
1177
|
+
Flags a job that checks out with persisted credentials (the default) and uploads an artifact — the token in \`.git/config\` can be swept into the artifact. Set \`persist-credentials: false\` on the checkout.
|
|
1178
|
+
|
|
1179
|
+
### GHA050 — Cache populated in a privileged context
|
|
1180
|
+
|
|
1181
|
+
**Severity:** warning
|
|
1182
|
+
|
|
1183
|
+
Flags \`actions/cache\` under a privileged trigger (\`pull_request_target\`, \`workflow_run\`). A cache entry influenced by a fork can be restored and executed by a later trusted run (cache poisoning) — restrict caching to trusted triggers.
|
|
1184
|
+
|
|
1185
|
+
### GHA051 — Publish step using a long-lived token instead of OIDC
|
|
1186
|
+
|
|
1187
|
+
**Severity:** info
|
|
1188
|
+
|
|
1189
|
+
Flags a publish/release job that uses a long-lived token secret while requesting no \`id-token: write\`. If the registry supports OIDC, mint a short-lived federated credential per run instead of holding a standing token.
|
|
1190
|
+
|
|
1191
|
+
### GHA052 — Software fetched and piped to a shell
|
|
1192
|
+
|
|
1193
|
+
**Severity:** warning
|
|
1194
|
+
|
|
1195
|
+
Flags a \`run:\` step that pipes a network download straight into a shell (\`curl ... | bash\`). The fetched code is unpinned and unverified — download to a file, verify a checksum/signature, then run it.
|
|
1196
|
+
|
|
1197
|
+
### GHA053 — Unsafe set-env / add-path opt-in
|
|
1198
|
+
|
|
1199
|
+
**Severity:** error
|
|
1200
|
+
|
|
1201
|
+
Flags re-enabling the \`set-env\` / \`add-path\` workflow commands removed for security (CVE-2020-15228) via \`ACTIONS_ALLOW_UNSECURE_COMMANDS\` or direct \`::set-env::\` / \`::add-path::\`. Use the \`$GITHUB_ENV\` / \`$GITHUB_PATH\` files instead.
|
|
1202
|
+
|
|
1203
|
+
### GHA054 — Known-bad feature usage
|
|
1204
|
+
|
|
1205
|
+
**Severity:** warning
|
|
1206
|
+
|
|
1207
|
+
Catch-all, data-driven check flagging emitted content matching a vendored snapshot of risky features (deprecated workflow commands, unsafe runtime opt-ins). Advisory and necessarily incomplete.
|
|
1208
|
+
|
|
1209
|
+
### GHA055 — Redundant runtime tool install
|
|
1210
|
+
|
|
1211
|
+
**Severity:** info
|
|
1212
|
+
|
|
1213
|
+
Flags a \`run:\` step that installs a tool GitHub-hosted runners already ship. The redundant install adds supply-chain surface for no benefit (irrelevant on self-hosted runners that may lack the tool).
|
|
1214
|
+
|
|
1215
|
+
### GHA056 — Workflow without a name
|
|
1216
|
+
|
|
1217
|
+
**Severity:** info
|
|
1218
|
+
|
|
1219
|
+
Flags a workflow with no top-level \`name:\`. Without one, GitHub falls back to the file path in the UI and audit logs, making runs harder to identify.
|
|
1220
|
+
|
|
1221
|
+
> Two items from issue #295 are intentionally not implemented at the post-synth layer: over-broad/unrevoked app-installation tokens (whether a minted token is scoped wider than used cannot be determined from emitted YAML) and a reference allowlist/denylist policy (configuration, overlapping the GHA029 trusted-owner allowlist).
|
|
1222
|
+
|
|
1223
|
+
### GHA057 — Dependency update executes untrusted code
|
|
1224
|
+
|
|
1225
|
+
**Severity:** error
|
|
1226
|
+
|
|
1227
|
+
Flags a Dependabot \`updates:\` entry with \`insecure-external-code-execution: allow\`, which runs a freshly-pulled dependency's lifecycle scripts during the update itself — a compromised release executes before any review. Set it to \`deny\`. Requires the \`Dependabot\` resource so the config is emitted (\`.github/dependabot.yml\`).
|
|
1228
|
+
|
|
1229
|
+
### GHA058 — Dependency update without a cooldown
|
|
1230
|
+
|
|
1231
|
+
**Severity:** warning
|
|
1232
|
+
|
|
1233
|
+
Flags a Dependabot \`updates:\` entry with no cooldown — a version published moments ago (including a compromised one) is adopted immediately. Configure a \`cooldown:\` window. The \`Dependabot\` composite ships a 7-day default.
|
|
1234
|
+
|
|
973
1235
|
## Running lint
|
|
974
1236
|
|
|
975
1237
|
\`\`\`bash
|
|
@@ -1099,7 +1361,7 @@ The inline \`chant-github\` skill covers the full workflow lifecycle:
|
|
|
1099
1361
|
- **Validate** — \`chant lint src/\`
|
|
1100
1362
|
- **Deploy** — commit and push the generated YAML
|
|
1101
1363
|
- **Status** — GitHub Actions UI or \`gh run list\`
|
|
1102
|
-
- **Troubleshooting** — lint rule codes (GHA001–GHA020), post-synth checks (GHA006–
|
|
1364
|
+
- **Troubleshooting** — lint rule codes (GHA001–GHA020), post-synth checks (GHA006–GHA058)
|
|
1103
1365
|
|
|
1104
1366
|
The skill is invocable as a slash command: \`/chant-github\`
|
|
1105
1367
|
|
|
@@ -1123,6 +1385,13 @@ The lexicon provides MCP (Model Context Protocol) tools and resources that AI ag
|
|
|
1123
1385
|
| MCP tool | Description |
|
|
1124
1386
|
|----------|-------------|
|
|
1125
1387
|
| \`diff\` | Compare current build output against previous output |
|
|
1388
|
+
| \`github:checks\` | Build the workflow and return its security findings (the GHA checks) |
|
|
1389
|
+
| \`github:workflow\` | Triggers and jobs as written — name, run order, step count |
|
|
1390
|
+
| \`github:references\` | Actions and images pulled in, and whether each is pinned to a commit SHA |
|
|
1391
|
+
| \`github:affected\` | Given a job, the jobs that would re-run because they depend on it |
|
|
1392
|
+
| \`github:workflow-yaml\` | The generated workflow YAML |
|
|
1393
|
+
|
|
1394
|
+
The \`github:*\` tools are **read-only context tools** (#327): each builds from your source and returns what chant already computes — before the workflow runs or merges. None touch the live GitHub instance, run history, or write anything.
|
|
1126
1395
|
|
|
1127
1396
|
| MCP resource | Description |
|
|
1128
1397
|
|--------------|-------------|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { Dependabot, DependabotConfig } from "./dependabot";
|
|
3
|
+
import { githubSerializer } from "../serializer";
|
|
4
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
5
|
+
|
|
6
|
+
describe("Dependabot composite (#294)", () => {
|
|
7
|
+
test("ships safe defaults: cooldown + external code execution denied", () => {
|
|
8
|
+
const cfg = Dependabot({ ecosystems: [{ packageEcosystem: "npm" }, { packageEcosystem: "github-actions" }] });
|
|
9
|
+
expect(cfg).toBeInstanceOf(DependabotConfig);
|
|
10
|
+
expect(cfg.props.version).toBe(2);
|
|
11
|
+
expect(cfg.props.updates).toHaveLength(2);
|
|
12
|
+
for (const u of cfg.props.updates) {
|
|
13
|
+
expect(u.cooldown?.defaultDays).toBe(7);
|
|
14
|
+
expect(u.insecureExternalCodeExecution).toBe("deny");
|
|
15
|
+
expect(u.directory).toBe("/");
|
|
16
|
+
expect(u.schedule.interval).toBe("weekly");
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("serializes to a .github/dependabot.yml file (kebab-case keys)", () => {
|
|
21
|
+
const cfg = Dependabot({ ecosystems: [{ packageEcosystem: "npm" }] });
|
|
22
|
+
const result = githubSerializer.serialize(new Map([["deps", cfg]])) as SerializerResult;
|
|
23
|
+
expect(typeof result).toBe("object");
|
|
24
|
+
const yaml = result.files?.["dependabot.yml"];
|
|
25
|
+
expect(yaml).toBeDefined();
|
|
26
|
+
expect(yaml).toContain("version: 2");
|
|
27
|
+
expect(yaml).toContain("package-ecosystem: npm");
|
|
28
|
+
expect(yaml).toContain("open-pull-requests-limit: 5");
|
|
29
|
+
expect(yaml).toContain("insecure-external-code-execution: deny");
|
|
30
|
+
expect(yaml).toContain("default-days: 7");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("emits dependabot.yml alongside a workflow", () => {
|
|
34
|
+
const cfg = Dependabot({ ecosystems: [{ packageEcosystem: "pip", interval: "daily" }] });
|
|
35
|
+
const wf = {
|
|
36
|
+
[Symbol.for("chant.declarable")]: true,
|
|
37
|
+
lexicon: "github",
|
|
38
|
+
entityType: "GitHub::Actions::Workflow",
|
|
39
|
+
kind: "resource" as const,
|
|
40
|
+
props: { name: "CI" },
|
|
41
|
+
};
|
|
42
|
+
const result = githubSerializer.serialize(
|
|
43
|
+
new Map<string, never>([["ci", wf as never], ["deps", cfg as never]]),
|
|
44
|
+
) as SerializerResult;
|
|
45
|
+
expect(result.primary).toContain("name: CI");
|
|
46
|
+
expect(result.files?.["dependabot.yml"]).toContain("package-ecosystem: pip");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-update configuration (`.github/dependabot.yml`) as a chant
|
|
3
|
+
* resource, so it is emitted and lintable like workflows (#294).
|
|
4
|
+
*
|
|
5
|
+
* Two failure modes the post-synth checks target:
|
|
6
|
+
* - an update that executes untrusted code from a freshly-pulled dependency
|
|
7
|
+
* during the update itself (`insecure-external-code-execution: allow`), and
|
|
8
|
+
* - a configuration with no cooldown — a version published seconds ago
|
|
9
|
+
* (including a compromised one) is adopted before anyone can react.
|
|
10
|
+
*
|
|
11
|
+
* The `Dependabot` composite ships safe defaults: a cooldown window and an
|
|
12
|
+
* explicit deny on external code execution.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
|
|
16
|
+
|
|
17
|
+
/** A single `updates:` entry in dependabot.yml. */
|
|
18
|
+
export interface DependabotUpdate {
|
|
19
|
+
/** e.g. "npm", "pip", "github-actions", "docker". */
|
|
20
|
+
packageEcosystem: string;
|
|
21
|
+
/** Directory to scan (defaults to "/"). */
|
|
22
|
+
directory?: string;
|
|
23
|
+
directories?: string[];
|
|
24
|
+
/** Update schedule. */
|
|
25
|
+
schedule: { interval: "daily" | "weekly" | "monthly"; day?: string; time?: string };
|
|
26
|
+
/** Cooldown window — delay adopting freshly-published versions. */
|
|
27
|
+
cooldown?: {
|
|
28
|
+
defaultDays?: number;
|
|
29
|
+
semverMajorDays?: number;
|
|
30
|
+
semverMinorDays?: number;
|
|
31
|
+
semverPatchDays?: number;
|
|
32
|
+
};
|
|
33
|
+
/** Cap on concurrent update PRs. */
|
|
34
|
+
openPullRequestsLimit?: number;
|
|
35
|
+
/** Whether to run a dependency's lifecycle scripts during the update. */
|
|
36
|
+
insecureExternalCodeExecution?: "allow" | "deny";
|
|
37
|
+
/** Passthrough for the rest of the dependabot update schema. */
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DependabotConfigProps {
|
|
42
|
+
version?: number;
|
|
43
|
+
updates: DependabotUpdate[];
|
|
44
|
+
registries?: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The `.github/dependabot.yml` resource. */
|
|
48
|
+
export class DependabotConfig implements Declarable {
|
|
49
|
+
readonly [DECLARABLE_MARKER] = true as const;
|
|
50
|
+
readonly lexicon = "github";
|
|
51
|
+
readonly entityType = "GitHub::Dependabot::Config";
|
|
52
|
+
readonly kind = "resource" as const;
|
|
53
|
+
readonly props: DependabotConfigProps;
|
|
54
|
+
|
|
55
|
+
constructor(props: DependabotConfigProps) {
|
|
56
|
+
this.props = { version: 2, ...props };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface DependabotEcosystem {
|
|
61
|
+
packageEcosystem: string;
|
|
62
|
+
directory?: string;
|
|
63
|
+
interval?: "daily" | "weekly" | "monthly";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface DependabotProps {
|
|
67
|
+
/** Ecosystems to keep updated. */
|
|
68
|
+
ecosystems: DependabotEcosystem[];
|
|
69
|
+
/** Cooldown window in days before adopting a freshly-published version (default 7). */
|
|
70
|
+
cooldownDays?: number;
|
|
71
|
+
/** Cap on concurrent update PRs per ecosystem (default 5). */
|
|
72
|
+
openPullRequestsLimit?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build a `DependabotConfig` with safe defaults: a cooldown window on every
|
|
77
|
+
* ecosystem and external code execution explicitly denied.
|
|
78
|
+
*/
|
|
79
|
+
export function Dependabot(props: DependabotProps): DependabotConfig {
|
|
80
|
+
const cooldownDays = props.cooldownDays ?? 7;
|
|
81
|
+
const limit = props.openPullRequestsLimit ?? 5;
|
|
82
|
+
const updates: DependabotUpdate[] = props.ecosystems.map((e) => ({
|
|
83
|
+
packageEcosystem: e.packageEcosystem,
|
|
84
|
+
directory: e.directory ?? "/",
|
|
85
|
+
schedule: { interval: e.interval ?? "weekly" },
|
|
86
|
+
cooldown: { defaultDays: cooldownDays },
|
|
87
|
+
openPullRequestsLimit: limit,
|
|
88
|
+
insecureExternalCodeExecution: "deny",
|
|
89
|
+
}));
|
|
90
|
+
return new DependabotConfig({ version: 2, updates });
|
|
91
|
+
}
|
package/src/composites/index.ts
CHANGED
|
@@ -24,3 +24,5 @@ export { DeployEnvironment } from "./deploy-environment";
|
|
|
24
24
|
export type { DeployEnvironmentProps } from "./deploy-environment";
|
|
25
25
|
export { GoCI } from "./go-ci";
|
|
26
26
|
export type { GoCIProps } from "./go-ci";
|
|
27
|
+
export { Dependabot, DependabotConfig } from "./dependabot";
|
|
28
|
+
export type { DependabotProps, DependabotConfigProps, DependabotUpdate, DependabotEcosystem } from "./dependabot";
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
DockerBuild,
|
|
31
31
|
DeployEnvironment,
|
|
32
32
|
GoCI,
|
|
33
|
+
Dependabot, DependabotConfig,
|
|
33
34
|
} from "./composites/index";
|
|
34
35
|
export type {
|
|
35
36
|
CheckoutProps, SetupNodeProps, SetupGoProps, SetupPythonProps,
|
|
@@ -40,6 +41,7 @@ export type {
|
|
|
40
41
|
DockerBuildProps,
|
|
41
42
|
DeployEnvironmentProps,
|
|
42
43
|
GoCIProps,
|
|
44
|
+
DependabotProps, DependabotConfigProps, DependabotUpdate, DependabotEcosystem,
|
|
43
45
|
} from "./composites/index";
|
|
44
46
|
|
|
45
47
|
// Spec utilities (for tooling)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
-
import {
|
|
3
|
+
import { gha013 } from "./gha013";
|
|
4
4
|
|
|
5
5
|
function makeCtx(yaml: string): PostSynthContext {
|
|
6
6
|
return {
|
|
@@ -16,7 +16,7 @@ function makeCtx(yaml: string): PostSynthContext {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
describe("
|
|
19
|
+
describe("GHA013: missing job-level permissions for sensitive triggers", () => {
|
|
20
20
|
test("flags job without permissions when using pull_request_target", () => {
|
|
21
21
|
const yaml = `name: Review
|
|
22
22
|
on:
|
|
@@ -27,9 +27,9 @@ jobs:
|
|
|
27
27
|
steps:
|
|
28
28
|
- run: echo review
|
|
29
29
|
`;
|
|
30
|
-
const diags =
|
|
30
|
+
const diags = gha013.check(makeCtx(yaml));
|
|
31
31
|
expect(diags).toHaveLength(1);
|
|
32
|
-
expect(diags[0].checkId).toBe("
|
|
32
|
+
expect(diags[0].checkId).toBe("GHA013");
|
|
33
33
|
expect(diags[0].severity).toBe("warning");
|
|
34
34
|
expect(diags[0].message).toContain("review");
|
|
35
35
|
});
|
|
@@ -44,9 +44,9 @@ jobs:
|
|
|
44
44
|
steps:
|
|
45
45
|
- run: echo deploy
|
|
46
46
|
`;
|
|
47
|
-
const diags =
|
|
47
|
+
const diags = gha013.check(makeCtx(yaml));
|
|
48
48
|
expect(diags).toHaveLength(1);
|
|
49
|
-
expect(diags[0].checkId).toBe("
|
|
49
|
+
expect(diags[0].checkId).toBe("GHA013");
|
|
50
50
|
expect(diags[0].message).toContain("deploy");
|
|
51
51
|
});
|
|
52
52
|
|
|
@@ -60,7 +60,7 @@ jobs:
|
|
|
60
60
|
steps:
|
|
61
61
|
- run: echo build
|
|
62
62
|
`;
|
|
63
|
-
const diags =
|
|
63
|
+
const diags = gha013.check(makeCtx(yaml));
|
|
64
64
|
expect(diags).toHaveLength(0);
|
|
65
65
|
});
|
|
66
66
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
12
|
-
id: "
|
|
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: "
|
|
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,108 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { gha029, findUnpinnedActions } from "./gha029";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["github", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["github", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("GHA029: unpinned action / reusable workflow", () => {
|
|
20
|
+
test("flags a step action pinned to a tag", () => {
|
|
21
|
+
const yaml = `name: CI
|
|
22
|
+
on:
|
|
23
|
+
push:
|
|
24
|
+
jobs:
|
|
25
|
+
build:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/setup-node@v4
|
|
29
|
+
- run: echo build
|
|
30
|
+
`;
|
|
31
|
+
const diags = gha029.check(makeCtx(yaml));
|
|
32
|
+
expect(diags).toHaveLength(1);
|
|
33
|
+
expect(diags[0].checkId).toBe("GHA029");
|
|
34
|
+
expect(diags[0].severity).toBe("warning");
|
|
35
|
+
expect(diags[0].entity).toBe("build");
|
|
36
|
+
expect(diags[0].message).toContain("actions/setup-node@v4");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("flags a job-level reusable workflow pinned to a tag", () => {
|
|
40
|
+
const yaml = `name: CI
|
|
41
|
+
on:
|
|
42
|
+
push:
|
|
43
|
+
jobs:
|
|
44
|
+
call:
|
|
45
|
+
uses: org/repo/.github/workflows/reusable.yml@v1
|
|
46
|
+
`;
|
|
47
|
+
const diags = gha029.check(makeCtx(yaml));
|
|
48
|
+
expect(diags).toHaveLength(1);
|
|
49
|
+
expect(diags[0].message).toContain("reusable.yml@v1");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("does not flag an action pinned to a full SHA", () => {
|
|
53
|
+
const yaml = `name: CI
|
|
54
|
+
on:
|
|
55
|
+
push:
|
|
56
|
+
jobs:
|
|
57
|
+
build:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/setup-node@1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b
|
|
61
|
+
- run: echo build
|
|
62
|
+
`;
|
|
63
|
+
const diags = gha029.check(makeCtx(yaml));
|
|
64
|
+
expect(diags).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("does not flag actions/checkout (owned by GHA021)", () => {
|
|
68
|
+
const yaml = `name: CI
|
|
69
|
+
on:
|
|
70
|
+
push:
|
|
71
|
+
jobs:
|
|
72
|
+
build:
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/checkout@v4
|
|
76
|
+
`;
|
|
77
|
+
const diags = gha029.check(makeCtx(yaml));
|
|
78
|
+
expect(diags).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("does not flag local actions", () => {
|
|
82
|
+
const yaml = `name: CI
|
|
83
|
+
on:
|
|
84
|
+
push:
|
|
85
|
+
jobs:
|
|
86
|
+
build:
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
steps:
|
|
89
|
+
- uses: ./.github/actions/local
|
|
90
|
+
`;
|
|
91
|
+
const diags = gha029.check(makeCtx(yaml));
|
|
92
|
+
expect(diags).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("allowlist exempts a trusted owner", () => {
|
|
96
|
+
const yaml = `name: CI
|
|
97
|
+
on:
|
|
98
|
+
push:
|
|
99
|
+
jobs:
|
|
100
|
+
build:
|
|
101
|
+
runs-on: ubuntu-latest
|
|
102
|
+
steps:
|
|
103
|
+
- uses: trustedco/action@v1
|
|
104
|
+
`;
|
|
105
|
+
expect(findUnpinnedActions(yaml)).toHaveLength(1);
|
|
106
|
+
expect(findUnpinnedActions(yaml, new Set(["trustedco"]))).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
});
|