Skip to content
4 changes: 4 additions & 0 deletions docs/src/content/docs/reference/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ Agentic markdown supports GitHub Actions expression substitutions and conditiona

This design enables rapid iteration on AI instructions while maintaining strict compilation requirements for security-sensitive frontmatter configuration. See [Editing Workflows](/gh-aw/guides/editing-workflows/) for complete guidance on when recompilation is needed versus when you can edit directly.

## Security Scanning

The markdown body of workflows (excluding frontmatter) is automatically scanned for malicious content when added via `gh aw add`, during trial mode, and at compile time for imported files. The scanner rejects workflows containing: Unicode abuse (zero-width characters, bidirectional overrides), hidden content (suspicious HTML comments, CSS-hidden elements), obfuscated links (data URIs, `javascript:` URLs, IP-based URLs, URL shorteners), dangerous HTML tags (`<script>`, `<iframe>`, `<object>`, `<form>`, event handlers), embedded executable content (SVG scripts, executable MIME data URIs), and social engineering patterns (prompt injection, base64-encoded commands, pipe-to-shell patterns). These checks cannot be overridden.

## Related Documentation

- [Editing Workflows](/gh-aw/guides/editing-workflows/) - When to recompile vs edit directly
Expand Down
11 changes: 11 additions & 0 deletions pkg/cli/add_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/github/gh-aw/pkg/constants"
"github.com/github/gh-aw/pkg/logger"
"github.com/github/gh-aw/pkg/tty"
workflowpkg "github.com/github/gh-aw/pkg/workflow"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird keyword, it's not commonly used

"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -395,6 +396,16 @@ func addWorkflowWithTracking(workflow *WorkflowSpec, number int, verbose bool, q
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Read workflow content (%d bytes)", len(sourceContent))))
}

// Security scan: reject workflows containing malicious or dangerous content
if findings := workflowpkg.ScanMarkdownSecurity(string(sourceContent)); len(findings) > 0 {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage("Security scan failed for workflow"))
fmt.Fprintln(os.Stderr, workflowpkg.FormatSecurityFindings(findings))
return fmt.Errorf("workflow '%s' failed security scan: %d issue(s) detected", workflowPath, len(findings))
}
if verbose {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Security scan passed"))
}

// Find git root to ensure consistent placement
gitRoot, err := findGitRoot()
if err != nil {
Expand Down
10 changes: 10 additions & 0 deletions pkg/cli/trial_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,16 @@ func installLocalWorkflowInTrialMode(originalDir, tempDir string, parsedSpec *Wo
return fmt.Errorf("failed to read local workflow file: %w", err)
}

// Security scan: reject workflows containing malicious or dangerous content
if findings := workflow.ScanMarkdownSecurity(string(content)); len(findings) > 0 {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage("Security scan failed for local workflow"))
fmt.Fprintln(os.Stderr, workflow.FormatSecurityFindings(findings))
return fmt.Errorf("local workflow '%s' failed security scan: %d issue(s) detected", parsedSpec.WorkflowName, len(findings))
}
if verbose {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Security scan passed"))
}

// Append text if provided
if appendText != "" {
contentStr := string(content)
Expand Down
31 changes: 31 additions & 0 deletions pkg/workflow/compiler_orchestrator_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package workflow
import (
"fmt"
"os"
"strings"

"github.com/github/gh-aw/pkg/console"
"github.com/github/gh-aw/pkg/logger"
Expand Down Expand Up @@ -99,6 +100,36 @@ func (c *Compiler) setupEngineAndImports(result *parser.FrontmatterResult, clean
return nil, err // Error is already formatted with source location
}

// Security scan imported markdown files' content (skip non-markdown imports like .yml)
for _, importedFile := range importsResult.ImportedFiles {
// Strip section references (e.g., "shared/foo.md#Section")
importFilePath := importedFile
if idx := strings.Index(importFilePath, "#"); idx >= 0 {
importFilePath = importFilePath[:idx]
}
// Only scan markdown files — .yml imports are YAML config, not markdown content
if !strings.HasSuffix(importFilePath, ".md") {
continue
}
// Resolve the import path to a full filesystem path
fullPath, resolveErr := parser.ResolveIncludePath(importFilePath, markdownDir, importCache)
if resolveErr != nil {
orchestratorEngineLog.Printf("Skipping security scan for unresolvable import: %s: %v", importedFile, resolveErr)
fmt.Fprintf(os.Stderr, "WARNING: Skipping security scan for unresolvable import '%s': %v\n", importedFile, resolveErr)
continue
}
importContent, readErr := os.ReadFile(fullPath)
if readErr != nil {
orchestratorEngineLog.Printf("Skipping security scan for unreadable import: %s: %v", fullPath, readErr)
fmt.Fprintf(os.Stderr, "WARNING: Skipping security scan for unreadable import '%s' (resolved path: %s): %v\n", importedFile, fullPath, readErr)
continue
}
if findings := ScanMarkdownSecurity(string(importContent)); len(findings) > 0 {
orchestratorEngineLog.Printf("Security scan failed for imported file: %s (%d findings)", importedFile, len(findings))
return nil, fmt.Errorf("imported workflow '%s' failed security scan: %s", importedFile, FormatSecurityFindings(findings))
}
}

// Merge network permissions from imports with top-level network permissions
if importsResult.MergedNetwork != "" {
orchestratorEngineLog.Printf("Merging network permissions from imports")
Expand Down
Loading
Loading