Class: Scanner

Inherits:
Object
  • Object
show all
Defined in:
lib/scanner.rb

Constant Summary collapse

PLATFORM_OPTIONS =
%i[auto github gitlab bitbucket].freeze

Instance Method Summary collapse

Constructor Details

#initialize(client:, formatter:, min_severity: :low, policy: nil, platform: :auto) ⇒ Scanner

Returns a new instance of Scanner.



20
21
22
23
24
25
26
27
# File 'lib/scanner.rb', line 20

def initialize(client:, formatter:, min_severity: :low, policy: nil, platform: :auto)
    @client = client
    @formatter = formatter
    @min_severity = min_severity
    @policy = policy || Policy.new
    @platform = platform
    @engine = RuleEngine.new
end

Instance Method Details

#scan(repo) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
120
121
122
123
124
125
126
127
128
# File 'lib/scanner.rb', line 29

def scan(repo)
    findings = []
    workflow_count = 0

    # Scan GitHub Actions workflows (unless platform explicitly excludes it)
    if scan_github?
        raw_workflows = @client.fetch_workflows(repo)

        workflows = raw_workflows.map { |w|
            Workflow.new(filename: w[:filename], content: w[:content])
        }

        workflow_count = workflows.length

        dependabot = @client.fetch_dependabot_config(repo)
        has_zizmor = workflows.any? { |w| w.filename.match?(/zizmor/i) }
        has_dependabot_actions = dependabot_has_actions?(dependabot)

        workflows.each do |wf|
            next if wf.parse_error?
            findings.concat(@engine.scan(wf))
        end

        unless has_dependabot_actions
            findings << Finding.new(
                rule: "missing-dependabot",
                severity: :low,
                file: "dependabot.yml",
                line: 0,
                code: nil,
                message: "No Dependabot configuration for github-actions ecosystem",
                fix: "Add package-ecosystem: github-actions to .github/dependabot.yml"
            )
        end

        unless has_zizmor
            findings << Finding.new(
                rule: "missing-zizmor",
                severity: :low,
                file: "(missing)",
                line: 0,
                code: nil,
                message: "No zizmor static analysis workflow found",
                fix: "Add a security_zizmor.yml workflow for GitHub Actions static analysis"
            )
        end
    end

    # Scan GitLab/Bitbucket platform configs when client supports it
    if @client.respond_to?(:fetch_platform_configs)
        platform_configs = @client.fetch_platform_configs

        platform_configs.each do |config|
            next unless scan_platform?(config[:platform])

            platform_scanner = case config[:platform]
            when :gitlab
                defined?(Platforms::GitLab) ? Platforms::GitLab.new(config[:content], filename: config[:filename]) : nil
            when :bitbucket
                defined?(Platforms::Bitbucket) ? Platforms::Bitbucket.new(config[:content], filename: config[:filename]) : nil
            end

            findings.concat(platform_scanner.scan) if platform_scanner
        end
    end

    findings.select! { |f| severity_passes?(f.severity) }

    # Apply policy overrides
    if @policy.loaded?
        # Filter out ignored files
        findings.reject! { |f| @policy.ignored?(f.file) }

        # Filter out excepted findings
        findings.reject! { |f| @policy.excepted?(f) }

        # Apply rule severity overrides — :off removes, others change severity
        findings.map! { |f|
            override = @policy.rule_severity(f.rule)
            if override == :off
                nil
            elsif override
                Finding.new(**f.to_h.merge(severity: override))
            else
                f
            end
        }.compact!

        # Re-apply severity filter after overrides
        findings.select! { |f| severity_passes?(f.severity) }
    end

    output = @formatter.format(
        repo: repo,
        workflow_count: workflow_count,
        findings: findings
    )

    { output: output, findings: findings, workflow_count: workflow_count }
end

#scan_org(org) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
# File 'lib/scanner.rb', line 130

def scan_org(org)
    repos = @client.fetch_repos(org)
    results = []

    repos.each do |repo|
        $stderr.puts "Scanning #{repo}..." if @formatter.is_a?(Formatter::Terminal)
        results << scan(repo)
    end

    results
end