Class: Rules::AiConfigInjection

Inherits:
Base
  • Object
show all
Defined in:
lib/rules/ai_config_injection.rb

Constant Summary collapse

PR_TRIGGERS =
%w[pull_request pull_request_target].freeze
AI_TOOL_ACTION_PATTERNS =
[
    /\banthropics\/claude/i,
    /\bgithub\/copilot/i,
    /\baider[_-]ai\//i,
    /\bcursor\//i,
    /\bcline\//i,
    /\bcontinue[_-]dev\//i,
    /\bwindsurf\//i,
    /\bcodex\//i,
    /\bsweep[_-]ai\//i,
    /\bdevin\//i,
].freeze
AI_TOOL_COMMANDS =
[
    /\bclaude\b/,
    /\baider\b/,
    /\bcursor\s+(review|fix|chat|ask|compose|run)\b/,
    /\bcopilot\b/,
    /\bsgpt\b/,
    /\bcline\b/,
    /\bcontinue\s+(chat|review|fix|ask|suggest|generate|dev)\b/,
    /\bwindsurf\b/,
    /\bcodex\b/,
    /\bdevin\b/,
].freeze
SANITIZATION_DIRS =
%w[
    .claude/
    .cursor/
    .continue/
    .github/copilot/
].freeze
SANITIZATION_FILES =
%w[
    .mcp.json
    CLAUDE.md
    .cursorrules
    .aider.conf.yml
    .aiderignore
    .copilot-instructions.md
    .clinerules
    .windsurfrules
    .continue/config.json
].freeze
SANITIZATION_PATHS =
(SANITIZATION_DIRS + SANITIZATION_FILES).freeze
SANITIZATION_FIX =
"Add a sanitization step after checkout: " \
"rm -rf .claude/ .cursor/ .continue/ .github/copilot/ && " \
"rm -f .mcp.json .cursorrules .aider.conf.yml .aiderignore " \
".copilot-instructions.md CLAUDE.md .clinerules .windsurfrules " \
".continue/config.json"

Instance Method Summary collapse

Instance Method Details

#check(workflow) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/rules/ai_config_injection.rb', line 62

def check(workflow)
    findings = []
    triggers = workflow.triggers

    pr_triggers = detect_pr_triggers(triggers)
    return findings if pr_triggers.empty?

    workflow.jobs.each do |_job_id, job|
        pr_triggers.each do |pr_trigger|
            is_prt = (pr_trigger == "pull_request_target")
            pr_checkout_found = false
            sanitized = false

            workflow.steps(job).each do |step|
                if !pr_checkout_found && pr_code_checkout?(step, is_prt)
                    pr_checkout_found = true
                    sanitized = false
                    next
                end

                next unless pr_checkout_found

                if sanitization_step?(step)
                    sanitized = true
                    next
                end

                if ai_tool_step?(step) && !sanitized && !isolated_working_dir?(step)
                    tool_name = identify_ai_tool(step)
                    sev = is_prt ? :critical : :high

                    code = step["uses"] ? "uses: #{step["uses"]}" : step["run"]&.lines&.first&.strip
                    line = if step["uses"]
                        workflow.line_of(/uses:\s*#{Regexp.escape(step["uses"])}/) || 0
                    elsif step["run"]
                        first_line = step["run"].lines.first&.strip
                        first_line ? (workflow.line_of(/#{Regexp.escape(first_line[0..40])}/) || 0) : 0
                    else
                        0
                    end

                    findings << Finding.new(
                        rule: name,
                        severity: sev,
                        file: workflow.filename,
                        line: line,
                        code: code,
                        message: "#{tool_name} runs on PR checkout code (#{pr_trigger} trigger) " \
                            "— attacker-controlled AI config files execute arbitrary code",
                        fix: SANITIZATION_FIX
                    )
                end
            end
        end
    end

    findings
end

#descriptionObject



4
# File 'lib/rules/ai_config_injection.rb', line 4

def description = "AI tool runs on PR checkout code with attacker-controlled config"

#nameObject



3
# File 'lib/rules/ai_config_injection.rb', line 3

def name = "ai-config-injection"

#severityObject



5
# File 'lib/rules/ai_config_injection.rb', line 5

def severity = :critical