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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +31 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +17 -0
  6. data/Gemfile.lock +99 -0
  7. data/README.md +557 -0
  8. data/Rakefile +12 -0
  9. data/bin/analyze +62 -0
  10. data/config.yml +16 -0
  11. data/corpus/automerge_via_action.yml +28 -0
  12. data/corpus/automerge_via_cli.yml +14 -0
  13. data/corpus/build-docker-image-run-drc-for-cell-gds-using-magic.yml +170 -0
  14. data/corpus/cmd.yml +14 -0
  15. data/corpus/container.yml +19 -0
  16. data/corpus/container_docker.yml +9 -0
  17. data/corpus/dispatch_command_injection.yml +17 -0
  18. data/corpus/inherit_secrets.yml +20 -0
  19. data/corpus/nameless.yml +11 -0
  20. data/corpus/permissions.yml +19 -0
  21. data/corpus/ruby.yml +12 -0
  22. data/corpus/shellcheck.yml +12 -0
  23. data/corpus/unsafe_checkout_code_execution.yml +21 -0
  24. data/corpus/unsafe_checkout_token_leak.yml +33 -0
  25. data/corpus/unscoped_secrets.yml +16 -0
  26. data/github_action.yml +36 -0
  27. data/lib/claws/application.rb +237 -0
  28. data/lib/claws/base_rule.rb +94 -0
  29. data/lib/claws/cli/color.rb +30 -0
  30. data/lib/claws/cli/yaml_with_lines.rb +124 -0
  31. data/lib/claws/engine.rb +25 -0
  32. data/lib/claws/formatter/github.rb +17 -0
  33. data/lib/claws/formatter/stdout.rb +13 -0
  34. data/lib/claws/formatters.rb +4 -0
  35. data/lib/claws/rule/automatic_merge.rb +49 -0
  36. data/lib/claws/rule/bulk_permissions.rb +20 -0
  37. data/lib/claws/rule/command_injection.rb +14 -0
  38. data/lib/claws/rule/empty_name.rb +14 -0
  39. data/lib/claws/rule/inherited_secrets.rb +17 -0
  40. data/lib/claws/rule/no_containers.rb +28 -0
  41. data/lib/claws/rule/risky_triggers.rb +32 -0
  42. data/lib/claws/rule/shellcheck.rb +109 -0
  43. data/lib/claws/rule/special_permissions.rb +37 -0
  44. data/lib/claws/rule/unapproved_runners.rb +31 -0
  45. data/lib/claws/rule/unpinned_action.rb +30 -0
  46. data/lib/claws/rule/unsafe_checkout.rb +36 -0
  47. data/lib/claws/rule.rb +13 -0
  48. data/lib/claws/version.rb +5 -0
  49. data/lib/claws/violation.rb +11 -0
  50. data/lib/claws/workflow.rb +221 -0
  51. data/lib/claws.rb +6 -0
  52. 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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
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"