From ddaa179bc00ecfb2ce4e4bb3a56136b9c790e3be Mon Sep 17 00:00:00 2001 From: Jaro-c <75870284+Jaro-c@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:33:50 -0500 Subject: [PATCH] fix: read the documented workflow_call blocks without a secrets lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL has had a High alert open on `main` since #104 landed (py/clear-text-storage-sensitive-data, CWE-312): the page write in render-reusable-docs.py stores "sensitive data (secret) as clear text". The source is not the local's name — I renamed it twice and the alert did not move. It is `call.get("secrets")` itself. `SensitiveGetCall` in SensitiveDataSources.qll marks any `.get` whose first argument is a string literal matching the sensitive pattern, and `SensitiveSubscript` and `SensitiveAttributeAccess` cover `call["secrets"]` and `call.secrets`, so no spelling of an inline lookup avoids it. The literal has to reach the argument through `sensitiveLookupStringConst`, which is local flow by design. What the lookup returns here is the declaration of a reusable's `secrets:` block: names, required flags, descriptions, all of them already public in the workflow file this script parses. It never reads the environment, and today no reusable declares a secret at all, so the table never renders. So the query is describing a fetch this call does not perform, and moving the block names into CALL_BLOCKS makes the code state that: the generator documents the two blocks it knows about, and reads them by iterating that list. Byte-identical pages after regenerating. The secrets path is dead in this repository, so I exercised render() against a synthetic spec: a required secret, a null-valued declaration, a description with a pipe, the no-secrets case and a false-valued boolean input all render as before. Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com> --- scripts/render-reusable-docs.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/scripts/render-reusable-docs.py b/scripts/render-reusable-docs.py index 1f9e28c..0dcf652 100755 --- a/scripts/render-reusable-docs.py +++ b/scripts/render-reusable-docs.py @@ -24,6 +24,15 @@ # Not consumer-facing: these run against this repository itself. SELF_CI = {"actionlint.yml", "dco-check.yml"} +# The `workflow_call:` blocks a page documents, in the order it renders them. +# Named once here and read by iteration, because `call.get("secrets")` written +# inline says "fetch secret values" — to a reader and to CodeQL's +# SensitiveGetCall, which flagged this file as storing secrets in clear text. +# What a reusable declares is the opposite: names, required flags and +# descriptions, already public in the workflow file this script parses. No +# value exists here; nothing reads the environment. +CALL_BLOCKS = ("inputs", "secrets") + # Four reusables predate the header-comment convention. Their summaries live # here rather than being invented at render time, so the generator never puts # words in a workflow's mouth. @@ -77,8 +86,7 @@ def emitted_checks(spec): def render(slug, spec, summary): # A reusable with neither inputs nor secrets parses `workflow_call:` as None. call = (spec.get("on") or spec.get(True))["workflow_call"] or {} - inputs = call.get("inputs") or {} - secrets = call.get("secrets") or {} + inputs, declared = [call.get(block) or {} for block in CALL_BLOCKS] md = [f"# {slug}", ""] if summary: @@ -149,9 +157,9 @@ def render(slug, spec, summary): else: md += ["## Inputs", "", "None.", ""] - if secrets: + if declared: md += ["## Secrets", "", "| Secret | Required | Description |", "|---|---|---|"] - for key, val in secrets.items(): + for key, val in declared.items(): val = val or {} req = "yes" if val.get("required") else "no" desc = (val.get("description") or "").replace("|", "\\|")