Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/dogfood-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:

- name: Validate A2ML manifests
if: steps.detect.outputs.count > 0
uses: hyperpolymath/a2ml-validate-action@0f8081cdfa293663ec6b204274b4272da302e564 # main (exit-127 comment fix)
uses: hyperpolymath/a2ml-ecosystem/validate-action@aa4b836bd969df2bc58128cb8e3d20bbc88d5e79 # main (exit-127 comment fix)
with:
path: '.'
strict: 'false'
Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:

- name: Validate K9 contracts
if: steps.detect.outputs.k9_count > 0
uses: hyperpolymath/k9-validate-action@91d662ecd1ac1e00875a7d77dff41933e3afe884 # main
uses: hyperpolymath/k9-ecosystem/validate-action@89f3c2702f4f650a92aa7411502f38da06abd562 # main
with:
path: '.'
strict: 'false'
Expand Down
76 changes: 73 additions & 3 deletions lib/rules/structural_drift.ex
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,11 @@ defmodule Hypatia.Rules.StructuralDrift do
"test/fixtures/",
"tests/fixtures/",
"scripts/fix-scripts/",
"third_party/"
"third_party/",
# Archived session notes record the tree AS IT WAS. A path that was
# valid when written is not rename drift; "fixing" it would falsify
# the record.
"docs/archive/"
]

find_files_by_ext(repo_path, [
Expand All @@ -989,13 +993,16 @@ defmodule Hypatia.Rules.StructuralDrift do
".twasm"
])
|> Enum.reject(fn rel ->
rel == "CHANGELOG.md" or Enum.any?(corpus_prefixes, &String.starts_with?(rel, &1))
rel == "CHANGELOG.md" or Enum.any?(corpus_prefixes, &String.starts_with?(rel, &1)) or
nested_project_doc?(repo_path, rel)
end)
|> Enum.flat_map(fn rel ->
path = Path.join(repo_path, rel)

case File.read(path) do
{:ok, content} ->
{:ok, raw} ->
content = strip_full_line_comments(raw, Path.extname(rel))

# Negative lookbehind: only a *repo-root-relative* `src/<dir>/`
# is rename-drift. A `src/` preceded by another path segment
# (`vcl-ut/src/bridges/`, `cli/src/commands/`, `echidna/src/rust/`,
Expand Down Expand Up @@ -1035,6 +1042,69 @@ defmodule Hypatia.Rules.StructuralDrift do
end
end

# Drop whole-line comments before matching. A commented-out example is not a
# live claim about the tree.
#
# Measured 2026-07-29 on gitbot-fleet's .machine_readable/INTENT.contractile,
# where SD022 fired on
#
# ; "src/abi/ - formal proofs, changes require re-verification"
#
# inside a `; *REMINDER: List areas where LLMs should check...*` block --
# template boilerplate, entirely commented out. Same family as the known
# unwrap-rule-matches-comments defect.
#
# Deliberately conservative: only `;` (scheme-shaped contractile/twasm) and
# `#` in TOML-shaped files. Markdown/AsciiDoc are left alone, since `#` there
# is a heading and may legitimately carry a path.
defp strip_full_line_comments(content, ext) do
leaders =
case ext do
e when e in [".contractile", ".twasm"] -> [";"]
e when e in [".toml", ".a2ml"] -> ["#", ";"]
_ -> []
end

if leaders == [] do
content
else
content
|> String.split("\n")
|> Enum.reject(fn line ->
t = String.trim_leading(line)
Enum.any?(leaders, &String.starts_with?(t, &1))
end)
|> Enum.join("\n")
end
end

# A doc inside a NESTED PROJECT ROOT (a subdirectory carrying its own build
# manifest) describes THAT project, whose layout is governed by its own
# repository -- not by this one. Reporting its paths as this repo's rename
# drift is noise the mirror cannot act on.
#
# Measured 2026-07-29 on gitbot-fleet: `bots/echidnabot/` is a vendored copy
# of the standalone echidnabot repo. Its CANONICAL_SOURCE.md documents
# `src/abi/` and states in the same table that the ABI namespace is
# "owner-managed in standalone; fleet does not need it for deploy". `src/abi/`
# exists upstream and is deliberately absent here, so SD022 fired on a doc
# that is correct. 10 of 11 findings in that repo were this shape.
#
# Only subtrees BELOW the repo root count -- a root manifest means the repo
# itself is the project, and its drift is in scope.
defp nested_project_doc?(repo_path, rel) do
manifests = ["Cargo.toml", "mix.exs", "package.json", "go.mod", "pyproject.toml"]

rel
|> Path.dirname()
|> Path.split()
|> Enum.scan(fn seg, acc -> Path.join(acc, seg) end)
|> Enum.reject(&(&1 in [".", ""]))
|> Enum.any?(fn dir ->
Enum.any?(manifests, &File.exists?(Path.join([repo_path, dir, &1])))
end)
end

# Index every `**/src/<name>` directory in the tracked tree (via the file
# list, so it respects .gitignore and skips _build/deps), returning the set
# of <name> basenames plus the set of real top-level directory names.
Expand Down
37 changes: 35 additions & 2 deletions lib/rules/workflow_audit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -522,8 +522,18 @@ defmodule Hypatia.Rules.WorkflowAudit do
# Actor-controllable expressions. Anchored loosely so we catch the
# expression wherever it appears in a run block — the previous full
# `run: |` block-shape match missed inline `run:` single-liners.
# `[\s\S]*?` previously spanned step boundaries: a `run:` in one step and a
# context expression in a LATER step's `env:` block matched as though the
# value were interpolated into the script. Measured 2026-07-29 on
# gitbot-fleet's repo-integrity-guard.yml -- flagged :critical although the
# untrusted values are bound via `env:` and consumed as quoted shell vars
# through `grep -F`, i.e. exactly the mitigation this rule asks for.
#
# `[^\n]` per line, and no line may start a new step (`- name:`/`- uses:`)
# or open a sibling mapping key at step indent, so the match stays inside
# one run block.
run_context_re =
~r/run:[\s\S]*?\$\{\{\s*github\.(?:head_ref|event\.pull_request\.(?:title|body|head\.ref)|event\.issue\.(?:title|body)|event\.comment\.body|event\.review\.body|event\.workflow_run\.(?:head_branch|display_title)|event\.commits)[^}]*\}\}/m
~r/run:(?:[^\n]*\n(?!\s*-\s+(?:name|uses|id):|\s{0,10}(?:env|with|if|name|uses):\s))*?[^\n]*\$\{\{\s*github\.(?:head_ref|event\.pull_request\.(?:title|body|head\.ref)|event\.issue\.(?:title|body)|event\.comment\.body|event\.review\.body|event\.workflow_run\.(?:head_branch|display_title)|event\.commits)[^}]*\}\}/m

