claws-scan 0.8.0 → 0.9.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +88 -0
- data/bin/analyze +1 -1
- data/example-config.yml +17 -0
- data/lib/claws/application.rb +36 -36
- data/lib/claws/base_rule.rb +5 -4
- data/lib/claws/cli/yaml_with_lines.rb +6 -2
- data/lib/claws/rule/checkout_with_static_credentials.rb +38 -0
- data/lib/claws/rule/command_injection.rb +1 -1
- data/lib/claws/rule.rb +1 -0
- data/lib/claws/version.rb +1 -1
- data/lib/claws/workflow.rb +3 -3
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5f4c3263217e12165f1f7c89c402a4e9cf1085ccfca4d4ab8316d9693569a8bb
|
4
|
+
data.tar.gz: f1bab1dd428690f47e8e81d97892e47d9cd2764233eb93c44f0f704f030e86e0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 766245e17380af395539025794b2461be4be0aa54ab20e8b3fbfb1128032c1228155e398d25bfb6295bd1050ca30d708a3408107b27d006874fa85fd94befc46
|
7
|
+
data.tar.gz: 6ba57d659080d29788dd8de41ff88f4aa6bacb7b2ba2ef0332714e85c13dd111ebe6745bf4b894ce3e905ced0a1cd2a688d200c33447ad06a728633bf8200943
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -8,6 +8,82 @@ This is in contrast to common static analysis tools that achieve this by requiri
|
|
8
8
|
|
9
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
10
|
|
11
|
+
## Getting Started
|
12
|
+
|
13
|
+
Claws is written in Ruby and distributed as a Ruby Gem, so you can install it using `gem`. Just point it at the [example configuration](example-config.yml) file and you should be good to go.
|
14
|
+
|
15
|
+
For one off scans, you can just follow these commands:
|
16
|
+
```
|
17
|
+
# Install claws
|
18
|
+
$ gem install claws-scan
|
19
|
+
|
20
|
+
# Optionally, specify a version
|
21
|
+
$ gem install claws-scan -v 0.7.5
|
22
|
+
|
23
|
+
# Scan a Github Action file
|
24
|
+
analyze -c example_config.yml -t .github/workflows/ci.yml
|
25
|
+
```
|
26
|
+
|
27
|
+
If you'd like to integrate this into your workflow like we have, this should be enough to get you started.
|
28
|
+
|
29
|
+
```yaml
|
30
|
+
name: Workflow Static Analyzer
|
31
|
+
|
32
|
+
on:
|
33
|
+
pull_request:
|
34
|
+
branches:
|
35
|
+
- main
|
36
|
+
|
37
|
+
jobs:
|
38
|
+
build:
|
39
|
+
name: Analyze Github Workflows
|
40
|
+
runs-on: ubuntu-latest
|
41
|
+
steps:
|
42
|
+
- name: Set Up Ruby
|
43
|
+
uses: ruby/setup-ruby@d8d83c3960843afb664e821fed6be52f37da5267 # v1.231.0
|
44
|
+
with:
|
45
|
+
ruby-version: '3.0'
|
46
|
+
# Grab your configuration file however makes sense for you
|
47
|
+
# We keep ours in a separate Github repo.
|
48
|
+
- name: Set Up Claws Config
|
49
|
+
run: |
|
50
|
+
echo ... > /tmp/claws-config.yml
|
51
|
+
# Optional, useful if you want Claws to run shellcheck for you
|
52
|
+
- name: Set Up Shellcheck
|
53
|
+
run: |
|
54
|
+
sudo apt-get update
|
55
|
+
sudo apt-get install -y shellcheck
|
56
|
+
- uses: actions/checkout@v4
|
57
|
+
with:
|
58
|
+
fetch-depth: 0
|
59
|
+
- name: Set Up Claws
|
60
|
+
run: |
|
61
|
+
gem install claws-scan -v 0.7.5
|
62
|
+
- name: Analyze Workflows
|
63
|
+
run: |
|
64
|
+
#!/bin/bash
|
65
|
+
|
66
|
+
# Collect all files in the .github/workflows directory
|
67
|
+
workflow_files=$(find .github/workflows -type f)
|
68
|
+
|
69
|
+
# Exit early if there are no workflow files
|
70
|
+
if [[ -z "$workflow_files" ]]; then
|
71
|
+
echo "No workflow files found in .github/workflows"
|
72
|
+
exit 0
|
73
|
+
fi
|
74
|
+
|
75
|
+
flags=()
|
76
|
+
|
77
|
+
# Iterate over each workflow file
|
78
|
+
while IFS= read -r file; do
|
79
|
+
echo "Processing $file"
|
80
|
+
flags+=("-t" "$file")
|
81
|
+
done <<< "$workflow_files"
|
82
|
+
|
83
|
+
# Run the analyze command with all gathered flags
|
84
|
+
analyze -f github -c /tmp/claws-config.yml "${flags[@]}"
|
85
|
+
```
|
86
|
+
|
11
87
|
## Built In Rules
|
12
88
|
|
13
89
|
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.
|
@@ -204,6 +280,18 @@ Shellcheck is a great tool for dealing with bugs or otherwise unintended effects
|
|
204
280
|
|
205
281
|
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
282
|
|
283
|
+
### CheckoutWithStaticCredentials
|
284
|
+
|
285
|
+
This rule flags any uses of `actions/checkout` using static credentials. Using static credentials can pose a risk because these credentials are typically not auditable and can be tricky to rotate. In the event of an incident where they are leaked, incident response to determine the scope of impact may be tough.
|
286
|
+
|
287
|
+
Where possible, the default `$GITHUB_TOKEN` should be used. Its settings can be configured directly from within the workflow. Check [the official documentation](https://docs.github.com/en/actions/tutorials/authenticate-with-github_token#modifying-the-permissions-for-the-github_token) for more information on how to do this.
|
288
|
+
|
289
|
+
If you are using a deploy key via SSH to access a package or otherwise an artifact from another repository, you can instead configure the repository to grant it access explicitly to that other repository. This will give the default `$GITHUB_TOKEN` access to that repository without needing to use a deploy key. To learn more about this, [check out the official documentation](https://docs.github.com/en/packages/learn-github-packages/configuring-a-packages-access-control-and-visibility). This is safer than a static deploy key because the credential is short lived and access can be audited.
|
290
|
+
|
291
|
+
If you need a Github Token to perform some authenticated action where the default `$GITHUB_TOKEN` doesn't do what you need, consider setting up a Github App and using [`actions/create-github-app-token`](https://github.com/actions/create-github-app-token). This will generate a short lived access token, and using an app creates a useful audit trail for what this access token can actually do. Then, use this token in just the build steps where it's needed.
|
292
|
+
|
293
|
+
In some cases, a static access token or deploy key may still be necessary, especially for APIs that are not yet supported by Github App Tokens. In these cases, make sure to limit the scope of the access token to the bare minimum necessary to function.
|
294
|
+
|
207
295
|
### UnapprovedRunners
|
208
296
|
|
209
297
|
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.
|
data/bin/analyze
CHANGED
data/example-config.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Enabled:
|
2
|
+
NoContainers:
|
3
|
+
approved_images: ["ubuntu-latest"]
|
4
|
+
SpecialPermissions:
|
5
|
+
EmptyName:
|
6
|
+
RiskyTriggers:
|
7
|
+
UnapprovedRunners:
|
8
|
+
allowed_runners: ["ubuntu-latest", "macos-latest"]
|
9
|
+
CommandInjection:
|
10
|
+
AutomaticMerge:
|
11
|
+
UnpinnedAction:
|
12
|
+
trusted_authors: ["actions"]
|
13
|
+
UnsafeCheckout:
|
14
|
+
InheritedSecrets:
|
15
|
+
BulkPermissions:
|
16
|
+
Shellcheck:
|
17
|
+
shellcheck_bin: "/usr/bin/shellcheck"
|
data/lib/claws/application.rb
CHANGED
@@ -43,10 +43,10 @@ module Claws
|
|
43
43
|
@detections.each do |detection|
|
44
44
|
detection.on_workflow.each do |rule|
|
45
45
|
violation = run_detection(
|
46
|
-
filename
|
47
|
-
detection
|
48
|
-
rule
|
49
|
-
workflow:
|
46
|
+
filename:,
|
47
|
+
detection:,
|
48
|
+
rule:,
|
49
|
+
workflow:
|
50
50
|
)
|
51
51
|
|
52
52
|
violations << violation if violation
|
@@ -58,7 +58,7 @@ module Claws
|
|
58
58
|
expression: rule[:expression],
|
59
59
|
values: {
|
60
60
|
data: detection.data,
|
61
|
-
workflow:
|
61
|
+
workflow:
|
62
62
|
}
|
63
63
|
)
|
64
64
|
end
|
@@ -72,11 +72,11 @@ module Claws
|
|
72
72
|
@detections.each do |detection|
|
73
73
|
detection.on_job.each do |rule|
|
74
74
|
violation = run_detection(
|
75
|
-
filename
|
76
|
-
detection
|
77
|
-
rule
|
78
|
-
workflow
|
79
|
-
job:
|
75
|
+
filename:,
|
76
|
+
detection:,
|
77
|
+
rule:,
|
78
|
+
workflow:,
|
79
|
+
job:
|
80
80
|
)
|
81
81
|
|
82
82
|
violations << violation if violation
|
@@ -88,8 +88,8 @@ module Claws
|
|
88
88
|
expression: rule[:expression],
|
89
89
|
values: {
|
90
90
|
data: detection.data,
|
91
|
-
workflow
|
92
|
-
job:
|
91
|
+
workflow:,
|
92
|
+
job:
|
93
93
|
}
|
94
94
|
)
|
95
95
|
end
|
@@ -104,12 +104,12 @@ module Claws
|
|
104
104
|
@detections.each do |detection|
|
105
105
|
detection.on_step.each do |rule|
|
106
106
|
violation = run_detection(
|
107
|
-
filename
|
108
|
-
detection
|
109
|
-
rule
|
110
|
-
workflow
|
111
|
-
job
|
112
|
-
step:
|
107
|
+
filename:,
|
108
|
+
detection:,
|
109
|
+
rule:,
|
110
|
+
workflow:,
|
111
|
+
job:,
|
112
|
+
step:
|
113
113
|
)
|
114
114
|
|
115
115
|
violations << violation if violation
|
@@ -121,9 +121,9 @@ module Claws
|
|
121
121
|
expression: rule[:expression],
|
122
122
|
values: {
|
123
123
|
data: detection.data,
|
124
|
-
workflow
|
125
|
-
job
|
126
|
-
step:
|
124
|
+
workflow:,
|
125
|
+
job:,
|
126
|
+
step:
|
127
127
|
}
|
128
128
|
)
|
129
129
|
end
|
@@ -137,19 +137,19 @@ module Claws
|
|
137
137
|
def run_detection(filename:, detection:, rule:, workflow:, job: nil, step: nil) # rubocop:disable Metrics/ParameterLists
|
138
138
|
violation = if rule.is_a? Symbol
|
139
139
|
get_dynamic_violation(
|
140
|
-
detection
|
140
|
+
detection:,
|
141
141
|
method: rule,
|
142
|
-
workflow
|
143
|
-
job
|
144
|
-
step:
|
142
|
+
workflow:,
|
143
|
+
job:,
|
144
|
+
step:
|
145
145
|
)
|
146
146
|
else
|
147
147
|
get_static_violations(
|
148
|
-
detection
|
149
|
-
rule
|
150
|
-
workflow
|
151
|
-
job
|
152
|
-
step:
|
148
|
+
detection:,
|
149
|
+
rule:,
|
150
|
+
workflow:,
|
151
|
+
job:,
|
152
|
+
step:
|
153
153
|
)
|
154
154
|
end
|
155
155
|
|
@@ -164,18 +164,18 @@ module Claws
|
|
164
164
|
def get_dynamic_violation(detection:, method:, workflow:, job:, step:)
|
165
165
|
detection.send(
|
166
166
|
method,
|
167
|
-
workflow
|
168
|
-
job
|
169
|
-
step:
|
167
|
+
workflow:,
|
168
|
+
job:,
|
169
|
+
step:
|
170
170
|
)
|
171
171
|
end
|
172
172
|
|
173
173
|
def get_static_violations(rule:, detection:, workflow:, job:, step:)
|
174
174
|
result = rule[:expression].eval_with(values: {
|
175
175
|
data: detection.data,
|
176
|
-
workflow
|
177
|
-
job
|
178
|
-
step:
|
176
|
+
workflow:,
|
177
|
+
job:,
|
178
|
+
step:
|
179
179
|
})
|
180
180
|
|
181
181
|
return unless result
|
data/lib/claws/base_rule.rb
CHANGED
@@ -14,6 +14,7 @@ class BaseRule
|
|
14
14
|
endswith: ->(string, needle) { string.to_s.end_with? needle },
|
15
15
|
difference: ->(arr1, arr2) { arr1.difference arr2 },
|
16
16
|
intersection: ->(arr1, arr2) { arr1.intersection arr2 },
|
17
|
+
get_key: ->(arr, key) { (arr || {}).fetch(key, nil) },
|
17
18
|
count: ->(n) { n.length }
|
18
19
|
}
|
19
20
|
)
|
@@ -45,23 +46,23 @@ class BaseRule
|
|
45
46
|
end
|
46
47
|
|
47
48
|
def self.on_workflow(value, highlight: nil, debug: false)
|
48
|
-
(@on_workflow ||= []) << extract_value(value, highlight
|
49
|
+
(@on_workflow ||= []) << extract_value(value, highlight:, debug:)
|
49
50
|
end
|
50
51
|
|
51
52
|
def self.on_job(value, highlight: nil, debug: false)
|
52
53
|
highlight = highlight.to_s unless highlight.nil?
|
53
|
-
(@on_job ||= []) << extract_value(value, highlight
|
54
|
+
(@on_job ||= []) << extract_value(value, highlight:, debug:)
|
54
55
|
end
|
55
56
|
|
56
57
|
def self.on_step(value, highlight: nil, debug: false)
|
57
58
|
highlight = highlight.to_s unless highlight.nil?
|
58
|
-
(@on_step ||= []) << extract_value(value, highlight
|
59
|
+
(@on_step ||= []) << extract_value(value, highlight:, debug:)
|
59
60
|
end
|
60
61
|
|
61
62
|
def self.extract_value(value, highlight: nil, debug: false)
|
62
63
|
case value
|
63
64
|
when String
|
64
|
-
{ expression: parse_rule(value), highlight
|
65
|
+
{ expression: parse_rule(value), highlight:, debug: }
|
65
66
|
when Symbol
|
66
67
|
value
|
67
68
|
else
|
@@ -18,7 +18,9 @@ module Psych
|
|
18
18
|
class ToRuby
|
19
19
|
def accept(target)
|
20
20
|
s = super(target)
|
21
|
-
|
21
|
+
|
22
|
+
# types that we cannot monkey patch into holding line information
|
23
|
+
if target.respond_to?(:line) and ![TrueClass, FalseClass, NilClass, Integer, Float].include? s.class
|
22
24
|
s.instance_eval do
|
23
25
|
extend(Locatable)
|
24
26
|
end
|
@@ -49,7 +51,9 @@ module Psych
|
|
49
51
|
key.line = 0 if key.respond_to? :line and key.line.nil?
|
50
52
|
key.line += 1 if key.respond_to? :line
|
51
53
|
key.freeze
|
52
|
-
|
54
|
+
|
55
|
+
# types that we cannot monkey patch into holding line information
|
56
|
+
if [TrueClass, FalseClass, NilClass, Integer, Float].include? key.class
|
53
57
|
val.line = 0 if val.respond_to? :line and val.line.nil?
|
54
58
|
val.line += 1 if val.respond_to? :line
|
55
59
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Claws
|
2
|
+
module Rule
|
3
|
+
class CheckoutWithStaticCredentials < BaseRule
|
4
|
+
description <<~DESC
|
5
|
+
Avoid using static credentials like deploy keys, SSH keys, or personal access
|
6
|
+
tokens to clone other repositories. Static credentials can be tricky to audit
|
7
|
+
and rotate, making them risky to hold onto, especially in the event of an
|
8
|
+
incident where they may be leaked.
|
9
|
+
|
10
|
+
Either grant your repository access directly to other repositories, or use a
|
11
|
+
Github App to generate a short lived access token.
|
12
|
+
|
13
|
+
For more information:
|
14
|
+
https://github.com/betterment/claws/blob/main/README.md#checkoutwithstaticcredentials
|
15
|
+
DESC
|
16
|
+
|
17
|
+
on_step %(
|
18
|
+
$step.meta.action.name == "actions/checkout" &&
|
19
|
+
(
|
20
|
+
get_key($step.with, "ssh-key") =~ "{{.*secrets\..*" ||
|
21
|
+
get_key($step.with, "ssh-key") =~ "{{.*env\..*" ||
|
22
|
+
get_key($step.with, "ssh-key") =~ "{{.*vars\..*" ||
|
23
|
+
get_key($step.with, "ssh-key") =~ ".*-----BEGIN.*"
|
24
|
+
)
|
25
|
+
), highlight: "with.ssh-key"
|
26
|
+
|
27
|
+
on_step %(
|
28
|
+
$step.meta.action.name == "actions/checkout" &&
|
29
|
+
(
|
30
|
+
get_key($step.with, "token") =~ "{{.*secrets\..*" ||
|
31
|
+
get_key($step.with, "token") =~ "{{.*env\..*" ||
|
32
|
+
get_key($step.with, "token") =~ "{{.*vars\..*" ||
|
33
|
+
get_key($step.with, "token") =~ "gh[a-z]_.*"
|
34
|
+
)
|
35
|
+
), highlight: "with.token"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -8,7 +8,7 @@ module Claws
|
|
8
8
|
https://github.com/betterment/claws/blob/main/README.md#commandinjection
|
9
9
|
DESC
|
10
10
|
|
11
|
-
on_step '$step.run =~ ".*{{
|
11
|
+
on_step '$step.run =~ ".*{{.*(github\.event|inputs)\..*}}.*"', highlight: "run"
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
data/lib/claws/rule.rb
CHANGED
data/lib/claws/version.rb
CHANGED
data/lib/claws/workflow.rb
CHANGED
@@ -173,7 +173,7 @@ class Workflow
|
|
173
173
|
name, version = action.split("@", 2)
|
174
174
|
author = name.split("/", 2)[0]
|
175
175
|
local = author == "."
|
176
|
-
{ type: "action", name
|
176
|
+
{ type: "action", name:, author:, version:, local: }
|
177
177
|
end
|
178
178
|
|
179
179
|
def extract_container_info_from_job(job)
|
@@ -195,8 +195,8 @@ class Workflow
|
|
195
195
|
|
196
196
|
{
|
197
197
|
type: "container",
|
198
|
-
image
|
199
|
-
version
|
198
|
+
image:,
|
199
|
+
version:,
|
200
200
|
full: "#{image}:#{version}"
|
201
201
|
}
|
202
202
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: claws-scan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Omar
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-08-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: equation
|
@@ -85,6 +85,7 @@ files:
|
|
85
85
|
- Rakefile
|
86
86
|
- bin/analyze
|
87
87
|
- config.yml
|
88
|
+
- example-config.yml
|
88
89
|
- lib/claws.rb
|
89
90
|
- lib/claws/application.rb
|
90
91
|
- lib/claws/base_rule.rb
|
@@ -97,6 +98,7 @@ files:
|
|
97
98
|
- lib/claws/rule.rb
|
98
99
|
- lib/claws/rule/automatic_merge.rb
|
99
100
|
- lib/claws/rule/bulk_permissions.rb
|
101
|
+
- lib/claws/rule/checkout_with_static_credentials.rb
|
100
102
|
- lib/claws/rule/command_injection.rb
|
101
103
|
- lib/claws/rule/empty_name.rb
|
102
104
|
- lib/claws/rule/inherited_secrets.rb
|
@@ -123,7 +125,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
123
125
|
requirements:
|
124
126
|
- - ">="
|
125
127
|
- !ruby/object:Gem::Version
|
126
|
-
version: '3.
|
128
|
+
version: '3.2'
|
127
129
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
130
|
requirements:
|
129
131
|
- - ">="
|