From 1b767a34a2daf171e86190ea73547d974749468b Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 10 Sep 2026 02:48:31 +0100 Subject: [PATCH 1/6] fix(rules): anchor egress findings to source and respect reusable runners --- lib/rules/research_extensions.ex | 30 ++++++++++++++++++--- test/research_extensions_test.exs | 34 ++++++++++++++++++++++++ test/research_extensions_wiring_test.exs | 34 ++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 6 deletions(-) diff --git a/lib/rules/research_extensions.ex b/lib/rules/research_extensions.ex index 8e3ef65b..aeb09aff 100644 --- a/lib/rules/research_extensions.ex +++ b/lib/rules/research_extensions.ex @@ -171,19 +171,41 @@ defmodule Hypatia.Rules.ResearchExtensions do content = File.read!(path) rel = Path.relative_to(path, repo_path) - touches_secrets? = Regex.match?(~r/\$\{\{\s*secrets\.[A-Za-z_][A-Za-z0-9_]*/, content) + # Comments are not runner configuration. Preserve physical line numbers + # so existing findings are not reported against a newly added line-1 header. + active_lines = + content + |> String.split("\n") + |> Enum.with_index(1) + |> Enum.reject(fn {line, _} -> String.starts_with?(String.trim_leading(line), "#") end) + + active_content = Enum.map_join(active_lines, "\n", &elem(&1, 0)) + + secret_line = + Enum.find(active_lines, fn {line, _} -> + Regex.match?(~r/\$\{\{\s*secrets\.[A-Za-z_][A-Za-z0-9_]*/, line) + end) - installs_harden? = Regex.match?(~r/uses:\s*step-security\/harden-runner/, content) + # A reusable-only caller has no runner or steps in which to install a + # hardener. The called workflow owns the runtime policy and is scanned + # in its source repository; its absence cannot be inferred at this seam. + local_runner? = Regex.match?(~r/^\s+runs-on:/m, active_content) + + installs_harden? = + Regex.match?(~r/^\s+(?:-\s+)?uses:\s*step-security\/harden-runner@/m, active_content) + + if not is_nil(secret_line) and local_runner? and not installs_harden? do + {_source, line} = secret_line - if touches_secrets? and not installs_harden? do [ %{ rule: "RE001", file: rel, severity: :warn, + line: line, reason: "workflow #{rel} references `secrets.*` but does not install " <> - "`step-security/harden-runner` — no outbound-egress telemetry", + "`step-security/harden-runner` — review outbound-egress monitoring", action: :report, detail: %{ fix: diff --git a/test/research_extensions_test.exs b/test/research_extensions_test.exs index 4e57e37b..6ef23325 100644 --- a/test/research_extensions_test.exs +++ b/test/research_extensions_test.exs @@ -43,6 +43,40 @@ defmodule Hypatia.Rules.ResearchExtensionsTest do assert length(findings) == 1 assert hd(findings).rule == "RE001" assert hd(findings).severity == :warn + assert hd(findings).line == 7 + File.rm_rf!(repo) + end + + test "reusable-only callers delegate runner hardening to the workflow source" do + repo = + create_repo_with_workflow(""" + jobs: + mirror: + uses: owner/standards/.github/workflows/mirror.yml@main + secrets: + MIRROR_KEY: ${{ secrets.MIRROR_KEY }} + """) + + assert ResearchExtensions.re001_missing_harden_runner(repo) == [] + File.rm_rf!(repo) + end + + test "commented hardening does not hide a real secret reference or move its location" do + repo = + create_repo_with_workflow(""" + # A managed header added by the action-lock tool + # uses: step-security/harden-runner@main + # Example: ${{ secrets.EXAMPLE }} + jobs: + deploy: + runs-on: ubuntu-latest + steps: + - run: deploy --token=${{ secrets.DEPLOY_KEY }} + """) + + [finding] = ResearchExtensions.re001_missing_harden_runner(repo) + assert finding.line == 8 + assert finding.severity == :warn File.rm_rf!(repo) end diff --git a/test/research_extensions_wiring_test.exs b/test/research_extensions_wiring_test.exs index 288399b2..b232befd 100644 --- a/test/research_extensions_wiring_test.exs +++ b/test/research_extensions_wiring_test.exs @@ -25,7 +25,7 @@ defmodule Hypatia.Rules.ResearchExtensionsWiringTest do @tmp_dir System.tmp_dir!() - # Trips RE001 (touches `secrets.*` with no harden-runner; :warn, no line) + # Trips RE001 (touches `secrets.*` with no harden-runner; :warn, source line) # and RE004 (`docker://` pinned by tag; :warn, line nested under :detail). @tripwire """ name: Deploy @@ -81,6 +81,28 @@ defmodule Hypatia.Rules.ResearchExtensionsWiringTest do end describe ":line carry-through" do + test "RE001 points to the secret reference through CLI and SARIF" do + repo = tripwire_repo() + + finding = + Enum.find(CLI.collect_findings(repo, [:research_extensions]), &(&1.type == "RE001")) + + assert finding.line == 8 + sarif = SARIF.from_findings([finding], repo) + + assert get_in(sarif, [ + "runs", + Access.at(0), + "results", + Access.at(0), + "locations", + Access.at(0), + "physicalLocation", + "region", + "startLine" + ]) == 8 + end + test "RE004's line, nested under :detail, survives normalization" do f = re004(tripwire_repo()) @@ -121,7 +143,15 @@ defmodule Hypatia.Rules.ResearchExtensionsWiringTest do ExUnit.CaptureIO.capture_io(:stderr, fn -> output = ExUnit.CaptureIO.capture_io(fn -> - CLI.main(["scan", repo, "--rules", "research_extensions", "--format", "github", "--exit-zero"]) + CLI.main([ + "scan", + repo, + "--rules", + "research_extensions", + "--format", + "github", + "--exit-zero" + ]) end) assert output =~ "::warning" From 85a757908703e3519b9238ead2b21dc2f75a0b9c Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 10 Sep 2026 02:54:29 +0100 Subject: [PATCH 2/6] fix(ci): adopt shared scanner contracts and restore missing native pin --- .github/workflows/actions.lock | 5 +++++ .github/workflows/dependabot-automerge.yml | 2 +- .github/workflows/governance.yml | 2 +- .github/workflows/hypatia-scan.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 04da3687..2466b667 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -304,6 +304,11 @@ dependencies: commit: 'sha1-2155aa26a21758f2ba119f61bc7e0e1981c106fb' owner_id: 6759885 repo_id: 1275650185 + 'hyperpolymath/smtp-notify-action@v0.2.0': + ref: 'v0.2.0' + commit: 'sha1-ede1191ef6ff3ac02c4f4d9efdf837ee517e11d7' + owner_id: 6759885 + repo_id: 1352485172 'ruby/setup-ruby@v1.321.0': ref: 'v1.321.0' commit: 'sha1-95ef2b042f9d7a56d8268cba8559e2842e2ad01b' diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index 4742b314..86842d10 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -54,7 +54,7 @@ concurrency: jobs: automerge: # Only run for PRs actually authored by Dependabot. - if: github.actor == 'dependabot[bot]' && github.event.pull_request.user.login == 'dependabot[bot]' + if: github.actor_id == '49699333' && github.event.pull_request.user.login == 'dependabot[bot]' runs-on: ubuntu-latest timeout-minutes: 30 permissions: diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index c656a376..a9a9af80 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -16,4 +16,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540 diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index ff5ee89b..3533b601 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -21,5 +21,5 @@ permissions: jobs: hypatia: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540 secrets: inherit diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index f9e7cb0f..0481ccd0 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -16,7 +16,7 @@ permissions: jobs: scorecard: - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540 permissions: actions: read contents: read From d52958bb1f7655a8f49dba8675693b7475085f61 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 10 Sep 2026 10:02:56 +0100 Subject: [PATCH 3/6] fix(rules): evaluate egress findings within each job --- lib/rules/research_extensions.ex | 107 ++++++++++++++++++++---------- test/research_extensions_test.exs | 35 ++++++++++ 2 files changed, 106 insertions(+), 36 deletions(-) diff --git a/lib/rules/research_extensions.ex b/lib/rules/research_extensions.ex index aeb09aff..1889213b 100644 --- a/lib/rules/research_extensions.ex +++ b/lib/rules/research_extensions.ex @@ -179,51 +179,86 @@ defmodule Hypatia.Rules.ResearchExtensions do |> Enum.with_index(1) |> Enum.reject(fn {line, _} -> String.starts_with?(String.trim_leading(line), "#") end) - active_content = Enum.map_join(active_lines, "\n", &elem(&1, 0)) + active_lines + |> workflow_job_lines() + |> Enum.flat_map(fn job_lines -> + active_content = Enum.map_join(job_lines, "\n", &elem(&1, 0)) - secret_line = - Enum.find(active_lines, fn {line, _} -> - Regex.match?(~r/\$\{\{\s*secrets\.[A-Za-z_][A-Za-z0-9_]*/, line) - end) + secret_line = + Enum.find(job_lines, fn {line, _} -> + Regex.match?(~r/\$\{\{\s*secrets\.[A-Za-z_][A-Za-z0-9_]*/, line) + end) - # A reusable-only caller has no runner or steps in which to install a - # hardener. The called workflow owns the runtime policy and is scanned - # in its source repository; its absence cannot be inferred at this seam. - local_runner? = Regex.match?(~r/^\s+runs-on:/m, active_content) + # Reusable jobs delegate their runtime to the source workflow. A sibling's + # runner or hardener cannot establish this job's execution policy. + local_runner? = Regex.match?(~r/^\s+runs-on:/m, active_content) - installs_harden? = - Regex.match?(~r/^\s+(?:-\s+)?uses:\s*step-security\/harden-runner@/m, active_content) + installs_harden? = + Regex.match?(~r/^\s+(?:-\s+)?uses:\s*step-security\/harden-runner@/m, active_content) - if not is_nil(secret_line) and local_runner? and not installs_harden? do - {_source, line} = secret_line + if not is_nil(secret_line) and local_runner? and not installs_harden? do + {_source, line} = secret_line - [ - %{ - rule: "RE001", - file: rel, - severity: :warn, - line: line, - reason: - "workflow #{rel} references `secrets.*` but does not install " <> - "`step-security/harden-runner` — review outbound-egress monitoring", - action: :report, - detail: %{ - fix: - "Add as the first step of each job:\n" <> - " - uses: step-security/harden-runner@\n" <> - " with:\n" <> - " egress-policy: block\n" <> - " allowed-endpoints: >\n" <> - " github.com:443" + [ + %{ + rule: "RE001", + file: rel, + severity: :warn, + line: line, + reason: + "job in #{rel} references `secrets.*` but does not install " <> + "`step-security/harden-runner` — review outbound-egress monitoring", + action: :report, + detail: %{ + fix: + "Add harden-runner as the first step of this job, with egress-policy: block " <> + "and an allowlist derived from the job's actual required endpoints." + } } - } - ] - else - [] - end + ] + else + [] + end + end) end) end + # Follow block-style jobs by indentation, retaining physical source lines. + # As with the other research rules, this is a local static text analysis. + defp workflow_job_lines(lines) do + {_in_jobs, _indent, groups} = + Enum.reduce(lines, {false, nil, []}, fn {line, _} = entry, {in_jobs, indent, groups} -> + cond do + Regex.match?(~r/^jobs:\s*(?:#.*)?$/, line) -> + {true, nil, groups} + + not in_jobs -> + {false, indent, groups} + + Regex.match?(~r/^\S/, line) -> + {false, nil, groups} + + true -> + header = Regex.run(~r/^(\s+)(?:[A-Za-z0-9_-]+|"[^"]+"|'[^']+'):/, line) + width = if header, do: String.length(Enum.at(header, 1)), else: nil + + cond do + width && (is_nil(indent) || width == indent) -> + {true, width, [[entry] | groups]} + + groups != [] -> + [current | rest] = groups + {true, indent, [[entry | current] | rest]} + + true -> + {true, indent, groups} + end + end + end) + + groups |> Enum.reverse() |> Enum.map(&Enum.reverse/1) + end + # ─── RE002: Harden-Runner in audit-only mode ───────────────────────── @doc """ diff --git a/test/research_extensions_test.exs b/test/research_extensions_test.exs index 6ef23325..5e03f29c 100644 --- a/test/research_extensions_test.exs +++ b/test/research_extensions_test.exs @@ -97,6 +97,41 @@ defmodule Hypatia.Rules.ResearchExtensionsTest do File.rm_rf!(repo) end + test "mixed reusable and local jobs do not share runner or hardening state" do + repo = + create_repo_with_workflow(""" + jobs: + shared: + uses: owner/standards/.github/workflows/mirror.yml@main + secrets: + MIRROR_KEY: ${{ secrets.MIRROR_KEY }} + local: + runs-on: ubuntu-latest + steps: + - run: echo no credentials + """) + + assert ResearchExtensions.re001_missing_harden_runner(repo) == [] + + File.write!(Path.join([repo, ".github/workflows", "test.yml"]), """ + jobs: + hardened: + runs-on: ubuntu-latest + steps: + - uses: step-security/harden-runner@main + - run: deploy --token=${{ secrets.ONE }} + exposed: + runs-on: ubuntu-latest + steps: + - run: deploy --token=${{ secrets.TWO }} + """) + + [finding] = ResearchExtensions.re001_missing_harden_runner(repo) + assert finding.line == 10 + assert finding.severity == :warn + File.rm_rf!(repo) + end + test "passes when no secrets are referenced" do repo = create_repo_with_workflow(""" From e047d4606abe8f4666b68ff4a05c91019fb9652c Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:36:16 +0000 Subject: [PATCH 4/6] fix(rules): ignore nested multiline runner configuration --- lib/rules/research_extensions.ex | 105 ++++++++++++++++++++++++++++-- test/research_extensions_test.exs | 25 +++++++ 2 files changed, 124 insertions(+), 6 deletions(-) diff --git a/lib/rules/research_extensions.ex b/lib/rules/research_extensions.ex index 1889213b..bd7aa892 100644 --- a/lib/rules/research_extensions.ex +++ b/lib/rules/research_extensions.ex @@ -182,8 +182,6 @@ defmodule Hypatia.Rules.ResearchExtensions do active_lines |> workflow_job_lines() |> Enum.flat_map(fn job_lines -> - active_content = Enum.map_join(job_lines, "\n", &elem(&1, 0)) - secret_line = Enum.find(job_lines, fn {line, _} -> Regex.match?(~r/\$\{\{\s*secrets\.[A-Za-z_][A-Za-z0-9_]*/, line) @@ -191,10 +189,8 @@ defmodule Hypatia.Rules.ResearchExtensions do # Reusable jobs delegate their runtime to the source workflow. A sibling's # runner or hardener cannot establish this job's execution policy. - local_runner? = Regex.match?(~r/^\s+runs-on:/m, active_content) - - installs_harden? = - Regex.match?(~r/^\s+(?:-\s+)?uses:\s*step-security\/harden-runner@/m, active_content) + local_runner? = direct_job_field?(job_lines, "runs-on") + installs_harden? = harden_runner_step?(job_lines) if not is_nil(secret_line) and local_runner? and not installs_harden? do {_source, line} = secret_line @@ -223,6 +219,103 @@ defmodule Hypatia.Rules.ResearchExtensions do end) end + defp direct_job_field?(job_lines, field) do + case job_field_indent(job_lines) do + nil -> + false + + field_indent -> + Enum.any?(job_lines, fn {line, _} -> + indent_of(line) == field_indent and + String.starts_with?(String.trim_leading(line), "#{field}:") + end) + end + end + + defp harden_runner_step?(job_lines) do + with field_indent when is_integer(field_indent) <- job_field_indent(job_lines), + step_lines when step_lines != [] <- lines_in_steps(job_lines, field_indent), + step_indent when is_integer(step_indent) <- step_indent(step_lines, field_indent) do + case Enum.reduce_while(step_lines, {false, nil, nil}, fn {line, _}, + {in_step, property_indent, + scalar_indent} -> + indent = indent_of(line) + + cond do + scalar_indent && indent > scalar_indent -> + {:cont, {in_step, property_indent, scalar_indent}} + + indent == step_indent and Regex.match?(~r/^\s*-\s+/, line) -> + if harden_runner_uses?(line) do + {:halt, true} + else + {:cont, {true, nil, block_scalar?(line) && indent}} + end + + in_step and indent > step_indent -> + direct_property? = is_nil(property_indent) or indent == property_indent + property_indent = property_indent || indent + + if direct_property? and harden_runner_uses?(line) do + {:halt, true} + else + {:cont, {in_step, property_indent, block_scalar?(line) && indent}} + end + + true -> + {:cont, {in_step, property_indent, nil}} + end + end) do + true -> true + _ -> false + end + else + _ -> false + end + end + + defp job_field_indent([_header | job_lines]) do + job_lines + |> Enum.map(&elem(&1, 0)) + |> Enum.reject(&(String.trim(&1) == "")) + |> Enum.map(&indent_of/1) + |> Enum.min(fn -> nil end) + end + + defp job_field_indent([]), do: nil + + defp lines_in_steps(job_lines, field_indent) do + case Enum.find_index(job_lines, fn {line, _} -> + indent_of(line) == field_indent and + String.starts_with?(String.trim_leading(line), "steps:") + end) do + nil -> + [] + + index -> + job_lines + |> Enum.drop(index + 1) + |> Enum.take_while(fn {line, _} -> + String.trim(line) == "" or indent_of(line) > field_indent + end) + end + end + + defp step_indent(step_lines, field_indent) do + step_lines + |> Enum.filter(fn {line, _} -> + indent_of(line) > field_indent and Regex.match?(~r/^\s*-\s+/, line) + end) + |> Enum.map(fn {line, _} -> indent_of(line) end) + |> Enum.min(fn -> nil end) + end + + defp harden_runner_uses?(line) do + Regex.match?(~r/^\s*(?:-\s+)?uses:\s*step-security\/harden-runner@/, line) + end + + defp block_scalar?(line), do: Regex.match?(~r/:\s*[>|][0-9+-]*\s*(?:#.*)?$/, line) + # Follow block-style jobs by indentation, retaining physical source lines. # As with the other research rules, this is a local static text analysis. defp workflow_job_lines(lines) do diff --git a/test/research_extensions_test.exs b/test/research_extensions_test.exs index 5e03f29c..ae880d8c 100644 --- a/test/research_extensions_test.exs +++ b/test/research_extensions_test.exs @@ -97,6 +97,31 @@ defmodule Hypatia.Rules.ResearchExtensionsTest do File.rm_rf!(repo) end + test "does not treat nested multiline values as runner configuration or hardening" do + repo = + create_repo_with_workflow(""" + jobs: + nested-values: + env: + WORKFLOW_EXAMPLE: | + runs-on: ubuntu-latest + - uses: step-security/harden-runner@main + steps: + - run: deploy --token=${{ secrets.NESTED_ONLY }} + exposed: + runs-on: ubuntu-latest + steps: + - run: | + runs-on: ubuntu-latest + - uses: step-security/harden-runner@main + deploy --token=${{ secrets.EXPOSED }} + """) + + [finding] = ResearchExtensions.re001_missing_harden_runner(repo) + assert finding.line == 15 + File.rm_rf!(repo) + end + test "mixed reusable and local jobs do not share runner or hardening state" do repo = create_repo_with_workflow(""" From 0e9046a73d32e5048e25e6245f671e058e493e62 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:13:11 +0000 Subject: [PATCH 5/6] docs(rules): clarify RE001 job-level detection --- lib/rules/research_extensions.ex | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/rules/research_extensions.ex b/lib/rules/research_extensions.ex index bd7aa892..63f01ef7 100644 --- a/lib/rules/research_extensions.ex +++ b/lib/rules/research_extensions.ex @@ -158,9 +158,13 @@ defmodule Hypatia.Rules.ResearchExtensions do # ─── RE001: Harden-Runner absent on secrets-touching workflow ───────── @doc """ - RE001: Workflow references `${{ secrets.* }}` but does not install - `step-security/harden-runner`. Provenance: StepSecurity Harden-Runner - deployment guide. + RE001: Reports each locally executed workflow job that references + `${{ secrets.* }}` without a `step-security/harden-runner@...` step in the + same job. Full-line comments and reusable-workflow jobs without a direct + `runs-on` field are ignored. + + Each finding points to the first matching secret reference in the job. + Provenance: StepSecurity Harden-Runner deployment guide. Severity: `:warn`. Action: `:report`. """ From ab1f855c6d459374400fa7065df4e2ce7039f1df Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:56:24 +0000 Subject: [PATCH 6/6] fix(rules): recognize quoted harden-runner uses --- lib/rules/research_extensions.ex | 2 +- test/research_extensions_test.exs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/rules/research_extensions.ex b/lib/rules/research_extensions.ex index 63f01ef7..4907b0a5 100644 --- a/lib/rules/research_extensions.ex +++ b/lib/rules/research_extensions.ex @@ -315,7 +315,7 @@ defmodule Hypatia.Rules.ResearchExtensions do end defp harden_runner_uses?(line) do - Regex.match?(~r/^\s*(?:-\s+)?uses:\s*step-security\/harden-runner@/, line) + Regex.match?(~r/^\s*(?:-\s+)?uses:\s*["']?step-security\/harden-runner@/, line) end defp block_scalar?(line), do: Regex.match?(~r/:\s*[>|][0-9+-]*\s*(?:#.*)?$/, line) diff --git a/test/research_extensions_test.exs b/test/research_extensions_test.exs index ae880d8c..bcaf94ab 100644 --- a/test/research_extensions_test.exs +++ b/test/research_extensions_test.exs @@ -97,6 +97,26 @@ defmodule Hypatia.Rules.ResearchExtensionsTest do File.rm_rf!(repo) end + test "passes when harden-runner uses values are quoted" do + repo = + create_repo_with_workflow(""" + jobs: + double-quoted: + runs-on: ubuntu-latest + steps: + - uses: "step-security/harden-runner@main" + - run: deploy --token=${{ secrets.DOUBLE_QUOTED_KEY }} + single-quoted: + runs-on: ubuntu-latest + steps: + - uses: 'step-security/harden-runner@main' + - run: deploy --token=${{ secrets.SINGLE_QUOTED_KEY }} + """) + + assert ResearchExtensions.re001_missing_harden_runner(repo) == [] + File.rm_rf!(repo) + end + test "does not treat nested multiline values as runner configuration or hardening" do repo = create_repo_with_workflow("""