unsafe_json_payload_re =
~r/-d\s*".*\$\{\{\s*github\.(?:head_ref|event\.pull_request\.(?:title|body|head\.ref)|event\.issue\.(?:title|body)|event\.comment\.body|event\.review\.body|event\.workflow_run\.(?:head_branch|display_title)|event\.commits)/s
Expand Down Expand Up @@ -989,7 +999,30 @@ defmodule Hypatia.Rules.WorkflowAudit do
Regex.match?(~r/if:[^\n]*steps\.[A-Za-z0-9_-]+\.outputs\./, step_block) and
String.contains?(stripped, "GITHUB_OUTPUT")

if Regex.match?(gate_re, step_block) or env_output_gate? do
# Third valid idiom, previously unrecognised: a job-level
# `env:` binds a presence boolean from the secret and steps gate
# on it. Measured 2026-07-29 on gitbot-fleet's instant-sync.yml,
# which does exactly this on every step plus an else branch, and
# was still reported :high.
#
# env:
# HAS_TOKEN: ${{ secrets.FARM_DISPATCH_TOKEN != '' }}
# steps:
# - if: env.HAS_TOKEN == 'true'
env_boolean_gate? =
case Regex.run(
~r/(\w+):\s*\$\{\{\s*secrets\.#{Regex.escape(secret_name)}\s*!=\s*['"]['"]?\s*\}\}/,
stripped
) do
[_, env_name] ->
Regex.match?(~r/if:[^\n]*env\.#{Regex.escape(env_name)}\b/, stripped)

_ ->
false
end

if Regex.match?(gate_re, step_block) or env_output_gate? or
env_boolean_gate? do
[]
else
[
Expand Down
Loading