claws-scan 0.7.3
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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +31 -0
- data/.ruby-version +1 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +99 -0
- data/README.md +557 -0
- data/Rakefile +12 -0
- data/bin/analyze +62 -0
- data/config.yml +16 -0
- data/corpus/automerge_via_action.yml +28 -0
- data/corpus/automerge_via_cli.yml +14 -0
- data/corpus/build-docker-image-run-drc-for-cell-gds-using-magic.yml +170 -0
- data/corpus/cmd.yml +14 -0
- data/corpus/container.yml +19 -0
- data/corpus/container_docker.yml +9 -0
- data/corpus/dispatch_command_injection.yml +17 -0
- data/corpus/inherit_secrets.yml +20 -0
- data/corpus/nameless.yml +11 -0
- data/corpus/permissions.yml +19 -0
- data/corpus/ruby.yml +12 -0
- data/corpus/shellcheck.yml +12 -0
- data/corpus/unsafe_checkout_code_execution.yml +21 -0
- data/corpus/unsafe_checkout_token_leak.yml +33 -0
- data/corpus/unscoped_secrets.yml +16 -0
- data/github_action.yml +36 -0
- data/lib/claws/application.rb +237 -0
- data/lib/claws/base_rule.rb +94 -0
- data/lib/claws/cli/color.rb +30 -0
- data/lib/claws/cli/yaml_with_lines.rb +124 -0
- data/lib/claws/engine.rb +25 -0
- data/lib/claws/formatter/github.rb +17 -0
- data/lib/claws/formatter/stdout.rb +13 -0
- data/lib/claws/formatters.rb +4 -0
- data/lib/claws/rule/automatic_merge.rb +49 -0
- data/lib/claws/rule/bulk_permissions.rb +20 -0
- data/lib/claws/rule/command_injection.rb +14 -0
- data/lib/claws/rule/empty_name.rb +14 -0
- data/lib/claws/rule/inherited_secrets.rb +17 -0
- data/lib/claws/rule/no_containers.rb +28 -0
- data/lib/claws/rule/risky_triggers.rb +32 -0
- data/lib/claws/rule/shellcheck.rb +109 -0
- data/lib/claws/rule/special_permissions.rb +37 -0
- data/lib/claws/rule/unapproved_runners.rb +31 -0
- data/lib/claws/rule/unpinned_action.rb +30 -0
- data/lib/claws/rule/unsafe_checkout.rb +36 -0
- data/lib/claws/rule.rb +13 -0
- data/lib/claws/version.rb +5 -0
- data/lib/claws/violation.rb +11 -0
- data/lib/claws/workflow.rb +221 -0
- data/lib/claws.rb +6 -0
- metadata +151 -0
data/README.md
ADDED
@@ -0,0 +1,557 @@
|
|
1
|
+
# Claws
|
2
|
+
|
3
|
+
Claws is a static analysis tool to help you write safer Github Workflows. Inspired by [rubocop](https://github.com/rubocop/rubocop) and its [def_node_matcher](https://docs.rubocop.org/rubocop-ast/node_pattern.html), Claws' rules are simple Ruby classes that contain expressions describing undesirable behaviors. These expressions (written in the [equation expression language](https://github.com/ancat/equation#language-features)) are evaluated at each "depth" of a Github Workflow: Workflow, Job, Step. Any part of a Workflow that causes an expression to return true is surfaced to the user as a violation.
|
4
|
+
|
5
|
+
Rules were designed to be straightforward to write. You do not need to write any application logic -- all you need is a single Equation expression to get started. These do not have to be static expressions either. As you write your expressions, you may find yourself wanting to yield some amount of configurability to the user of your rules. Claws, however, allows you to use variables in your expressions that are populated at runtime by whatever values the user provides.
|
6
|
+
|
7
|
+
This is in contrast to common static analysis tools that achieve this by requiring custom application logic to different configuration values as edgecases. For Claws, this means instead of having to write Ruby code that handles parsing configuration options and branching based on those values, you can represent these as options from within your expression.
|
8
|
+
|
9
|
+
While it's important to be able to easily write a Rule, it's just as important (if not more!) to write good tests for them. Like with Rubocop, Claws comes with a couple RSpec helpers that makes it easy to write test cases. Test cases are simply example Workflows that exercise a Rule's expressions, ensuring that a modification to a Rule can't accidentally affect its ability to detect known bad content.
|
10
|
+
|
11
|
+
## Built In Rules
|
12
|
+
|
13
|
+
These are all the rules that come out of the box with Claws. They can all be found in [the rules subdirectory](https://github.com/Betterment/claws/tree/main/lib/claws/rule), and some of them have configuration options.
|
14
|
+
|
15
|
+
### AutomaticMerge
|
16
|
+
|
17
|
+
This rule flags a Github Action that looks like it might attempt to automatically merge a pull request, regardless of the criteria to do so. It makes no attempt at understanding the criteria, but instead it flags to a reviewer that this is happening and the logic behind it should be scrutinized.
|
18
|
+
|
19
|
+
It attempts to detect automatic merges using two heuristics:
|
20
|
+
* any invocation of the [gh cli](https://cli.github.com/manual/gh_pr) that looks like a command that'll merge the PR
|
21
|
+
* any use of a known Github Action that automatically merges.
|
22
|
+
|
23
|
+
#### Configuration Options
|
24
|
+
|
25
|
+
| Option | Default Value | Description |
|
26
|
+
|-------------|------------------------|---------------------------------------------------------------|
|
27
|
+
| `pr_events` | ["push", "pull_request_target", "pull_request", "pull_request_comment", "pull_request_review","pull_request_review_comment", "workflow_dispatch", "workflow_call"] | A list of github events to consider an action capable of automatically merging a pull request |
|
28
|
+
| automerge_actions | ["reitermarkus/automerge", "pascalgn/automerge-action"] | Common Github Actions used to automatically merge a pull request |
|
29
|
+
|
30
|
+
### BulkPermissions
|
31
|
+
|
32
|
+
This rule flags a Github Action that requests `write-all` or `read-all` permissions anywhere.
|
33
|
+
|
34
|
+
Github Actions should list each individual permission it needs to run properly, instead of requesting everything all at once. This helps mitigate the damage that a Github Action that either has a malicious dependency or otherwise has been compromised can do.
|
35
|
+
|
36
|
+
The following workflow for example, requests write permissions for everything:
|
37
|
+
|
38
|
+
```yaml
|
39
|
+
name: Deploy
|
40
|
+
|
41
|
+
on:
|
42
|
+
push:
|
43
|
+
branches:
|
44
|
+
- main
|
45
|
+
|
46
|
+
permissions: write-all
|
47
|
+
|
48
|
+
jobs:
|
49
|
+
build:
|
50
|
+
runs-on: ubuntu-latest
|
51
|
+
steps:
|
52
|
+
- uses: action/checkout@v3
|
53
|
+
- name: push
|
54
|
+
run: rake release
|
55
|
+
```
|
56
|
+
|
57
|
+
But because we know specifically what this workflow needs to run, we can be more explicit about the permissions it requests.
|
58
|
+
|
59
|
+
```yaml
|
60
|
+
name: Deploy
|
61
|
+
|
62
|
+
on:
|
63
|
+
push:
|
64
|
+
branches:
|
65
|
+
- main
|
66
|
+
|
67
|
+
permissions:
|
68
|
+
packages: write
|
69
|
+
|
70
|
+
jobs:
|
71
|
+
build:
|
72
|
+
runs-on: ubuntu-latest
|
73
|
+
steps:
|
74
|
+
- uses: action/checkout@v3
|
75
|
+
- name: push
|
76
|
+
run: rake release
|
77
|
+
```
|
78
|
+
|
79
|
+
### CommandInjection
|
80
|
+
|
81
|
+
This rule looks for Github Actions that run shell commands that are vulnerable to command injection. Specifically, it looks for shell commands that use Github's parameterization feature to embed user input (e.g. `${{ github.event.inputs.name }}`) While the command injection vulnerability may not look obvious, even with quotes around the variable, these variables are expanded before the shell command is executed, so the only way to safely embed these variables in your command is to pass them in as environment variables.
|
82
|
+
|
83
|
+
For example, take the following job:
|
84
|
+
|
85
|
+
```yaml
|
86
|
+
jobs:
|
87
|
+
greet:
|
88
|
+
runs-on: ubuntu-latest
|
89
|
+
steps:
|
90
|
+
- name: Checkout
|
91
|
+
uses: actions/checkout@v1
|
92
|
+
- name: Greet
|
93
|
+
run: ./scripts/greet.sh "${{ github.event.inputs.name }}"
|
94
|
+
```
|
95
|
+
|
96
|
+
A user could trigger this workflow and pass in a name as input with `$(echo hacked)`, causing the final command to look like this:
|
97
|
+
|
98
|
+
```
|
99
|
+
./scripts/greet.sh "$(echo hacked)"
|
100
|
+
```
|
101
|
+
|
102
|
+
which naturally, will execute the user's code in your workflow. This would give an attacker the ability to execute arbitrary code in the context of your workflow, so if it has access to credentials or sensitive files, they would be able to access those too. To address this vulnerablity, we would pass this input in as an environment variable:
|
103
|
+
|
104
|
+
```yaml
|
105
|
+
jobs:
|
106
|
+
greet:
|
107
|
+
runs-on: ubuntu-latest
|
108
|
+
steps:
|
109
|
+
- name: Checkout
|
110
|
+
uses: actions/checkout@v1
|
111
|
+
- name: Greet
|
112
|
+
env:
|
113
|
+
NAME: ${{ github.events.inputs.name }}
|
114
|
+
run: ./scripts/greet.sh "$NAME"
|
115
|
+
```
|
116
|
+
|
117
|
+
For more information, check [Github's official blog post on these bugs](https://github.blog/security/supply-chain-security/four-tips-to-keep-your-github-actions-workflows-secure/#understanding-command-injection-vulnerabilities-in-github-actions-workflows).
|
118
|
+
|
119
|
+
### EmptyName
|
120
|
+
|
121
|
+
This rule flags Github Actions that have empty names. Not specifying a name makes it harder to distinguish workflows, especially in repositories with many of them.
|
122
|
+
|
123
|
+
### InheritedSecrets
|
124
|
+
|
125
|
+
This rule flags Github Actions that allow reusable workflows to inherit secrets from the calling workflow. This means any secrets pulled in by a workflow will be accessible to the reusable workflow, even if it doesn't need them to function. This can be problematic for bigger workflows that do many things and pull in many secrets, especially when new secrets are added that will quietly be passed to the reusable workflow.
|
126
|
+
|
127
|
+
The workflow should be called with just the secrets it needs to run.
|
128
|
+
|
129
|
+
For example, this workflow uses a reusable workflow with `secrets` set to `inherit`. From simply reading the code, it's not clear what secrets it needs to run; you would need to read the workflow's source code to determine that for yourself, and there's still no guarantee that won't change:
|
130
|
+
|
131
|
+
```yaml
|
132
|
+
jobs:
|
133
|
+
call-workflow-passing-data:
|
134
|
+
uses: octo-org/example-repo/.github/workflows/reusable-workflow.yml@main
|
135
|
+
secrets: inherit
|
136
|
+
```
|
137
|
+
|
138
|
+
Instead, we can pass in a specific secret:
|
139
|
+
|
140
|
+
```yaml
|
141
|
+
jobs:
|
142
|
+
call-workflow-passing-data:
|
143
|
+
uses: octo-org/example-repo/.github/workflows/reusable-workflow.yml@main
|
144
|
+
secrets:
|
145
|
+
access-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
146
|
+
```
|
147
|
+
|
148
|
+
Being explicit about secrets keeps us safe and makes our code reviewers' lives easier.
|
149
|
+
|
150
|
+
For more information, check out [Github's official documentation on passing in secrets](https://docs.github.com/en/actions/sharing-automations/reusing-workflows#passing-inputs-and-secrets-to-a-reusable-workflow).
|
151
|
+
|
152
|
+
### NoContainers
|
153
|
+
|
154
|
+
This rule flags any actions that use non-standard container images. Because using a container image can obscure the purpose of a step, some organizations may want to limit their use.
|
155
|
+
|
156
|
+
As an alternative, you can either
|
157
|
+
* Ignore this finding for a one off scenario.
|
158
|
+
* Opt out of using a container altogether.
|
159
|
+
* Configure the rule to allow only specific container images.
|
160
|
+
|
161
|
+
By default, the container image allowlist is empty. You could for example, add `ubuntu-latest` to that list if this is an image you're comfortable with developers using in their workflows.
|
162
|
+
|
163
|
+
#### Configuration Options
|
164
|
+
|
165
|
+
| Option | Default Value | Description |
|
166
|
+
|-------------|------------------------|---------------------------------------------------------------|
|
167
|
+
| `approved_images` | [] | An array of approved container images |
|
168
|
+
|
169
|
+
### RiskyTriggers
|
170
|
+
|
171
|
+
This rule flags actions that have triggers that may have unintended side effects. By default, this rule looks for two triggers:
|
172
|
+
|
173
|
+
* `pull_request_target`: This trigger allows an action to run by default in the context of a user's supplied branch. This can lead to unintended consequences where code is executed from the branch, giving an attacker code execution in the context of any of your secrets or other sensitive data. Check out [Github's blog post on this topic](https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/) for more information.
|
174
|
+
* `workflow_dispatch`: This trigger executes a workflow outside of the typical pull request flow. Anyone with push permissions for the repository can trigger this workflow which sometimes is desirable, but sometimes can be a surprise, especially for larger Github organizations. You may want someone to be able to write to a repository, but not execute code with any associated secrets.
|
175
|
+
|
176
|
+
To review changes to Github Actions that use `pull_request_target` or `workflow_dispatch`, asking the following questions should help:
|
177
|
+
* Does this workflow fetch code from a user supplied branch and execute any of it? Keep in mind that code execution can happen indirectly e.g. an `npm install` command may execute code from the user's branch, even if the step in your Github Action doesn't take user input directly.
|
178
|
+
* In the event we do need to execute user supplied code (e.g. tests), are we passing any credentials to it? If we are, what are the capabilities of these credentials? Can we use these credentials in a separate job to isolate them from user code?
|
179
|
+
|
180
|
+
In some cases, a workflow can be rewritten to not need either of these permissions. In other cases, this is impossible. This rule exists to flag to the code reviewer that this is a risk that needs to be weighed.
|
181
|
+
|
182
|
+
If one of these triggers is one you've already accounted for in your threat model, you can remove it from the `risky_triggers`, or you could add new ones altogether.
|
183
|
+
|
184
|
+
#### Configuration Options
|
185
|
+
|
186
|
+
| Option | Default Value | Description |
|
187
|
+
|-------------|------------------------|---------------------------------------------------------------|
|
188
|
+
| `risky_triggers` | ["pull_request_target", "workflow_dispatch"] | An array of triggers you consider risky. |
|
189
|
+
|
190
|
+
### Shellcheck
|
191
|
+
|
192
|
+
This rule runs [Shellcheck](https://github.com/koalaman/shellcheck) on shell commands. Effectively, this rule forks off to `shellcheck` and any non-zero complaints it has are considered findings.
|
193
|
+
|
194
|
+
Shellcheck is a great tool for dealing with bugs or otherwise unintended effects in shell commands, some of which can result in vulnerabilities. As with running `shellcheck` individually, you can ignore specific findings in shell commands embedded inside workflows.
|
195
|
+
|
196
|
+
#### Configuration Options
|
197
|
+
|
198
|
+
| Option | Default Value | Description |
|
199
|
+
|-------------|------------------------|---------------------------------------------------------------|
|
200
|
+
| `shellcheck_bin` | "/opt/homebrew/bin/shellcheck" | A string that contains the path to the shellcheck binary on your system. |
|
201
|
+
|
202
|
+
|
203
|
+
### SpecialPermissions
|
204
|
+
|
205
|
+
This rule flags workflows that request write access to specific unusual permissions. While this rule cannot flag how these permissions are exercised, it serves as a warning to code reviewers that if these permissions are requested, the way they are used should be scrutinized. A reviewer may find that a permission is left over from testing and no longer needed, or that a specific permission was never needed.
|
206
|
+
|
207
|
+
### UnapprovedRunners
|
208
|
+
|
209
|
+
This rule flags workflows that use runners that they might not need or should not use. This can come in handy when an organization has available self hosted or otherwise expensive runners but wants to be particular about when they're used.
|
210
|
+
|
211
|
+
Like with some other rules, this rule doesn't inspect the way a runner is used. Instead, it is meant to signal to code reviewers that the author may be doing something they shouldn't be. These findings can be resolved in a couple ways:
|
212
|
+
* Add a one off exception for this workflow.
|
213
|
+
* Use a different runner.
|
214
|
+
* Add the runner to the `allowed_runners` configuration.
|
215
|
+
|
216
|
+
See [Github's documentation for `runs-on`](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on) for more information.
|
217
|
+
|
218
|
+
#### Configuration Options
|
219
|
+
|
220
|
+
| Option | Default Value | Description |
|
221
|
+
|-------------|------------------------|---------------------------------------------------------------|
|
222
|
+
| `allowed_runners` | ["ubuntu-latest"] | An array containing the types of runners a workflow is allowed to use. |
|
223
|
+
|
224
|
+
### UnpinnedActions
|
225
|
+
|
226
|
+
This rule flags any use of reusable actions that do not pin to a specific commit hash. This is intended to catch potential supply chain issues where a workflow references a third party workflow that may later be modified to introduce a vulnerability or otherwise malicious code. By pinning to a specific commit hash, you can mitigate these risks by ensuring that the code your workflow depends on doesn't change without you knowing.
|
227
|
+
|
228
|
+
For example:
|
229
|
+
|
230
|
+
```yaml
|
231
|
+
name: CI
|
232
|
+
|
233
|
+
on: push
|
234
|
+
|
235
|
+
jobs:
|
236
|
+
checkout:
|
237
|
+
runs-on: ubuntu
|
238
|
+
steps:
|
239
|
+
- uses: coolworkflows/very_safe_checkout
|
240
|
+
```
|
241
|
+
|
242
|
+
This workflow uses the third party `coolworkflows/very_safe_checkout` workflow. Because a commit hash isn't specified, we end up pulling in the latest version of this workflow every time our workflow runs. This means we are at risk of pulling untested code which at worst can introduce a vulnerability or malicious code, and at best introduce backwards incompatible code that breaks our workflow.
|
243
|
+
|
244
|
+
This workflow should be rewritten to reference a specific commit hash, ensuring that the code we tested with is the code we will always use:
|
245
|
+
|
246
|
+
```yaml
|
247
|
+
name: CI
|
248
|
+
|
249
|
+
on: push
|
250
|
+
|
251
|
+
jobs:
|
252
|
+
checkout:
|
253
|
+
runs-on: ubuntu
|
254
|
+
steps:
|
255
|
+
- uses: coolworkflows/very_safe_checkout@436766774e42e826479ba5868232e5a9c8986887
|
256
|
+
```
|
257
|
+
|
258
|
+
Now every time we run our workflow, we pull in the same version of `very_safe_checkout` every single time, making it difficult for unchecked code to make its way into our workflows.
|
259
|
+
|
260
|
+
Because it may be tedious to use specific commit hashes, you can allowlist specific organizations whose actions you consider to be trusted (e.g. the official `actions` organization, or your own organization).
|
261
|
+
|
262
|
+
Check out [Github's official documentation on the different techniques for pinning](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-third-party-actions) to better understand the risks they come with.
|
263
|
+
|
264
|
+
#### Configuration Options
|
265
|
+
|
266
|
+
| Option | Default Value | Description |
|
267
|
+
|-------------|------------------------|---------------------------------------------------------------|
|
268
|
+
| `trusted_authors` | [] | An array of github organizations that contain workflows that you trust. |
|
269
|
+
|
270
|
+
### UnsafeCheckout
|
271
|
+
|
272
|
+
This rule flags workflows that may check out user supplied code in an unsafe way. Workflows that do this are at high risk of introducing arbitrary code execution vulnerabilities where user supplied code is executed in a trusted environment with secrets and other sensitive data. It does this by looking at any uses of the `actions/checkout` action with user supplied input.
|
273
|
+
|
274
|
+
Like with many other rules, this rule cannot check for you that this is done being safely. Instead, it serves as a flag for code reviewers to double check that the logic used to fetch user supplied code and the way that code is used is safe. For example, running a linter on user supplied code may be safe, but executing a script in a branch given to us by the user is not.
|
275
|
+
|
276
|
+
For example, take this workflow that checks out code supplied by the user:
|
277
|
+
|
278
|
+
```yaml
|
279
|
+
on: [pull_request_target]
|
280
|
+
|
281
|
+
jobs:
|
282
|
+
build:
|
283
|
+
name: Build
|
284
|
+
runs-on: ubuntu-latest
|
285
|
+
steps:
|
286
|
+
# check out the attacker controlled branch with their code
|
287
|
+
- uses: actions/checkout@v2
|
288
|
+
with:
|
289
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
290
|
+
|
291
|
+
# set up the environment and run specs
|
292
|
+
# because Rakefile comes from the attacker's branch
|
293
|
+
# we end up executing their code, even though they don't
|
294
|
+
# control the command here
|
295
|
+
- run: |
|
296
|
+
rake setup
|
297
|
+
rake spec
|
298
|
+
```
|
299
|
+
|
300
|
+
This workflow runs unit tests in a user supplied branch. Unit tests are just arbitrary code, so a user could create a unit test that for example dumps all the secrets in environment variables for them to look at. In this case, the workflow may need to be rewritten so that tests only run after being merged (i.e. approved by a code reviewer and confirmed to not have any malicious code) or the strategy may need to be scrapped altogether.
|
301
|
+
|
302
|
+
This rule only looks for user supplied branches being checked out for `pull_request_target` and `workflow_dispatch` triggers. Depending on your threat model, you may need to configure `risky_events` appropriately, for example if you trust your Github organization settings enough to be comfortable with all uses of `workflow_dispatch`.
|
303
|
+
|
304
|
+
#### Configuration Options
|
305
|
+
|
306
|
+
| Option | Default Value | Description |
|
307
|
+
|-------------|------------------------|---------------------------------------------------------------|
|
308
|
+
| `risky_events` | ["pull_request_target", "workflow_dispatch"] | An array of Github events you consider risky. |
|
309
|
+
|
310
|
+
## Walkthrough
|
311
|
+
|
312
|
+
Let's start with a minimal configuration file that enables some basic Rules.
|
313
|
+
|
314
|
+
```yaml
|
315
|
+
Enabled:
|
316
|
+
AutomaticMerge:
|
317
|
+
UnsafeCheckout:
|
318
|
+
UnpinnedAction:
|
319
|
+
```
|
320
|
+
|
321
|
+
and here's the Workflow file we'll be testing:
|
322
|
+
|
323
|
+
```yaml
|
324
|
+
on: push
|
325
|
+
name: Pretend to Build
|
326
|
+
jobs:
|
327
|
+
DoNothing:
|
328
|
+
runs-on: ubuntu-latest
|
329
|
+
steps:
|
330
|
+
- name: Checkout
|
331
|
+
uses: actions/checkout
|
332
|
+
```
|
333
|
+
|
334
|
+
Now let's run Claws on this file:
|
335
|
+
|
336
|
+
```
|
337
|
+
$ bundle exec bin/analyze -c config.yml -t corpus/unpinned.yml
|
338
|
+
|
339
|
+
Violation: UnpinnedAction on corpus/unpinned.yml:8
|
340
|
+
All reusable actions must be pinned to a specific version.
|
341
|
+
steps:
|
342
|
+
- name: Checkout
|
343
|
+
>>> uses: actions/checkout
|
344
|
+
```
|
345
|
+
|
346
|
+
It's identified this reusable action as problematic because it's not pinned to a specific version. You can read [Github's own docs](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions) on the topic, but in short, an unpinned version means the contents of the Action can change without your Workflow's contents changing, running the risk of executing new and undesired code.
|
347
|
+
|
348
|
+
Now we have several options for remediation here.
|
349
|
+
|
350
|
+
* **We could ignore this finding (please don't)**
|
351
|
+
|
352
|
+
We can do this by adding a comment to this Workflow to let Claws know we want to continue being a bad person. There are some scenarios where this is totally fine, e.g. an overzealous expression, or some other compensating control that Claws has no insight into (e.g. your Github org doesn't allow externally sourced Actions)
|
353
|
+
|
354
|
+
```yaml
|
355
|
+
on: push
|
356
|
+
name: Pretend to Build
|
357
|
+
jobs:
|
358
|
+
DoNothing:
|
359
|
+
runs-on: ubuntu-latest
|
360
|
+
steps:
|
361
|
+
- name: Checkout
|
362
|
+
# ignore: UnpinnedAction
|
363
|
+
uses: actions/checkout
|
364
|
+
```
|
365
|
+
|
366
|
+
This not only tells Claws to avoid surfacing this finding, but it also signals to other developers that this is bad behavior but was explicitly allowed. Some `git blame` spelunking may help them understand the context better.
|
367
|
+
|
368
|
+
* **We could tell the Rule that `actions` is a trusted developer.**
|
369
|
+
|
370
|
+
Because `actions` is Github's official account for their reusable actions, we could simply tell the Rule to avoid flagging this. We can update our config from earlier to look a little something like this:
|
371
|
+
|
372
|
+
```yaml
|
373
|
+
Enabled:
|
374
|
+
AutomaticMerge:
|
375
|
+
UnsafeCheckout:
|
376
|
+
UnpinnedAction:
|
377
|
+
trusted_authors: ["actions"]
|
378
|
+
```
|
379
|
+
|
380
|
+
Now not only have we remediated this specific finding, but any other Workflows that use an action from Github's official account without pinning it will not be treated as a violation.
|
381
|
+
|
382
|
+
Note, configuration options like `trusted_authors` are specific to the individual Rules. Each Rule defines what values it can pull from your configuration file. We'll cover this in technical depth below.
|
383
|
+
|
384
|
+
* **And of course, we could just do the right thing and pin the version.**
|
385
|
+
|
386
|
+
At the time of writing, the latest version of the `action/checkout` action is 3.5.3, with a corresponding commit hash of `c85c95e3d7251135ab7dc9ce3241c5835cc595a9`. A full length commit hash is the only way to get an immutable reference to code at a specific point in time, so we can just add that to our Workflow and be done with it:
|
387
|
+
|
388
|
+
```yaml
|
389
|
+
on: push
|
390
|
+
name: Pretend to Build
|
391
|
+
jobs:
|
392
|
+
DoNothing:
|
393
|
+
runs-on: ubuntu-latest
|
394
|
+
steps:
|
395
|
+
- name: Checkout
|
396
|
+
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9
|
397
|
+
```
|
398
|
+
|
399
|
+
With that being said, it's up to you to decide how to remediate violations. Claws makes it easy to do that on your own terms.
|
400
|
+
|
401
|
+
## Anatomy of A Rule
|
402
|
+
|
403
|
+
Here's the source to the `UnpinnedAction` Rule from above.
|
404
|
+
|
405
|
+
```ruby
|
406
|
+
class UnpinnedAction < Rule
|
407
|
+
description "All reusable actions must be pinned to a specific version."
|
408
|
+
|
409
|
+
on_step %(
|
410
|
+
$action != null &&
|
411
|
+
(
|
412
|
+
$action.version == null ||
|
413
|
+
contains(["main", "master"], $action.version)
|
414
|
+
) &&
|
415
|
+
!contains($data.trusted_authors, $action.author)
|
416
|
+
), highlight: "uses"
|
417
|
+
|
418
|
+
def data
|
419
|
+
{
|
420
|
+
"trusted_authors": configuration.fetch("trusted_authors", [])
|
421
|
+
}
|
422
|
+
end
|
423
|
+
end
|
424
|
+
```
|
425
|
+
|
426
|
+
The `on_step` indicates the depth at which this expression will be evaluated. Here, this means for every "step" in a Workflow, this expression will be evaluated. A Rule can have any number of `on_workflow`, `on_job`, and `on_step` expressions. This can come in handy if your expression is getting a bit unwieldy. It might make more sense to break it up into multiple expressions.
|
427
|
+
|
428
|
+
To give users some "breathing room", this Rule has a `trusted_authors` configuration option which is exposed to the expression as a variable via `$data.trusted_authors`. By default, its value is an empty array, meaning users who don't need this feature don't need to use it. This is part of what makes Claws' Rules so powerful: expressions are easy to read and fully configurable if you want them to be.
|
429
|
+
|
430
|
+
## Debug Mode
|
431
|
+
|
432
|
+
Here's a little walkthrough on how to use it. Let's start by setting one of the expressions in `AutomaticMerge` to debug: true.
|
433
|
+
|
434
|
+
```diff
|
435
|
+
diff --git a/lib/rules/automatic_merge.rb b/lib/rules/automatic_merge.rb
|
436
|
+
index 076e995..e16da33 100644
|
437
|
+
--- a/lib/rules/automatic_merge.rb
|
438
|
+
+++ b/lib/rules/automatic_merge.rb
|
439
|
+
@@ -11,11 +11,11 @@ class AutomaticMerge < Rule
|
440
|
+
|
441
|
+
on_step %(
|
442
|
+
contains_any($workflow.on, $data.pr_events) && (
|
443
|
+
$action.name in $data.automerge_actions
|
444
|
+
)
|
445
|
+
- ), highlight: "uses"
|
446
|
+
+ ), highlight: "uses", debug: true
|
447
|
+
|
448
|
+
def data
|
449
|
+
{
|
450
|
+
"automerge_actions":
|
451
|
+
configuration.fetch("automerge_actions", default_automerge_actions),
|
452
|
+
```
|
453
|
+
|
454
|
+
Now any invocation of this rule will automatically drop you into a REPL. This is most useful when combined with a specific test case. For example, this rule has five test cases:
|
455
|
+
|
456
|
+
```
|
457
|
+
$ bundle exec rspec spec/rules/automatic_merge_spec.rb
|
458
|
+
|
459
|
+
AutomaticMerge
|
460
|
+
with default configuration
|
461
|
+
flags a step that uses an automerge action
|
462
|
+
flags a step that uses the CLI to merge a PR
|
463
|
+
doesn't flag a step for using an unrelated action
|
464
|
+
doesn't flag a step for doing something unrelated with the CLI
|
465
|
+
with a custom configuration
|
466
|
+
flags a step that uses an automerge action
|
467
|
+
|
468
|
+
Finished in 0.007 seconds (files took 0.21 seconds to load)
|
469
|
+
5 examples, 0 failures
|
470
|
+
```
|
471
|
+
|
472
|
+
We don't want to be dropped into a REPL for all five cases. In a real world scenario, you'd probably pick a test case that's failing and you're not sure why. In my scenario, all the tests are passing, so let's just do the first one.
|
473
|
+
|
474
|
+
```
|
475
|
+
$ bundle exec rspec spec/rules/automatic_merge_spec.rb:7
|
476
|
+
Run options: include {:locations=>{"./spec/rules/automatic_merge_spec.rb"=>[7]}}
|
477
|
+
|
478
|
+
AutomaticMerge
|
479
|
+
with default configuration
|
480
|
+
!!! CLAWS DEBUG !!!
|
481
|
+
<Expression '
|
482
|
+
contains_any($workflow.on, $data.pr_events) && (
|
483
|
+
$action.name in $data.automerge_actions
|
484
|
+
)
|
485
|
+
'> returned true
|
486
|
+
Tips:
|
487
|
+
* values available in @debug_values
|
488
|
+
* eval a test expression: e 'expression'
|
489
|
+
* ^D to exit
|
490
|
+
|
491
|
+
From: /Users/omar/src/unbungle/lib/application.rb:194 Application#enter_debug:
|
492
|
+
|
493
|
+
184: def enter_debug(result:, expression:, values:)
|
494
|
+
185: @debug_values = values
|
495
|
+
186:
|
496
|
+
187: require 'pry'
|
497
|
+
188: puts "!!! CLAWS DEBUG !!!".red
|
498
|
+
189: puts "#{expression} returned #{result}".red
|
499
|
+
190: puts "Tips:"
|
500
|
+
191: puts "* values available in @debug_values".green
|
501
|
+
192: puts "* eval a test expression: e 'expression'".green
|
502
|
+
193: puts "* ^D to exit".green
|
503
|
+
=> 194: binding.pry
|
504
|
+
195: end
|
505
|
+
|
506
|
+
[1] pry(#<Application>)>
|
507
|
+
```
|
508
|
+
|
509
|
+
This is a regular `binding.pry` REPL, but instead of having to navigate project internals, you can look at the immediately local environment (printed in the tips!). If we're not sure why this test is failing, let's see if the rule is parsed correctly by checking what it thinks the action's name is, and what it's being checked against:
|
510
|
+
|
511
|
+
```
|
512
|
+
[1] pry(#<Application>)> e '$action.name'
|
513
|
+
"pascalgn/automerge-action"
|
514
|
+
=> nil
|
515
|
+
[2] pry(#<Application>)> e '$data.automerge_actions'
|
516
|
+
["reitermarkus/automerge", "pascalgn/automerge-action"]
|
517
|
+
=> nil
|
518
|
+
```
|
519
|
+
|
520
|
+
Ok, we can see that it's properly parsed the action name is correctly checking against this list. Because we're evaluating arbitrary expressions, we can evaluate entire subsets of our rule to validate that they work, piece by piece:
|
521
|
+
|
522
|
+
```
|
523
|
+
[4] pry(#<Application>)> e '$action.name in $data.automerge_actions'
|
524
|
+
true
|
525
|
+
=> nil
|
526
|
+
```
|
527
|
+
We can also get a full snapshot of the local environment as the rule executed, in case there's any clues there:
|
528
|
+
|
529
|
+
```
|
530
|
+
[5] pry(#<Application>)> @debug_values
|
531
|
+
=> {:data=>
|
532
|
+
{:automerge_actions=>["reitermarkus/automerge", "pascalgn/automerge-action"],
|
533
|
+
:pr_events=>
|
534
|
+
["push",
|
535
|
+
"pull_request_target",
|
536
|
+
"pull_request",
|
537
|
+
"pull_request_comment",
|
538
|
+
"pull_request_review",
|
539
|
+
"pull_request_review_comment",
|
540
|
+
"workflow_dispatch",
|
541
|
+
"workflow_call"]},
|
542
|
+
:workflow=>
|
543
|
+
{"name"=>"Automerge via Github Action",
|
544
|
+
"on"=>"pull_request",
|
545
|
+
"jobs"=>{"deploy"=>{"steps"=>[{"id"=>"merge this pull request", "name"=>"automerge", "uses"=>"pascalgn/automerge-action@v0.15.5"}], "runs_on"=>nil}}},
|
546
|
+
:job=>{"steps"=>[{"id"=>"merge this pull request", "name"=>"automerge", "uses"=>"pascalgn/automerge-action@v0.15.5"}], "runs_on"=>nil},
|
547
|
+
:step=>{"id"=>"merge this pull request", "name"=>"automerge", "uses"=>"pascalgn/automerge-action@v0.15.5"},
|
548
|
+
:action=>{"name"=>"pascalgn/automerge-action", "version"=>"v0.15.5", "author"=>"pascalgn"},
|
549
|
+
:secrets=>[]}
|
550
|
+
```
|
551
|
+
|
552
|
+
Since this rule is functioning as desired, we don't need to dig any further. But hopefully this should make rule development a lot easier.
|
553
|
+
|
554
|
+
## Writing Tests
|
555
|
+
|
556
|
+
Rules should have corresponding specs that contain sample Workflows that exercise all the different ways to trigger their expressions. See [specs](./specs/rules/) for more info. More docs on the topic TBD :~)
|
557
|
+
|
data/Rakefile
ADDED
data/bin/analyze
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "claws"
|
5
|
+
require "slop"
|
6
|
+
|
7
|
+
flags = Slop::Options.new
|
8
|
+
flags.banner = "usage: process [options] ..."
|
9
|
+
flags.separator ""
|
10
|
+
flags.separator "Options:"
|
11
|
+
flags.string "-c", "--config", required: true
|
12
|
+
flags.string "-f", "--format", default: "stdout"
|
13
|
+
flags.array "-t", "--target", required: true
|
14
|
+
|
15
|
+
parser = Slop::Parser.new flags
|
16
|
+
begin
|
17
|
+
result = parser.parse ARGV
|
18
|
+
options = result.to_hash
|
19
|
+
rescue Slop::Error => e
|
20
|
+
puts e
|
21
|
+
puts flags
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
|
25
|
+
unless File.exist? options[:config]
|
26
|
+
puts "Couldn't load config file: #{options[:config]}"
|
27
|
+
exit 1
|
28
|
+
end
|
29
|
+
|
30
|
+
missing_files = options[:target].reject { |f| File.file? f }
|
31
|
+
unless missing_files.empty?
|
32
|
+
puts "Couldn't find files: #{missing_files.inspect}"
|
33
|
+
exit 1
|
34
|
+
end
|
35
|
+
|
36
|
+
formatter = case options[:format]
|
37
|
+
when "stdout" then Claws::Formatter::Stdout
|
38
|
+
when "github" then Claws::Formatter::Github
|
39
|
+
else
|
40
|
+
puts "Unknown output format: #{options[:format]}"
|
41
|
+
exit 1
|
42
|
+
end
|
43
|
+
|
44
|
+
app = Claws::Application.new
|
45
|
+
configuration = YAMLWithLines.load(File.open(options[:config]).read)
|
46
|
+
enabled_detections = configuration.fetch("Enabled", {}).keys
|
47
|
+
enabled_detections.each do |detection_name|
|
48
|
+
data = configuration["Enabled"][detection_name] || {}
|
49
|
+
detection = Object.const_get("Claws::Rule::#{detection_name}")
|
50
|
+
.new(configuration: data)
|
51
|
+
app.load_detection(detection)
|
52
|
+
end
|
53
|
+
|
54
|
+
violations_seen = false
|
55
|
+
options[:target].each do |target|
|
56
|
+
violations = app.analyze(target, File.open(target).read)
|
57
|
+
violations_seen = true unless violations.empty?
|
58
|
+
|
59
|
+
formatter.report_violations(violations)
|
60
|
+
end
|
61
|
+
|
62
|
+
exit 1 if violations_seen
|
data/config.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
Enabled:
|
2
|
+
NoContainers:
|
3
|
+
approved_images: ['ubuntu-latest']
|
4
|
+
SpecialPermissions:
|
5
|
+
EmptyName:
|
6
|
+
RiskyTriggers:
|
7
|
+
UnapprovedRunners:
|
8
|
+
allowed_runners: ["ubuntu-latest", "macos-12"]
|
9
|
+
CommandInjection:
|
10
|
+
AutomaticMerge:
|
11
|
+
UnpinnedAction:
|
12
|
+
trusted_authors: ["Betterment"]
|
13
|
+
loose_validation: false
|
14
|
+
UnsafeCheckout:
|
15
|
+
InheritedSecrets:
|
16
|
+
BulkPermissions:
|
@@ -0,0 +1,28 @@
|
|
1
|
+
name: Automerge via Github Action
|
2
|
+
|
3
|
+
on:
|
4
|
+
pull_request:
|
5
|
+
types:
|
6
|
+
- labeled
|
7
|
+
- unlabeled
|
8
|
+
- synchronize
|
9
|
+
- opened
|
10
|
+
- edited
|
11
|
+
- ready_for_review
|
12
|
+
- reopened
|
13
|
+
- unlocked
|
14
|
+
pull_request_review:
|
15
|
+
types:
|
16
|
+
- submitted
|
17
|
+
check_suite:
|
18
|
+
types:
|
19
|
+
- completed
|
20
|
+
status: {}
|
21
|
+
|
22
|
+
jobs:
|
23
|
+
automerge:
|
24
|
+
runs-on: ubuntu-latest
|
25
|
+
steps:
|
26
|
+
- id: automerge
|
27
|
+
name: automerge
|
28
|
+
uses: "pascalgn/automerge-action@v0.15.5"
|