Skip to content

Commit cf222c2

Browse files
authored
Add secret-masking frontmatter field for custom redaction steps (#2881)
1 parent a683a84 commit cf222c2

File tree

13 files changed

+2392
-71
lines changed

13 files changed

+2392
-71
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
secret-masking:
3+
steps:
4+
- name: Redact dummy password pattern
5+
if: always()
6+
run: |
7+
echo "Searching for dummy password patterns in /tmp/gh-aw/"
8+
find /tmp/gh-aw -type f -exec sed -i 's/password123/REDACTED/g' {} + 2>/dev/null || true
9+
echo "Secret masking complete"
10+
---
11+
12+
This shared workflow provides additional secret redaction steps to mask dummy password patterns in generated files.

.github/workflows/test-secret-masking.lock.yml

Lines changed: 1741 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
on: workflow_dispatch
3+
permissions:
4+
contents: read
5+
engine: copilot
6+
imports:
7+
- shared/secret-redaction-test.md
8+
---
9+
10+
# Test Secret Masking Workflow
11+
12+
This workflow tests the secret-masking feature by importing custom secret redaction steps.
13+
14+
The imported steps will search for and replace the pattern "password123" with "REDACTED" in all files under /tmp/gh-aw/.

docs/src/content/docs/reference/frontmatter-full.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,6 +1740,14 @@ safe-outputs:
17401740
# (optional)
17411741
runs-on: "example-value"
17421742

1743+
# Configuration for secret redaction behavior in workflow outputs and artifacts
1744+
# (optional)
1745+
secret-masking:
1746+
# Additional secret redaction steps to inject after the built-in secret redaction.
1747+
# Use this to mask secrets in generated files using custom patterns.
1748+
# (optional)
1749+
steps: []
1750+
17431751
# Repository access roles required to trigger agentic workflows. Defaults to
17441752
# ['admin', 'maintainer', 'write'] for security. Use 'all' to allow any
17451753
# authenticated user (⚠️ security consideration).

docs/src/content/docs/status.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Browse the [workflow source files](https://github.com/githubnext/gh-aw/tree/main
6565
| [Test jqschema](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/test-jqschema.md) | copilot | [![Test jqschema](https://github.com/githubnext/gh-aw/actions/workflows/test-jqschema.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/test-jqschema.lock.yml) | - | - |
6666
| [Test Ollama Threat Scanning](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/test-ollama-threat-detection.md) | copilot | [![Test Ollama Threat Scanning](https://github.com/githubnext/gh-aw/actions/workflows/test-ollama-threat-detection.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/test-ollama-threat-detection.lock.yml) | - | - |
6767
| [Test Post-Steps Workflow](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/test-post-steps.md) | copilot | [![Test Post-Steps Workflow](https://github.com/githubnext/gh-aw/actions/workflows/test-post-steps.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/test-post-steps.lock.yml) | - | - |
68+
| [Test Secret Masking Workflow](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/test-secret-masking.md) | copilot | [![Test Secret Masking Workflow](https://github.com/githubnext/gh-aw/actions/workflows/test-secret-masking.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/test-secret-masking.lock.yml) | - | - |
6869
| [Test Svelte MCP](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/test-svelte.md) | copilot | [![Test Svelte MCP](https://github.com/githubnext/gh-aw/actions/workflows/test-svelte.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/test-svelte.lock.yml) | - | - |
6970
| [The Daily Repository Chronicle](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/daily-repo-chronicle.md) | copilot | [![The Daily Repository Chronicle](https://github.com/githubnext/gh-aw/actions/workflows/daily-repo-chronicle.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/daily-repo-chronicle.lock.yml) | `0 16 * * 1-5` | - |
7071
| [Tidy](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/tidy.md) | copilot | [![Tidy](https://github.com/githubnext/gh-aw/actions/workflows/tidy.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/tidy.lock.yml) | - | - |

pkg/parser/frontmatter.go

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,19 @@ type FrontmatterResult struct {
9090

9191
// ImportsResult holds the result of processing imports from frontmatter
9292
type ImportsResult struct {
93-
MergedTools string // Merged tools configuration from all imports
94-
MergedMCPServers string // Merged mcp-servers configuration from all imports
95-
MergedEngines []string // Merged engine configurations from all imports
96-
MergedSafeOutputs []string // Merged safe-outputs configurations from all imports
97-
MergedMarkdown string // Merged markdown content from all imports
98-
MergedSteps string // Merged steps configuration from all imports
99-
MergedRuntimes string // Merged runtimes configuration from all imports
100-
MergedServices string // Merged services configuration from all imports
101-
MergedNetwork string // Merged network configuration from all imports
102-
MergedPermissions string // Merged permissions configuration from all imports
103-
ImportedFiles []string // List of imported file paths (for manifest)
104-
AgentFile string // Path to custom agent file (if imported)
93+
MergedTools string // Merged tools configuration from all imports
94+
MergedMCPServers string // Merged mcp-servers configuration from all imports
95+
MergedEngines []string // Merged engine configurations from all imports
96+
MergedSafeOutputs []string // Merged safe-outputs configurations from all imports
97+
MergedMarkdown string // Merged markdown content from all imports
98+
MergedSteps string // Merged steps configuration from all imports
99+
MergedRuntimes string // Merged runtimes configuration from all imports
100+
MergedServices string // Merged services configuration from all imports
101+
MergedNetwork string // Merged network configuration from all imports
102+
MergedPermissions string // Merged permissions configuration from all imports
103+
MergedSecretMasking string // Merged secret-masking steps from all imports
104+
ImportedFiles []string // List of imported file paths (for manifest)
105+
AgentFile string // Path to custom agent file (if imported)
105106
}
106107

107108
// ExtractFrontmatterFromContent parses YAML frontmatter from markdown content string
@@ -409,6 +410,7 @@ func ProcessImportsFromFrontmatterWithManifest(frontmatter map[string]any, baseD
409410
var servicesBuilder strings.Builder
410411
var networkBuilder strings.Builder
411412
var permissionsBuilder strings.Builder
413+
var secretMaskingBuilder strings.Builder
412414
var engines []string
413415
var safeOutputs []string
414416
var processedFiles []string
@@ -550,21 +552,28 @@ func ProcessImportsFromFrontmatterWithManifest(frontmatter map[string]any, baseD
550552
if err == nil && permissionsContent != "" && permissionsContent != "{}" {
551553
permissionsBuilder.WriteString(permissionsContent + "\n")
552554
}
555+
556+
// Extract secret-masking from imported file
557+
secretMaskingContent, err := extractSecretMaskingFromContent(string(content))
558+
if err == nil && secretMaskingContent != "" && secretMaskingContent != "{}" {
559+
secretMaskingBuilder.WriteString(secretMaskingContent + "\n")
560+
}
553561
}
554562

555563
return &ImportsResult{
556-
MergedTools: toolsBuilder.String(),
557-
MergedMCPServers: mcpServersBuilder.String(),
558-
MergedEngines: engines,
559-
MergedSafeOutputs: safeOutputs,
560-
MergedMarkdown: markdownBuilder.String(),
561-
MergedSteps: stepsBuilder.String(),
562-
MergedRuntimes: runtimesBuilder.String(),
563-
MergedServices: servicesBuilder.String(),
564-
MergedNetwork: networkBuilder.String(),
565-
MergedPermissions: permissionsBuilder.String(),
566-
ImportedFiles: processedFiles,
567-
AgentFile: agentFile,
564+
MergedTools: toolsBuilder.String(),
565+
MergedMCPServers: mcpServersBuilder.String(),
566+
MergedEngines: engines,
567+
MergedSafeOutputs: safeOutputs,
568+
MergedMarkdown: markdownBuilder.String(),
569+
MergedSteps: stepsBuilder.String(),
570+
MergedRuntimes: runtimesBuilder.String(),
571+
MergedServices: servicesBuilder.String(),
572+
MergedNetwork: networkBuilder.String(),
573+
MergedPermissions: permissionsBuilder.String(),
574+
MergedSecretMasking: secretMaskingBuilder.String(),
575+
ImportedFiles: processedFiles,
576+
AgentFile: agentFile,
568577
}, nil
569578
}
570579

@@ -1024,6 +1033,11 @@ func ExtractPermissionsFromContent(content string) (string, error) {
10241033
return extractFrontmatterField(content, "permissions", "{}")
10251034
}
10261035

1036+
// extractSecretMaskingFromContent extracts secret-masking section from frontmatter as JSON string
1037+
func extractSecretMaskingFromContent(content string) (string, error) {
1038+
return extractFrontmatterField(content, "secret-masking", "{}")
1039+
}
1040+
10271041
// extractFrontmatterField extracts a specific field from frontmatter as JSON string
10281042
func extractFrontmatterField(content, fieldName, emptyValue string) (string, error) {
10291043
result, err := ExtractFrontmatterFromContent(content)

pkg/parser/schemas/included_file_schema.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,21 @@
257257
},
258258
"additionalProperties": false
259259
},
260+
"secret-masking": {
261+
"type": "object",
262+
"description": "Secret masking configuration to be merged with main workflow",
263+
"properties": {
264+
"steps": {
265+
"type": "array",
266+
"description": "Additional secret redaction steps to inject after the built-in secret redaction",
267+
"items": {
268+
"type": "object",
269+
"additionalProperties": true
270+
}
271+
}
272+
},
273+
"additionalProperties": false
274+
},
260275
"runtimes": {
261276
"type": "object",
262277
"description": "Runtime environment version overrides. Allows customizing runtime versions (e.g., Node.js, Python) or defining new runtimes. Merged with main workflow runtimes.",

pkg/parser/schemas/main_workflow_schema.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2904,6 +2904,28 @@
29042904
},
29052905
"additionalProperties": false
29062906
},
2907+
"secret-masking": {
2908+
"type": "object",
2909+
"description": "Configuration for secret redaction behavior in workflow outputs and artifacts",
2910+
"properties": {
2911+
"steps": {
2912+
"type": "array",
2913+
"description": "Additional secret redaction steps to inject after the built-in secret redaction. Use this to mask secrets in generated files using custom patterns.",
2914+
"items": {
2915+
"$ref": "#/properties/githubActionsStep"
2916+
},
2917+
"examples": [
2918+
[
2919+
{
2920+
"name": "Redact custom secrets",
2921+
"run": "find /tmp/gh-aw -type f -exec sed -i 's/password123/REDACTED/g' {} +"
2922+
}
2923+
]
2924+
]
2925+
}
2926+
},
2927+
"additionalProperties": false
2928+
},
29072929
"roles": {
29082930
"description": "Repository access roles required to trigger agentic workflows. Defaults to ['admin', 'maintainer', 'write'] for security. Use 'all' to allow any authenticated user (\u26a0\ufe0f security consideration).",
29092931
"oneOf": [

pkg/workflow/compiler.go

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -170,26 +170,27 @@ type WorkflowData struct {
170170
EngineConfig *EngineConfig // Extended engine configuration
171171
AgentFile string // Path to custom agent file (from imports)
172172
StopTime string
173-
Command string // for /command trigger support
174-
CommandEvents []string // events where command should be active (nil = all events)
175-
CommandOtherEvents map[string]any // for merging command with other events
176-
AIReaction string // AI reaction type like "eyes", "heart", etc.
177-
Jobs map[string]any // custom job configurations with dependencies
178-
Cache string // cache configuration
179-
NeedsTextOutput bool // whether the workflow uses ${{ needs.task.outputs.text }}
180-
NetworkPermissions *NetworkPermissions // parsed network permissions
181-
SafeOutputs *SafeOutputsConfig // output configuration for automatic output routes
182-
Roles []string // permission levels required to trigger workflow
183-
CacheMemoryConfig *CacheMemoryConfig // parsed cache-memory configuration
184-
SafetyPrompt bool // whether to include XPIA safety prompt (default true)
185-
Runtimes map[string]any // runtime version overrides from frontmatter
186-
ToolsTimeout int // timeout in seconds for tool/MCP operations (0 = use engine default)
187-
GitHubToken string // top-level github-token expression from frontmatter
188-
ToolsStartupTimeout int // timeout in seconds for MCP server startup (0 = use engine default)
189-
Features map[string]bool // feature flags from frontmatter
190-
ActionCache *ActionCache // cache for action pin resolutions
191-
ActionResolver *ActionResolver // resolver for action pins
192-
StrictMode bool // strict mode for action pinning
173+
Command string // for /command trigger support
174+
CommandEvents []string // events where command should be active (nil = all events)
175+
CommandOtherEvents map[string]any // for merging command with other events
176+
AIReaction string // AI reaction type like "eyes", "heart", etc.
177+
Jobs map[string]any // custom job configurations with dependencies
178+
Cache string // cache configuration
179+
NeedsTextOutput bool // whether the workflow uses ${{ needs.task.outputs.text }}
180+
NetworkPermissions *NetworkPermissions // parsed network permissions
181+
SafeOutputs *SafeOutputsConfig // output configuration for automatic output routes
182+
Roles []string // permission levels required to trigger workflow
183+
CacheMemoryConfig *CacheMemoryConfig // parsed cache-memory configuration
184+
SafetyPrompt bool // whether to include XPIA safety prompt (default true)
185+
Runtimes map[string]any // runtime version overrides from frontmatter
186+
ToolsTimeout int // timeout in seconds for tool/MCP operations (0 = use engine default)
187+
GitHubToken string // top-level github-token expression from frontmatter
188+
ToolsStartupTimeout int // timeout in seconds for MCP server startup (0 = use engine default)
189+
Features map[string]bool // feature flags from frontmatter
190+
ActionCache *ActionCache // cache for action pin resolutions
191+
ActionResolver *ActionResolver // resolver for action pins
192+
StrictMode bool // strict mode for action pinning
193+
SecretMasking *SecretMaskingConfig // secret masking configuration
193194
}
194195

195196
// BaseSafeOutputConfig holds common configuration fields for all safe output types
@@ -223,6 +224,11 @@ type SafeOutputsConfig struct {
223224
RunsOn string `yaml:"runs-on,omitempty"` // Runner configuration for safe-outputs jobs
224225
}
225226

227+
// SecretMaskingConfig holds configuration for secret redaction behavior
228+
type SecretMaskingConfig struct {
229+
Steps []map[string]any `yaml:"steps,omitempty"` // Additional secret redaction steps to inject after built-in redaction
230+
}
231+
226232
// CompileWorkflow converts a markdown workflow to GitHub Actions YAML
227233
func (c *Compiler) CompileWorkflow(markdownPath string) error {
228234
// Parse the markdown file
@@ -757,6 +763,17 @@ func (c *Compiler) ParseWorkflowFile(markdownPath string) (*WorkflowData, error)
757763
// Extract SafeOutputs configuration early so we can use it when applying default tools
758764
safeOutputs := c.extractSafeOutputsConfig(result.Frontmatter)
759765

766+
// Extract SecretMasking configuration
767+
secretMasking := c.extractSecretMaskingConfig(result.Frontmatter)
768+
769+
// Merge secret-masking from imports with top-level secret-masking
770+
if importsResult.MergedSecretMasking != "" {
771+
secretMasking, err = c.MergeSecretMasking(secretMasking, importsResult.MergedSecretMasking)
772+
if err != nil {
773+
return nil, fmt.Errorf("failed to merge secret-masking: %w", err)
774+
}
775+
}
776+
760777
var tools map[string]any
761778

762779
// Extract tools from the main file
@@ -928,6 +945,7 @@ func (c *Compiler) ParseWorkflowFile(markdownPath string) (*WorkflowData, error)
928945
TrialLogicalRepo: c.trialLogicalRepoSlug,
929946
GitHubToken: extractStringValue(result.Frontmatter, "github-token"),
930947
StrictMode: c.strictMode,
948+
SecretMasking: secretMasking,
931949
}
932950

933951
// Initialize action cache and resolver

pkg/workflow/compiler_yaml.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowDat
262262

263263
// Add secret redaction step BEFORE any artifact uploads
264264
// This ensures all artifacts are scanned for secrets before being uploaded
265-
c.generateSecretRedactionStep(yaml, yaml.String())
265+
c.generateSecretRedactionStep(yaml, yaml.String(), data)
266266

267267
// Add output collection step only if safe-outputs feature is used (GH_AW_SAFE_OUTPUTS functionality)
268268
if data.SafeOutputs != nil {

0 commit comments

Comments
 (0)