|
2 | 2 | # frozen_string_literal: true |
3 | 3 |
|
4 | 4 | require "minitest/autorun" |
| 5 | +require "date" |
5 | 6 | require "fileutils" |
6 | 7 | require "tmpdir" |
7 | 8 | require "yaml" |
8 | 9 |
|
9 | 10 | class RepositorySecurityPolicyTest < Minitest::Test |
10 | 11 | ROOT = File.expand_path("..", __dir__) |
| 12 | + YAML_TIMESTAMP_CLASSES = [Date, Time].freeze |
11 | 13 |
|
12 | 14 | def test_github_actions_are_pinned_to_full_commit_shas |
13 | 15 | reference_sets = Dir.glob(File.join(ROOT, ".github/workflows/*.{yml,yaml}")).map do |path| |
14 | | - workflow = YAML.safe_load_file(path, aliases: true) |
15 | | - [path, workflow_uses(workflow)] |
| 16 | + [path, workflow_uses(load_yaml_file(path))] |
16 | 17 | end |
17 | 18 | reference_sets.concat(action_paths.map do |path| |
18 | | - action = YAML.safe_load_file(path, aliases: true) |
19 | | - [path, composite_uses(action)] |
| 19 | + [path, composite_uses(load_yaml_file(path))] |
20 | 20 | end) |
21 | 21 |
|
22 | 22 | mutable_uses = reference_sets.flat_map do |path, references| |
@@ -54,6 +54,35 @@ def test_action_reference_scanner_reads_composite_action_structure |
54 | 54 | assert_equal [["runs.steps.0.uses", "owner/action@v1"]], composite_uses(action) |
55 | 55 | end |
56 | 56 |
|
| 57 | + def test_action_reference_scanner_accepts_timestamp_scalars_and_still_rejects_mutable_uses |
| 58 | + sha = "0123456789abcdef0123456789abcdef01234567" |
| 59 | + |
| 60 | + Dir.mktmpdir("repository-security-policy") do |root| |
| 61 | + immutable_workflow = File.join(root, "immutable.yml") |
| 62 | + mutable_workflow = File.join(root, "mutable.yml") |
| 63 | + File.write(immutable_workflow, <<~YAML) |
| 64 | + generated_on: 2026-08-02 |
| 65 | + jobs: |
| 66 | + validate: |
| 67 | + steps: [{ uses: owner/action@#{sha} }] |
| 68 | + YAML |
| 69 | + File.write(mutable_workflow, <<~YAML) |
| 70 | + generated_at: 2026-08-02T12:34:56Z |
| 71 | + jobs: |
| 72 | + validate: |
| 73 | + steps: [{ uses: owner/action@v1 }] |
| 74 | + YAML |
| 75 | + |
| 76 | + immutable_references = workflow_uses(load_yaml_file(immutable_workflow)) |
| 77 | + mutable_references = workflow_uses(load_yaml_file(mutable_workflow)) |
| 78 | + |
| 79 | + assert_equal [["jobs.validate.steps.0.uses", "owner/action@#{sha}"]], immutable_references |
| 80 | + assert acceptable_action_reference?(immutable_references.first.last) |
| 81 | + assert_equal [["jobs.validate.steps.0.uses", "owner/action@v1"]], mutable_references |
| 82 | + refute acceptable_action_reference?(mutable_references.first.last) |
| 83 | + end |
| 84 | + end |
| 85 | + |
57 | 86 | def test_action_scanner_keeps_nested_temp_named_directories |
58 | 87 | Dir.mktmpdir("repository-security-policy") do |root| |
59 | 88 | nested_action = File.join(root, "skills/example/tmp/action.yml") |
@@ -110,6 +139,10 @@ def test_dependabot_proposes_pinned_action_updates_for_review |
110 | 139 |
|
111 | 140 | private |
112 | 141 |
|
| 142 | + def load_yaml_file(path) |
| 143 | + YAML.safe_load_file(path, permitted_classes: YAML_TIMESTAMP_CLASSES, aliases: true) |
| 144 | + end |
| 145 | + |
113 | 146 | def action_paths(root = ROOT) |
114 | 147 | Dir.glob(File.join(root, "**/action.{yml,yaml}"), File::FNM_DOTMATCH).reject do |path| |
115 | 148 | first_part = path.delete_prefix("#{root}/").split("/", 2).first |
|
0 commit comments