From d2bbf75cac791983e2cc58d89727d216c15f8cb8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 18:57:48 +0000 Subject: [PATCH 1/2] fix(matcher): scorecard findings unreachable due to language gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recipe matcher rejected every scorecard-source finding (~310 ecosystem- wide), routing them to :control "no safe fix available" advisories. Root cause: `lib/recipe_matcher.ex` filtered candidate recipes with `"*" in langs or language in langs`. Two failure modes: 1. 12 recipes declared `languages: ["any"]` — never matched, since `"any"` is not a sentinel the filter recognises and no repo has `"any"` as its primary language. 2. 8 scorecard / workflow-file recipes declared `languages: ["yaml"]` — never matched, since yaml is a workflow-file type, not any repo's primary language. So `recipe-pin-dependencies`, `recipe-fix-workflow-permissions`, etc. were unreachable for SC013/ SC018 findings — the exact rule families dominating the daily remediation sweep. Fix: - `langs_match?/2` private helper accepts `"*"` and `"any"` as synonymous language-agnostic sentinels. - `effective_language_for/2` remaps the lookup language to `"yaml"` for patterns whose `source` is `"scorecard"` or whose `category` names a known workflow-file rule family (DependencyPinning, TokenPermissions, DangerousWorkflow, etc.). The repo's primary language is irrelevant for workflow-file findings. - Applied to `best_recipe/2`, `category_match_recipe/2`, and `fuzzy_match_recipe/2`. Tests pin all three invariants. All 22 scorecard recipe `fix_script` references already exist on disk in `scripts/fix-scripts/` — the bug was purely in matcher reachability, not missing fix implementations. Closes the dispatcher half of the "no security stuff being sorted" symptom. Remaining M7 work (PAT for cross-repo dispatch, push fixes to remotes) still needs operator action, but the manifests will now carry populated fix_script fields for scorecard findings. --- lib/recipe_matcher.ex | 42 ++++++++++++++++++++++++++----- test/recipe_matcher_test.exs | 48 ++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/lib/recipe_matcher.ex b/lib/recipe_matcher.ex index 21c1d618..748fe50e 100644 --- a/lib/recipe_matcher.ex +++ b/lib/recipe_matcher.ex @@ -32,8 +32,7 @@ defmodule Hypatia.RecipeMatcher do def best_recipe(pattern_id, language) do find_recipes(pattern_id) |> Enum.find(fn recipe -> - langs = Map.get(recipe, "languages", []) - "*" in langs or language in langs + langs_match?(Map.get(recipe, "languages", []), language) end) end @@ -140,12 +139,12 @@ defmodule Hypatia.RecipeMatcher do # Match recipe by target_categories field -- most reliable match defp category_match_recipe(pattern, language) do category = Map.get(pattern, "category", "") + effective_language = effective_language_for(pattern, language) all_recipes() |> Enum.filter(fn recipe -> cats = Map.get(recipe, "target_categories", []) - langs = Map.get(recipe, "languages", []) - lang_ok = "*" in langs or language in langs + lang_ok = langs_match?(Map.get(recipe, "languages", []), effective_language) lang_ok and category in cats end) |> Enum.sort_by(fn r -> Map.get(r, "confidence", 0) end, :desc) @@ -155,6 +154,7 @@ defmodule Hypatia.RecipeMatcher do defp fuzzy_match_recipe(pattern, language) do pa_rule = Map.get(pattern, "pa_rule", "") description = Map.get(pattern, "description", "") |> String.downcase() + effective_language = effective_language_for(pattern, language) # Skip if no PA rule to match against if pa_rule == "" do @@ -162,8 +162,7 @@ defmodule Hypatia.RecipeMatcher do else all_recipes() |> Enum.filter(fn recipe -> - langs = Map.get(recipe, "languages", []) - lang_ok = "*" in langs or language in langs + lang_ok = langs_match?(Map.get(recipe, "languages", []), effective_language) recipe_pattern_ids = Map.get(recipe, "pattern_ids", []) @@ -196,6 +195,37 @@ defmodule Hypatia.RecipeMatcher do end end + # Both "*" and "any" are language-agnostic sentinels. Historical recipes + # use one or the other; treating them as synonyms keeps both groups + # routable (without this, ~12 recipes declared "any" matched no patterns). + defp langs_match?(langs, language) do + "*" in langs or "any" in langs or language in langs + end + + # Scorecard / workflow-file findings are about .github/workflows/*.yml, + # not the repo's primary language. Without this remap, recipes declared + # `languages: ["yaml"]` (pin-deps, token-permissions, etc.) never match + # because no repo has yaml as its primary language, and every scorecard + # finding falls through to :control "no safe fix available". + defp effective_language_for(pattern, language) do + cond do + Map.get(pattern, "source") == "scorecard" -> "yaml" + workflow_file_category?(Map.get(pattern, "category", "")) -> "yaml" + true -> language + end + end + + defp workflow_file_category?(category) do + category in [ + "DependencyPinning", + "PinnedDependencies", + "TokenPermissions", + "DangerousWorkflow", + "DependencyUpdateTool", + "BranchProtection" + ] + end + defp load_recipe(path) do with {:ok, content} <- File.read(path), {:ok, data} <- Jason.decode(content) do diff --git a/test/recipe_matcher_test.exs b/test/recipe_matcher_test.exs index c2ead171..5eae2cb5 100644 --- a/test/recipe_matcher_test.exs +++ b/test/recipe_matcher_test.exs @@ -93,4 +93,52 @@ defmodule Hypatia.RecipeMatcherTest do assert RecipeMatcher.substitution_for_category("FakeCategory") == nil end end + + describe "best_recipe_for_pattern/2 — language matching" do + test "'any' sentinel matches any repo language" do + # recipe-scorecard-license declares languages: ["any"] + pattern = %{ + "id" => "SC-010-some-repo", + "category" => "License", + "pa_rule" => "SC010", + "source" => "scorecard" + } + + recipe = RecipeMatcher.best_recipe_for_pattern(pattern, "rust") + assert recipe != nil + assert recipe["id"] in ["recipe-scorecard-license", "recipe-add-license-file"] + assert recipe["fix_script"] not in [nil, ""] + end + + test "scorecard DependencyPinning pattern resolves yaml recipe regardless of repo language" do + # Reproduces the production gap: SC013 findings across 230+ repos + # routed to :control "no safe fix available" because recipe-pin-deps + # declares languages: ["yaml"] and no repo has yaml as primary lang. + pattern = %{ + "id" => "SC-013-007-lang", + "category" => "DependencyPinning", + "pa_rule" => "SC013", + "source" => "scorecard", + "description" => "1 workflow(s) with tag-pinned actions" + } + + recipe = RecipeMatcher.best_recipe_for_pattern(pattern, "elixir") + assert recipe != nil, "scorecard pattern must route to a recipe, not :control" + assert recipe["fix_script"] not in [nil, ""] + assert recipe["triangle_tier"] in ["eliminate", "substitute"] + end + + test "scorecard TokenPermissions pattern resolves yaml recipe" do + pattern = %{ + "id" => "SC-018-some-repo", + "category" => "TokenPermissions", + "pa_rule" => "SC018", + "source" => "scorecard" + } + + recipe = RecipeMatcher.best_recipe_for_pattern(pattern, "go") + assert recipe != nil + assert recipe["fix_script"] == "fix-workflow-permissions.sh" + end + end end From 7cc2667331c2f6acb7ad00465fd946f73961a85f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 19:32:18 +0000 Subject: [PATCH 2/2] chore(baseline): regenerate .hypatia-baseline.json against current tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The baseline had drifted into pure historical risk: 71 accepted findings (31 critical, 40 high) generated before the #278 stale-escript fix and the wave of code_safety/security_errors cleanups landed. A fresh scan against the current tree finds 35 findings, all medium-or-lower: - 32 low (code_safety hot-path expects, ncl_docker_not_podman, workflow_audit missing-workflow, structural_drift, etc.) - 3 medium (git_state transient + structural_drift) - 0 critical, 0 high Most old baseline entries are either: - fixed in code (e.g. the believe_me at src/abi/RuleEngine.idr is now inline-suppressed with a documented `-- hypatia: allow` directive), - migrated/refactored (e.g. lib/direct_github_pr.ex no longer exists), - or were covered by the new total-Python-ban / scanner-soundness wave. Net effect: every gate threshold of "fail on critical|high above baseline" now starts from an empty critical/high ledger — net-new critical or high findings will stand out, which is what the baseline is supposed to enable. Generated with the canonical Elixir escript pipeline against this tree (no rule changes, just a snapshot refresh). Severity threshold "low" so the snapshot reflects the full advisory surface, not just gates. --- .hypatia-baseline.json | 502 ++++++++++------------------------------- 1 file changed, 125 insertions(+), 377 deletions(-) diff --git a/.hypatia-baseline.json b/.hypatia-baseline.json index c3277c30..3ef7458f 100644 --- a/.hypatia-baseline.json +++ b/.hypatia-baseline.json @@ -1,499 +1,247 @@ [ { - "severity": "critical", + "severity": "low", "rule_module": "code_safety", - "type": "believe_me", - "file": "src/abi/RuleEngine.idr", + "type": "expect_in_hot_path", + "file": "fixer/src/scanner.rs", "action": "flag" }, { - "severity": "critical", + "severity": "low", "rule_module": "code_safety", - "type": "elixir_system_cmd_interpolation", - "file": "lib/direct_github_pr.ex", - "action": "flag" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "elixir_system_cmd_interpolation", - "file": "lib/hypatia/cli.ex", - "action": "flag" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "elixir_system_cmd_interpolation", - "file": "lib/mix/tasks/hypatia.audit_repos.ex", - "action": "flag" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "elixir_system_cmd_interpolation", - "file": "lib/mix/tasks/hypatia.deploy_prevention_workflows.ex", + "type": "expect_in_hot_path", + "file": "integration/src/ci_simulation/assertions.rs", "action": "flag" }, { - "severity": "critical", + "severity": "low", "rule_module": "code_safety", - "type": "elixir_system_cmd_interpolation", - "file": "lib/rules/structural_drift.ex", + "type": "expect_in_hot_path", + "file": "integration/src/ci_simulation/scenarios.rs", "action": "flag" }, { - "severity": "critical", + "severity": "low", "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "adapters/src/sourcehut.rs", + "type": "ncl_docker_not_podman", + "file": ".machine_readable/svc/k9/hypatia-metadata.k9.ncl", "action": "flag" }, { - "severity": "critical", + "severity": "low", "rule_module": "code_safety", "type": "unwrap_dangerous_default", "file": "cli/src/commands/batch.rs", "action": "flag" }, { - "severity": "critical", + "severity": "low", "rule_module": "code_safety", "type": "unwrap_dangerous_default", "file": "cli/src/commands/scan.rs", "action": "flag" }, { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "clients/rust/hypatia-client/src/ffi.rs", - "action": "flag" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "data/src/cache.rs", - "action": "flag" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "data/src/dragonfly.rs", - "action": "flag" - }, - { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "integration/src/ci_simulation/scenarios.rs", - "action": "flag" - }, - { - "severity": "critical", + "severity": "low", "rule_module": "code_safety", "type": "unwrap_dangerous_default", - "file": "integration/tests/fleet_test.rs", + "file": "scripts/ci-tools/src/bin/check-k9iser-paths.rs", "action": "flag" }, { - "severity": "critical", - "rule_module": "code_safety", - "type": "unwrap_dangerous_default", - "file": "integration/tests/forge_test.rs", - "action": "flag" - }, - { - "severity": "critical", + "severity": "low", "rule_module": "code_safety", "type": "unwrap_dangerous_default", "file": "tools/cii-registrar/src/main.rs", "action": "flag" }, { - "severity": "critical", - "rule_module": "security_errors", - "type": "secret_detected", - "file": ".audittraining/security-errors/echidnabot.md", - "action": "revoke_rotate_and_purge" - }, - { - "severity": "critical", - "rule_module": "security_errors", - "type": "secret_detected", - "file": ".audittraining/security-errors/echidnabot.md", - "action": "revoke_rotate_and_purge" - }, - { - "severity": "critical", - "rule_module": "security_errors", - "type": "secret_detected", - "file": ".github/workflows/integration.yml", - "action": "revoke_rotate_and_purge" - }, - { - "severity": "critical", - "rule_module": "security_errors", - "type": "secret_detected", - "file": ".hypatia-exemptions.md", - "action": "revoke_rotate_and_purge" - }, - { - "severity": "critical", - "rule_module": "security_errors", - "type": "secret_detected", - "file": "adapters/src/codeberg.rs", - "action": "revoke_rotate_and_purge" - }, - { - "severity": "critical", - "rule_module": "security_errors", - "type": "secret_detected", - "file": "adapters/tests/adapter_tests.rs", - "action": "revoke_rotate_and_purge" - }, - { - "severity": "critical", - "rule_module": "security_errors", - "type": "secret_detected", - "file": "hooks/lib/cache.sh", - "action": "revoke_rotate_and_purge" - }, - { - "severity": "critical", - "rule_module": "security_errors", - "type": "secret_detected", - "file": "integration/run-tests.sh", - "action": "revoke_rotate_and_purge" - }, - { - "severity": "critical", - "rule_module": "security_errors", - "type": "secret_detected", - "file": "lib/rules/security_errors.ex", - "action": "revoke_rotate_and_purge" - }, - { - "severity": "critical", - "rule_module": "security_errors", - "type": "secret_detected", - "file": "scripts/fix-scripts/fix-hardcoded-secrets.sh", - "action": "revoke_rotate_and_purge" - }, - { - "severity": "critical", - "rule_module": "security_errors", - "type": "secret_detected", - "file": "scripts/fix-scripts/fix-hardcoded-secrets.sh", - "action": "revoke_rotate_and_purge" - }, - { - "severity": "critical", - "rule_module": "security_errors", - "type": "secret_detected", - "file": "test/code_safety_test.exs", - "action": "revoke_rotate_and_purge" - }, - { - "severity": "critical", - "rule_module": "structural_drift", - "type": "SD008", - "file": "src/abi/RuleEngine.idr", - "action": "fix_proof" - }, - { - "severity": "critical", - "rule_module": "workflow_audit", - "type": "actions_expression_injection", - "file": "mirror.yml", - "action": "sanitize_context" - }, - { - "severity": "critical", - "rule_module": "workflow_audit", - "type": "actions_expression_injection", - "file": "quality.yml", - "action": "sanitize_context" - }, - { - "severity": "high", - "rule_module": "cicd_rules", - "type": "missing_requirement", - "file": ".github/dependabot.yml", - "action": "create" - }, - { - "severity": "high", - "rule_module": "cicd_rules", - "type": "missing_requirement", - "file": ".github/workflows/scorecard.yml", - "action": "create" - }, - { - "severity": "high", - "rule_module": "cicd_rules", - "type": "missing_requirement", - "file": "permissions: read-all", - "action": "create" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "elixir_send_unsanitised", - "file": "lib/rules/security_errors.ex", - "action": "flag" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "from_raw", - "file": "clients/rust/hypatia-client/src/ffi.rs", - "action": "flag" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "lock_unwrap", - "file": "cli/src/app_state.rs", - "action": "flag" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "ncl_http_url", - "file": "fleet-config.k9.ncl", - "action": "flag" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "panic_macro", - "file": "integration/src/ci_simulation/assertions.rs", - "action": "flag" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "shell_download_then_run", - "file": "scripts/fix-scripts/fix-heredoc-install.sh", - "action": "flag" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "adapters/src/bitbucket.rs", - "action": "flag" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "adapters/src/codeberg.rs", - "action": "flag" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "adapters/src/github.rs", - "action": "flag" - }, - { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "adapters/src/gitlab.rs", - "action": "flag" - }, - { - "severity": "high", + "severity": "low", "rule_module": "code_safety", "type": "unwrap_without_check", - "file": "adapters/src/radicle.rs", + "file": "cli/build.rs", "action": "flag" }, { - "severity": "high", + "severity": "low", "rule_module": "code_safety", "type": "unwrap_without_check", - "file": "adapters/src/sourcehut.rs", + "file": "cli/src/commands/batch.rs", "action": "flag" }, { - "severity": "high", + "severity": "low", "rule_module": "code_safety", "type": "unwrap_without_check", - "file": "adapters/tests/adapter_tests.rs", + "file": "cli/src/commands/fleet.rs", "action": "flag" }, { - "severity": "high", + "severity": "low", "rule_module": "code_safety", "type": "unwrap_without_check", - "file": "cli/build.rs", + "file": "cli/src/commands/scan.rs", "action": "flag" }, { - "severity": "high", + "severity": "low", "rule_module": "code_safety", "type": "unwrap_without_check", - "file": "cli/src/app_state.rs", + "file": "cli/src/output.rs", "action": "flag" }, { - "severity": "high", + "severity": "low", "rule_module": "code_safety", "type": "unwrap_without_check", - "file": "cli/src/commands/batch.rs", + "file": "fixer/src/main.rs", "action": "flag" }, { - "severity": "high", + "severity": "low", "rule_module": "code_safety", "type": "unwrap_without_check", - "file": "cli/src/commands/fleet.rs", + "file": "integration/src/ci_simulation/scenarios.rs", "action": "flag" }, { - "severity": "high", + "severity": "low", "rule_module": "code_safety", "type": "unwrap_without_check", - "file": "cli/src/commands/scan.rs", + "file": "integration/src/lib.rs", "action": "flag" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "cli/src/config.rs", - "action": "flag" + "severity": "low", + "rule_module": "git_state", + "type": "GS006", + "file": ".", + "action": "review" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "cli/src/output.rs", + "severity": "low", + "rule_module": "honest_completion", + "type": "no_state_file", + "file": "/home/user/hypatia", "action": "flag" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "clients/rust/hypatia-client/src/ffi.rs", - "action": "flag" + "severity": "low", + "rule_module": "root_hygiene", + "type": "stale", + "file": "DESIGN-NARRATIVE.md", + "action": "move" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "data/src/cache.rs", - "action": "flag" + "severity": "low", + "rule_module": "structural_drift", + "type": "SD013", + "file": ".gitignore", + "action": "globalise_gitignore_pattern" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "data/src/dragonfly.rs", - "action": "flag" + "severity": "low", + "rule_module": "structural_drift", + "type": "SD013", + "file": ".gitignore", + "action": "globalise_gitignore_pattern" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "data/src/verisim.rs", - "action": "flag" + "severity": "low", + "rule_module": "structural_drift", + "type": "SD013", + "file": ".gitignore", + "action": "globalise_gitignore_pattern" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "fixer/src/main.rs", - "action": "flag" + "severity": "low", + "rule_module": "workflow_audit", + "type": "missing_workflow", + "file": "guix-nix-policy.yml", + "action": "create" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "integration/src/ci_simulation/mod.rs", - "action": "flag" + "severity": "low", + "rule_module": "workflow_audit", + "type": "missing_workflow", + "file": "instant-sync.yml", + "action": "create" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "integration/src/ci_simulation/scenarios.rs", - "action": "flag" + "severity": "low", + "rule_module": "workflow_audit", + "type": "missing_workflow", + "file": "jekyll-gh-pages.yml", + "action": "create" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "integration/src/lib.rs", - "action": "flag" + "severity": "low", + "rule_module": "workflow_audit", + "type": "missing_workflow", + "file": "jekyll.yml", + "action": "create" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "integration/tests/arangodb_test.rs", - "action": "flag" + "severity": "low", + "rule_module": "workflow_audit", + "type": "missing_workflow", + "file": "npm-bun-blocker.yml", + "action": "create" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "integration/tests/ci_simulation_test.rs", - "action": "flag" + "severity": "low", + "rule_module": "workflow_audit", + "type": "missing_workflow", + "file": "rsr-antipattern.yml", + "action": "create" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "integration/tests/fleet_test.rs", - "action": "flag" + "severity": "low", + "rule_module": "workflow_audit", + "type": "missing_workflow", + "file": "scorecard-enforcer.yml", + "action": "create" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "integration/tests/forge_test.rs", - "action": "flag" + "severity": "low", + "rule_module": "workflow_audit", + "type": "missing_workflow", + "file": "ts-blocker.yml", + "action": "create" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "integration/tests/hooks_test.rs", - "action": "flag" + "severity": "low", + "rule_module": "workflow_audit", + "type": "missing_workflow", + "file": "wellknown-enforcement.yml", + "action": "create" }, { - "severity": "high", - "rule_module": "code_safety", - "type": "unwrap_without_check", - "file": "integration/tests/registry_test.rs", - "action": "flag" + "severity": "low", + "rule_module": "workflow_audit", + "type": "missing_workflow", + "file": "workflow-linter.yml", + "action": "create" }, { - "severity": "high", + "severity": "medium", "rule_module": "git_state", - "type": "GS005", + "type": "GS001", "file": ".", - "action": "flag" + "action": "commit" }, { - "severity": "high", - "rule_module": "workflow_audit", - "type": "download_then_run", - "file": "docs.yml", - "action": "verify_download_integrity" + "severity": "medium", + "rule_module": "git_state", + "type": "GS007", + "file": ".", + "action": "delete_remote_branches" }, { - "severity": "high", - "rule_module": "workflow_audit", - "type": "unsafe_curl_payload", - "file": "hypatia-scan.yml", - "action": "use_jq_payload" + "severity": "medium", + "rule_module": "structural_drift", + "type": "SD009", + "file": "ffi/zig/src/main.zig", + "action": "add_spdx_header" } ]