claws-scan 0.7.6 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e35f096c235fba21325d4385fd83ac9c7ca2466ca1e82e72311f3079dcd02276
4
- data.tar.gz: c5a5f8206a0f047bf0b49b31ba9976a8c41ad5c3476c8167683140298efcc4a6
3
+ metadata.gz: 5f4c3263217e12165f1f7c89c402a4e9cf1085ccfca4d4ab8316d9693569a8bb
4
+ data.tar.gz: f1bab1dd428690f47e8e81d97892e47d9cd2764233eb93c44f0f704f030e86e0
5
5
  SHA512:
6
- metadata.gz: 8f58f8c09db0ccb0b3c1df00f09710bad07bcc691cb4b41681d5d6e2ff62e157c45742505aa9469d2e6b81d35b6a8ce020863019be057f7b36a780ddf314e65a
7
- data.tar.gz: 0161dad252e79ee9e79d85eb7af21d2eccf1ce5ae6b82d415fcd4a6d060989e5245c57593ca82ad2b7401716ecf5edbd89b3cb9d02bde036e72dcec3d5339ee7
6
+ metadata.gz: 766245e17380af395539025794b2461be4be0aa54ab20e8b3fbfb1128032c1228155e398d25bfb6295bd1050ca30d708a3408107b27d006874fa85fd94befc46
7
+ data.tar.gz: 6ba57d659080d29788dd8de41ff88f4aa6bacb7b2ba2ef0332714e85c13dd111ebe6745bf4b894ce3e905ced0a1cd2a688d200c33447ad06a728633bf8200943
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 3.0
2
+ TargetRubyVersion: 3.2.3
3
3
 
4
4
  Style/StringLiterals:
5
5
  Enabled: true
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- claws-scan (0.7.6)
4
+ claws-scan (0.9.0)
5
5
  equation (~> 0.6)
6
6
  pry
7
7
  slop (~> 4.9)
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
@@ -5,7 +5,7 @@ require "claws"
5
5
  require "slop"
6
6
 
7
7
  flags = Slop::Options.new
8
- flags.banner = "usage: process [options] ..."
8
+ flags.banner = "usage: analyze [options] ..."
9
9
  flags.separator ""
10
10
  flags.separator "Options:"
11
11
  flags.string "-c", "--config", required: true
@@ -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"
@@ -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: filename,
47
- detection: detection,
48
- rule: rule,
49
- workflow: 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: 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: filename,
76
- detection: detection,
77
- rule: rule,
78
- workflow: workflow,
79
- job: 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: workflow,
92
- job: 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: filename,
108
- detection: detection,
109
- rule: rule,
110
- workflow: workflow,
111
- job: job,
112
- step: 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: workflow,
125
- job: job,
126
- step: 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: detection,
140
+ detection:,
141
141
  method: rule,
142
- workflow: workflow,
143
- job: job,
144
- step: step
142
+ workflow:,
143
+ job:,
144
+ step:
145
145
  )
146
146
  else
147
147
  get_static_violations(
148
- detection: detection,
149
- rule: rule,
150
- workflow: workflow,
151
- job: job,
152
- step: 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: workflow,
168
- job: job,
169
- step: 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: workflow,
177
- job: job,
178
- step: step
176
+ workflow:,
177
+ job:,
178
+ step:
179
179
  })
180
180
 
181
181
  return unless result
@@ -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: highlight, debug: debug)
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: highlight, debug: debug)
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: highlight, debug: debug)
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: highlight, debug: debug }
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
- if target.respond_to?(:line) and ![TrueClass, FalseClass, NilClass, Integer].include? s.class
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
- if [TrueClass, FalseClass, NilClass, Integer].include? key.class
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 =~ ".*{{[ ]+.*(github.event|inputs).*}}.*"', highlight: "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
@@ -11,3 +11,4 @@ require "claws/rule/inherited_secrets"
11
11
  require "claws/rule/command_injection"
12
12
  require "claws/rule/bulk_permissions"
13
13
  require "claws/rule/shellcheck"
14
+ require "claws/rule/checkout_with_static_credentials"
data/lib/claws/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Claws
4
- VERSION = "0.7.6"
4
+ VERSION = "0.9.0"
5
5
  end
@@ -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: name, author: author, version: version, local: local }
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: image,
199
- version: 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.7.6
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-05-08 00:00:00.000000000 Z
11
+ date: 2025-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: equation
@@ -85,7 +85,7 @@ files:
85
85
  - Rakefile
86
86
  - bin/analyze
87
87
  - config.yml
88
- - github_action.yml
88
+ - example-config.yml
89
89
  - lib/claws.rb
90
90
  - lib/claws/application.rb
91
91
  - lib/claws/base_rule.rb
@@ -98,6 +98,7 @@ files:
98
98
  - lib/claws/rule.rb
99
99
  - lib/claws/rule/automatic_merge.rb
100
100
  - lib/claws/rule/bulk_permissions.rb
101
+ - lib/claws/rule/checkout_with_static_credentials.rb
101
102
  - lib/claws/rule/command_injection.rb
102
103
  - lib/claws/rule/empty_name.rb
103
104
  - lib/claws/rule/inherited_secrets.rb
@@ -124,7 +125,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
124
125
  requirements:
125
126
  - - ">="
126
127
  - !ruby/object:Gem::Version
127
- version: '3.0'
128
+ version: '3.2'
128
129
  required_rubygems_version: !ruby/object:Gem::Requirement
129
130
  requirements:
130
131
  - - ">="
data/github_action.yml DELETED
@@ -1,36 +0,0 @@
1
- name: Workflow Static Analyzer
2
-
3
- on:
4
- pull_request:
5
- branches:
6
- - main
7
-
8
- jobs:
9
- build:
10
- runs-on: ubuntu-latest
11
- name: Static Analyze
12
- steps:
13
- - name: Set Up Ruby
14
- uses: ruby/setup-ruby@v1
15
- with:
16
- ruby-version: '3.0'
17
- - uses: actions/checkout@v3
18
- with:
19
- fetch-depth: 0
20
- - name: Get PR diff Files
21
- uses: technote-space/get-diff-action@v5
22
- id: modified_actions
23
- with:
24
- PATTERNS: .github/workflows/*.y*ml
25
- - name: Set Up Claws
26
- run: |
27
- gem install --source "https://${{ secrets.BETTERMENT_GH_PACKAGES_PAT }}@rubygems.pkg.github.com/betterment" claws --version "0.1.4"
28
- - name: Analyze New/Changed Workflows
29
- run: |
30
- bungler_flags=""
31
- for workflow in ${{ env.GIT_DIFF }}
32
- do
33
- echo "$workflow"
34
- bungler_flags="-t $workflow $bungler_flags"
35
- done
36
- analyze -f github -c .claws-config.yml $bungler_flags