classifyStepSecrets separates secrets found in a step into two categories: - unsafeRefs: secrets found in fields other than "env" or "with" (for uses: action steps), or secrets in env:/with: bindings when the step also writes to $GITHUB_ENV - safeRefs: secrets found in step-level env: bindings (cont
(step any)
| 142 | // safe-bound secrets are treated as entirely unsafe, because writing to |
| 143 | // $GITHUB_ENV would leak the secret to subsequent steps (including the agent). |
| 144 | func classifyStepSecrets(step any) (unsafeRefs, safeRefs []string) { |
| 145 | stepMap, ok := step.(map[string]any) |
| 146 | if !ok { |
| 147 | // Non-map steps: all secrets are considered unsafe. |
| 148 | return extractSecretsFromStepValue(step), nil |
| 149 | } |
| 150 | |
| 151 | // Check if this is a uses: action step. For action steps, with: inputs are |
| 152 | // passed to the external action (not interpolated into shell scripts), and |
| 153 | // the GitHub Actions runner masks with: values derived from secrets. |
| 154 | // Only treat with: as safe when uses is a valid non-empty string reference. |
| 155 | usesVal, hasUses := stepMap["uses"] |
| 156 | if hasUses { |
| 157 | usesStr, isString := usesVal.(string) |
| 158 | hasUses = isString && strings.TrimSpace(usesStr) != "" |
| 159 | } |
| 160 | |
| 161 | var localUnsafe, localSafe []string |
| 162 | for key, val := range stepMap { |
| 163 | refs := extractSecretsFromStepValue(val) |
| 164 | if key == "env" { |
| 165 | if _, isMap := val.(map[string]any); isMap { |
| 166 | localSafe = append(localSafe, refs...) |
| 167 | } else { |
| 168 | // Malformed env (string, slice, etc.): treat as unsafe. |
| 169 | localUnsafe = append(localUnsafe, refs...) |
| 170 | } |
| 171 | } else if key == "with" && hasUses { |
| 172 | if _, isMap := val.(map[string]any); isMap { |
| 173 | localSafe = append(localSafe, refs...) |
| 174 | } else { |
| 175 | // Malformed with (string, slice, etc.): treat as unsafe. |
| 176 | localUnsafe = append(localUnsafe, refs...) |
| 177 | } |
| 178 | } else { |
| 179 | localUnsafe = append(localUnsafe, refs...) |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | // If the step has safe-bound secrets AND references $GITHUB_ENV in any |
| 184 | // non-env/non-with field, reclassify all safe refs as unsafe. Writing to |
| 185 | // $GITHUB_ENV would persist the secret to subsequent steps. |
| 186 | if len(localSafe) > 0 && stepReferencesGitHubEnv(stepMap) { |
| 187 | localUnsafe = append(localUnsafe, localSafe...) |
| 188 | localSafe = nil |
| 189 | } |
| 190 | |
| 191 | return localUnsafe, localSafe |
| 192 | } |
| 193 | |
| 194 | // extractSecretsFromStepValue recursively walks a step value (which may be a map, |
| 195 | // slice, or primitive) and returns all secrets.* expressions found in string values. |