Skip to content
Merged
11 changes: 4 additions & 7 deletions .github/workflows/dogfood-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v7.0.1

- name: Test manifest detection
run: ruby test/dogfood_manifest_detection_test.rb

- name: Check for A2ML and DEED files
id: detect
run: |
Expand Down Expand Up @@ -82,7 +79,7 @@ jobs:
else
echo "## A2ML Validation" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Found **${A2ML_COUNT}** candidate .a2ml/.deed file(s). The validator applies the configured exclusions; see step output for results." >> "$GITHUB_STEP_SUMMARY"
echo "Scanned **${A2ML_COUNT}** .a2ml/.deed file(s). See step output for details." >> "$GITHUB_STEP_SUMMARY"
fi

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -291,8 +288,8 @@ jobs:
SCORE=0
MAX=5

# A2ML manifest present?
if find . -name '*.a2ml' -not -path './.git/*' | head -1 | grep -q .; then
# A2ML or DEED manifest present?
if find . -type f \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then
SCORE=$((SCORE + 1))
A2ML_STATUS=":white_check_mark:"
else
Expand Down Expand Up @@ -338,7 +335,7 @@ jobs:

| Tool/Format | Status | Notes |
|-------------|--------|-------|
| A2ML manifest (0-AI-MANIFEST.a2ml) | ${A2ML_STATUS} | Required for all RSR repos |
| A2ML/DEED manifest | ${A2ML_STATUS} | Required for all RSR repos |
| K9 contracts | ${K9_STATUS} | Required for repos with config files |
| .editorconfig | ${EC_STATUS} | Required for all repos |
| Groove endpoint | ${GROOVE_STATUS} | Required for service repos |
Expand Down
62 changes: 52 additions & 10 deletions lib/hypatia/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ defmodule Hypatia.CLI do
code_safety,migration_rules,scorecard,
green_web,git_state,dependabot_alerts,
secret_scanning_alerts,code_scanning_alerts,
structural_drift,implementation_inside_canon
structural_drift,implementation_inside_canon,content_patterns
--format <fmt> Output format: json (default), text, github, sarif
--severity <lvl> Minimum severity to report: critical, high, medium (default), low, info
--path <dir> Path to scan (alternative to positional argument)
Expand Down Expand Up @@ -56,7 +56,8 @@ defmodule Hypatia.CLI do
:secret_scanning_alerts,
:code_scanning_alerts,
:structural_drift,
:implementation_inside_canon
:implementation_inside_canon,
:content_patterns
Comment thread
coderabbitai[bot] marked this conversation as resolved.
]

@severity_order %{
Expand Down Expand Up @@ -306,13 +307,17 @@ defmodule Hypatia.CLI do
# ─── Finding collection across rule modules ──────────────────────────

@doc """
Run the named rule modules against `repo_path` and return normalized findings
(`%{rule_module, type, severity, file, reason, action}`). Public so the RSR
conformance oracle can delegate content-scan criteria to the live scanners
rather than reimplement per-file detection. `rules` is a list of module atoms
(e.g. `[:cicd_rules, :structural_drift]`); GitHub-API modules
(`:dependabot_alerts`, `:secret_scanning_alerts`, `:code_scanning_alerts`,
`:scorecard`) require network + token and return nothing offline.
Run the named rule modules against `repo_path` and return unsuppressed findings
normalised as `%{rule_module, type, severity, file, reason, action}` maps.
Content-pattern findings also include their one-based source `line`. Public so
the RSR conformance oracle can delegate content-scan criteria to the live
scanners rather than reimplement per-file detection.

`rules` is a list of module atoms (for example, `[:content_patterns,
:structural_drift]`). GitHub alert modules (`:dependabot_alerts`,
`:secret_scanning_alerts`, and `:code_scanning_alerts`) require network access
and credentials; when unavailable, they write a warning to standard error and
contribute no findings.
"""
def collect_findings(repo_path, rules) do
results = []
Expand Down Expand Up @@ -856,6 +861,43 @@ defmodule Hypatia.CLI do
results
end

# ─── Content-pattern rules ───────────────────────────────────────────
#
# `CicdRules.scan_content_patterns/1` is a glob+regex, per-line content
# engine over the `@blocked_patterns` table. It shipped complete but
# unwired: until now nothing in `lib/` called it, so every table entry
# carrying `:pattern` + `:applies_to` was dormant and only its unit test
# ever exercised it. Wiring it here makes rule authoring a matter of
# adding a table row rather than writing a module.
#
# This is the only branch that emits a real `:line`. Everything else
# normalizes without one, which is why SARIF's `startLine` was uniformly
# 1 before this landed. Suppression is NOT applied here -- the uniform
# pass below funnels every finding through ScannerSuppression exactly
# once, and doing it twice would be both redundant and a second place
# for exemptions to silently diverge.
results =
if :content_patterns in rules do
normalized =
repo_path
|> Hypatia.Rules.CicdRules.scan_content_patterns()
|> Enum.map(fn f ->
%{
rule_module: "content_patterns",
severity: to_string(Map.get(f, :severity, "medium")),
type: to_string(f.rule),
file: f.file,
line: f.line,
reason: f.reason,
action: "flag"
}
end)

results ++ normalized
else
results
end

# ─── Uniform suppression pass ──────────────────────────────────────
#
# Several rule paths above (structural_drift, code_scanning_alerts,
Expand Down Expand Up @@ -1340,7 +1382,7 @@ defmodule Hypatia.CLI do
code_safety,migration_rules,scorecard,green_web,
git_state,dependabot_alerts,
secret_scanning_alerts,code_scanning_alerts,
structural_drift,implementation_inside_canon
structural_drift,implementation_inside_canon,content_patterns
--format, -f <fmt> Output format: json (default), text, github, sarif, sarif
--severity, -s <lvl> Minimum severity: critical, high, medium (default), low
--path, -p <dir> Path to scan (alternative to positional arg)
Expand Down
Loading
Loading