File Coverage

File:lib/CheckSpelling/Sarif.pm
Coverage:86.1%

linestmtbrancondsubtimecode
1#! -*-perl-*-
2
3package CheckSpelling::Sarif;
4
5our $VERSION='0.1.0';
6our $flatten=0;
7
8
1
1
1
100614
1
2
use utf8;
9
10
1
1
1
13
1
44
use File::Basename;
11
1
1
1
175
1098
53
use Digest::SHA qw($errmsg);
12
1
1
1
1
1
24
use JSON::PP;
13
1
1
1
152
3703
25
use Hash::Merge qw( merge );
14
1
1
1
128
1
21
use CheckSpelling::Util;
15
1
1
1
148
0
1617
use CheckSpelling::GitSources;
16
17sub encode_low_ascii {
18
8
160
    $_ = shift;
19
8
1
9
3
    s/([\x{0}-\x{9}\x{0b}\x{1f}#%])/"\\u".sprintf("%04x",ord($1))/eg;
20
8
5
    return $_;
21}
22
23sub url_encode {
24
9
13
    $_ = shift;
25
9
0
8
0
    s<([^-!\$&'()*+,/:;=?\@A-Za-z0-9_.~])><"%".sprintf("%02x",ord($1))>eg;
26
9
16
    return $_;
27}
28
29sub double_slash_escape {
30
7
3
    $_ = shift;
31
7
22
    s/(["()\]\\])/\\\\$1/g;
32
7
25
    return $_;
33}
34
35sub fingerprintLocations {
36
5
6
    my ($locations, $encoded_files_ref, $line_hashes_ref, $hashes_needed_for_files_ref, $message, $hashed_message) = @_;
37
5
3
    my @locations_json = ();
38
5
4
    my @fingerprints = ();
39
5
14
    for my $location (@$locations) {
40
8
7
        my $encoded_file = $location->{uri};
41
8
4
        my $line = $location->{startLine};
42
8
5
        my $column = $location->{startColumn};
43
8
4
        my $endColumn = $location->{endColumn};
44
8
5
        my $partialFingerprint = '';
45
8
5
        my $file = $encoded_files_ref->{$encoded_file};
46
8
5
        if (defined $line_hashes_ref->{$file}) {
47
8
13
            my $line_hash = $line_hashes_ref->{$file}{$line};
48
8
6
            if (defined $line_hash) {
49
4
4
1
8
                my @instances = sort keys %{$hashes_needed_for_files_ref->{$file}{$line}{$hashed_message}};
50
4
1
                my $hit = scalar @instances;
51
4
4
                while (--$hit > 0) {
52
0
0
                    last if $instances[$hit] == $column;
53                }
54
4
7
                $partialFingerprint = Digest::SHA::sha1_base64("$line_hash:$message:$hit");
55            }
56        }
57
8
6
        push @fingerprints, $partialFingerprint;
58
8
7
        my $startColumn = $column ? qq<, "startColumn": $column> : '';
59
8
7
        $endColumn = $endColumn ? qq<, "endColumn": $endColumn> : '';
60
8
5
        $line = 1 unless $line;
61
8
6
        my $json_fragment = qq<{ "physicalLocation": { "artifactLocation": { "uri": "$encoded_file", "uriBaseId": "%SRCROOT%" }, "region": { "startLine": $line$startColumn$endColumn } } }>;
62
8
5
        push @locations_json, $json_fragment;
63    }
64
5
9
    return { locations_json => \@locations_json, fingerprints => \@fingerprints };
65}
66
67sub hashFiles {
68
2
2
    my ($hashes_needed_for_files_ref, $line_hashes_ref, $directoryToRepo_ref, $used_hashes_ref) = @_;
69
2
6
    for my $file (sort keys %$hashes_needed_for_files_ref) {
70
2
3
        $line_hashes_ref->{$file} = ();
71
2
14
        unless (-e $file) {
72
0
0
            delete $hashes_needed_for_files_ref->{$file};
73
0
0
            next;
74        }
75
2
2
1
6
        my @lines = sort (keys %{$hashes_needed_for_files_ref->{$file}});
76
2
68
        unless (defined $directoryToRepo_ref->{dirname($file)}) {
77
2
9
            my ($parsed_file, $git_base_dir, $prefix, $remote_url, $rev, $branch) = CheckSpelling::GitSources::git_source_and_rev($file);
78        }
79
2
31
        open $file_fh, '<', $file;
80
2
4
        my $line = shift @lines;
81
2
2
        $line = 2 if $line == 1;
82
2
3
        my $buffer = '';
83
2
15
        while (<$file_fh>) {
84
94
59
            if ($line == $.) {
85
5
5
                my $sample = substr $buffer, -100, 100;
86
5
6
                my $hash = Digest::SHA::sha1_base64($sample);
87
5
6
                for (; $line == $.; $line = shift @lines) {
88
6
8
                    my $hit = $used_hashes_ref->{$hash}++;
89
6
5
                    $hash = "$hash:$hit" if $hit;
90
6
4
                    $line_hashes_ref->{$file}{$line} = $hash;
91
6
7
                    last unless @lines;
92                }
93            }
94
94
49
            $buffer .= $_;
95
94
317
            $buffer =~ s/\s+/ /g;
96
94
92
            $buffer = substr $buffer, -100, 100;
97        }
98
2
7
        close $file_fh;
99    }
100}
101
102sub addToHashesNeededForFiles {
103
8
9
    my ($file, $line, $column, $message, $hashes_needed_for_files_ref) = @_;
104
8
22
    my $hashed_message = Digest::SHA::sha1_base64($message);
105
8
10
    $hashes_needed_for_files_ref->{$file} = () unless defined $hashes_needed_for_files_ref->{$file};
106
8
17
    $hashes_needed_for_files_ref->{$file}{$line} = () unless defined $hashes_needed_for_files_ref->{$file}{$line};
107
8
14
    $hashes_needed_for_files_ref->{$file}{$line}{$hashed_message} = () unless defined $hashes_needed_for_files_ref->{$file}{$line}{$hashed_message};
108
8
16
    $hashes_needed_for_files_ref->{$file}{$line}{$hashed_message}{$column} = '1';
109}
110
111sub parse_warnings {
112
1
1
    my ($warnings) = @_;
113
1
1
    our $flatten;
114
1
0
    our %directoryToRepo;
115
1
0
    our $provenanceInsertion;
116
1
1
    our %provenanceStringToIndex;
117
1
0
    our %directoryToProvenanceInsertion;
118
1
1
    my @results;
119
1
10
    unless (open WARNINGS, '<', $warnings) {
120
0
0
        print STDERR "Could not open $warnings\n";
121
0
0
        return [];
122    }
123
1
2
    my $rules = ();
124
1
1
    my %encoded_files = ();
125
1
1
    my %hashes_needed_for_files = ();
126
1
8
    while (<WARNINGS>) {
127
9
11
        next if m{^https://};
128
8
25
        next unless m{^(.+):(\d+):(\d+) \.\.\. (\d+),\s(Error|Warning|Notice)\s-\s(.+\s\((.+)\))$};
129
7
21
        my ($file, $line, $column, $endColumn, $severity, $message, $code) = ($1, $2, $3, $4, $5, $6, $7);
130
7
83
        my $directory = dirname($file);
131
7
6
        unless (defined $directoryToProvenanceInsertion{$directory}) {
132
2
2
            my $provenanceString = collectVersionControlProvenance($file);
133
2
140
            if (defined $provenanceStringToIndex{$provenanceString}) {
134
0
0
                $directoryToProvenanceInsertion{$directory} = $provenanceStringToIndex{$provenanceString};
135            } else {
136
2
4
                $provenanceStringToIndex{$provenanceString} = $provenanceInsertion;
137
2
2
                $directoryToProvenanceInsertion{$directory} = $provenanceInsertion;
138
2
2
                ++$provenanceInsertion;
139            }
140        }
141        # single-slash-escape `"` and `\`
142
7
5
        $message =~ s/(["\\])/\\$1/g;
143
7
7
        $message = encode_low_ascii $message;
144        # double-slash-escape `"`, `(`, `)`, `]`
145
7
6
        $message = double_slash_escape $message;
146        # encode `message` and `file` to protect against low ascii`
147
7
5
        my $encoded_file = url_encode $file;
148
7
4
        $encoded_files{$encoded_file} = $file;
149        # hack to make the first `...` identifier a link (that goes nowhere, but is probably blue and underlined) in GitHub's SARIF view
150
7
9
        if ($message =~ /(`{2,})/) {
151
1
17
            my $backticks = $1;
152
1
21
            while ($message =~ /($backticks`+)(?=[`].*?\g{-1})/gs) {
153
0
0
                $backticks = $1 if length($1) > length($backticks);
154            }
155
1
16
            $message =~ s/(^|[^\\])$backticks(.+?)$backticks/${1}[${2}](#security-tab)/;
156        } else {
157
6
17
            $message =~ s/(^|[^\\])\`([^`]+[^`\\])\`/${1}[${2}](#security-tab)/;
158        }
159        # replace '`' with `\`+`'` because GitHub's SARIF parser doesn't like them
160
7
4
        $message =~ s/\`/'/g;
161
7
7
        unless (defined $rules->{$code}) {
162
2
5
            $rules->{$code} = {};
163        }
164
7
5
        my $rule = $rules->{$code};
165
7
5
        unless (defined $rule->{$message}) {
166
4
10
            $rule->{$message} = [];
167        }
168
7
7
        addToHashesNeededForFiles($file, $line, $column, $message, \%hashes_needed_for_files);
169
7
5
        my $locations = $rule->{$message};
170
7
13
        my $physicalLocation = {
171            'uri' => $encoded_file,
172            'startLine' => $line,
173            'startColumn' => $column,
174            'endColumn' => $endColumn,
175        };
176
7
2
        push @$locations, $physicalLocation;
177
7
19
        $rule->{$message} = $locations;
178    }
179
1
1
    my %line_hashes = ();
180
1
0
    my %used_hashes = ();
181
1
2
    hashFiles(\%hashes_needed_for_files, \%line_hashes, \%directoryToRepo, \%used_hashes);
182
1
1
1
2
    for my $code (sort keys %{$rules}) {
183
2
1
        my $rule = $rules->{$code};
184
2
2
1
2
        for my $message (sort keys %{$rule}) {
185
4
7
            my $hashed_message = Digest::SHA::sha1_base64($message);
186
4
2
            my $locations = $rule->{$message};
187
4
4
            my $fingerprintResults = fingerprintLocations($locations, \%encoded_files, \%line_hashes, \%hashes_needed_for_files, $message, $hashed_message);
188
4
4
3
1
            my @locations_json = @{$fingerprintResults->{locations_json}};
189
4
4
3
2
            my @fingerprints = @{$fingerprintResults->{fingerprints}};
190
4
3
            if ($flatten) {
191
0
0
                my $locations_json_flat = join ',', @locations_json;
192
0
0
                my $partialFingerprints;
193
0
0
                my $partialFingerprint = (sort @fingerprints)[0];
194
0
0
                if ($partialFingerprint ne '') {
195
0
0
                    $partialFingerprints = qq<"partialFingerprints": { "cs0" : "$partialFingerprint" },>;
196                }
197
0
0
                my $result_json = qq<{"ruleId": "$code", $partialFingerprints "message": { "text": "$message" }, "locations": [ $locations_json_flat ] }>;
198
0
0
                my $result = decode_json $result_json;
199
0
0
                push @results, $result;
200            } else {
201
4
2
                my $limit = scalar @locations_json;
202
4
4
                for (my $i = 0; $i < $limit; ++$i) {
203
7
4
                    my $locations_json_flat = $locations_json[$i];
204
7
4
                    my $partialFingerprints = '';
205
7
3
                    my $partialFingerprint = $fingerprints[$i];
206
7
5
                    if ($partialFingerprint ne '') {
207
4
2
                        $partialFingerprints = qq<"partialFingerprints": { "cs0" : "$partialFingerprint" },>;
208                    }
209
7
7
                    my $result_json = qq<{"ruleId": "$code", $partialFingerprints "message": { "text": "$message" }, "locations": [ $locations_json_flat ] }>;
210
7
6
                    my $result = decode_json $result_json;
211
7
7797
                    push @results, $result;
212                }
213            }
214        }
215    }
216
1
4
    close WARNINGS;
217
1
8
    return \@results;
218}
219
220sub get_runs_from_sarif {
221
2
4
    my ($sarif_json) = @_;
222
2
0
    my %runs_view;
223
2
3
    return %runs_view unless $sarif_json->{'runs'};
224
2
2
1
2
    my @sarif_json_runs=@{$sarif_json->{'runs'}};
225
2
5
    foreach my $sarif_json_run (@sarif_json_runs) {
226
2
2
1
3
        my %sarif_json_run_hash=%{$sarif_json_run};
227
2
3
        next unless defined $sarif_json_run_hash{'tool'};
228
229
2
2
0
3
        my %sarif_json_run_tool_hash = %{$sarif_json_run_hash{'tool'}};
230
2
2
        next unless defined $sarif_json_run_tool_hash{'driver'};
231
232
2
2
0
4
        my %sarif_json_run_tool_driver_hash = %{$sarif_json_run_tool_hash{'driver'}};
233        next unless defined $sarif_json_run_tool_driver_hash{'name'} &&
234
2
7
            defined $sarif_json_run_tool_driver_hash{'rules'};
235
236
2
0
        my $driver_name = $sarif_json_run_tool_driver_hash{'name'};
237
2
2
1
3
        my @sarif_json_run_tool_driver_rules = @{$sarif_json_run_tool_driver_hash{'rules'}};
238
2
1
        my %driver_view;
239
2
2
        for my $driver_rule (@sarif_json_run_tool_driver_rules) {
240
38
17
            next unless defined $driver_rule->{'id'};
241
38
39
            $driver_view{$driver_rule->{'id'}} = $driver_rule;
242        }
243
2
3
        $runs_view{$sarif_json_run_tool_driver_hash{'name'}} = \%driver_view;
244    }
245
2
3
    return %runs_view;
246}
247
248sub collectVersionControlProvenance {
249
2
2
    my ($file) = @_;
250
2
6
    my ($parsed_file, $git_base_dir, $prefix, $remote_url, $rev, $branch) = CheckSpelling::GitSources::git_source_and_rev($file);
251
2
3
    my $base = substr $parsed_file, 0, length($file);
252
2
4
    my $provenance = [$remote_url, $rev, $branch, $git_base_dir];
253
2
11
    return JSON::PP::encode_json($provenance);
254}
255
256sub generateVersionControlProvenance {
257
1
1
    my ($versionControlProvenanceList, $run) = @_;
258
1
0
    my %provenance;
259    sub buildVersionControlProvenance {
260
1
0
        my $d = $_;
261
1
1
1
1
        my ($remote_url, $rev, $branch, $git_base_dir) = @{JSON::PP::decode_json($d)};
262
1
301
        my $dir = $git_base_dir eq '.' ? '%SRCROOT%' : "DIR_$provenanceStringToIndex{$d}";
263
1
1
        my $mappedTo = {
264            "uriBaseId" => $dir
265        };
266
1
2
        my $versionControlProvenance = {
267            "mappedTo" => $mappedTo
268        };
269
1
1
        $versionControlProvenance->{"revisionId"} = $rev if defined $rev;
270
1
2
        $versionControlProvenance->{"branch"} = $branch if defined $branch;
271
1
1
        $versionControlProvenance->{"repositoryUri"} = $remote_url if defined $remote_url;
272
1
3
        return $versionControlProvenance;
273    }
274
1
2
    @provenanceList = map(buildVersionControlProvenance,@$versionControlProvenanceList);
275
1
2
    $run->{"versionControlProvenance"} = \@provenanceList;
276}
277
278my $provenanceInsertion = 0;
279my %provenanceStringToIndex = ();
280my %directoryToProvenanceInsertion = ();
281
282sub main {
283
1
17578
    my ($sarif_template_file, $sarif_template_overlay_file, $category) = @_;
284
1
5
    unless (-f $sarif_template_file) {
285
0
0
        warn "Could not find sarif template";
286
0
0
        return '';
287    }
288
289
1
1
    $ENV{GITHUB_SERVER_URL} = '' unless defined $ENV{GITHUB_SERVER_URL};
290
1
2
    $ENV{GITHUB_REPOSITORY} = '' unless defined $ENV{GITHUB_REPOSITORY};
291
1
3
    my $sarif_template = CheckSpelling::Util::read_file $sarif_template_file;
292
1
1
    die "sarif template is empty" unless $sarif_template;
293
294
1
0
7
0
    my $json = JSON::PP->new->utf8->pretty->sort_by(sub { $JSON::PP::a cmp $JSON::PP::b });
295
1
79
    my $sarif_json = $json->decode($sarif_template);
296
297
1
103451
    if (defined $sarif_template_overlay_file && -s $sarif_template_overlay_file) {
298
1
9
        my $merger = Hash::Merge->new();
299
1
86
        my $merge_behaviors = $merger->{'behaviors'}->{$merger->get_behavior()};
300
1
6
        my $merge_arrays = $merge_behaviors->{'ARRAY'}->{'ARRAY'};
301
302        $merge_behaviors->{'ARRAY'}->{'ARRAY'} = sub {
303
36
4085
            return $merge_arrays->(@_) if ref($_[0][0]).ref($_[1][0]);
304
36
36
9
41
            return [@{$_[1]}];
305
1
1
        };
306
307
1
1
        my $sarif_template_overlay = CheckSpelling::Util::read_file $sarif_template_overlay_file;
308
1
1
        my %runs_base = get_runs_from_sarif($sarif_json);
309
310
1
1
        my $sarif_template_hash = $json->decode($sarif_template_overlay);
311
1
1786
        my %runs_overlay = get_runs_from_sarif($sarif_template_hash);
312
1
12
        for my $run_id (keys %runs_overlay) {
313
1
2
            if (defined $runs_base{$run_id}) {
314
1
0
                my $run_base_hash = $runs_base{$run_id};
315
1
1
                my $run_overlay_hash = $runs_overlay{$run_id};
316
1
1
                for my $overlay_id (keys %$run_overlay_hash) {
317                    $run_base_hash->{$overlay_id} = $merger->merge(
318                        $run_overlay_hash->{$overlay_id},
319
1
2
                        $run_base_hash->{$overlay_id}
320                    );
321                }
322            } else {
323
0
0
                $runs_base{$run_id} = $runs_overlay{$run_id};
324            }
325        }
326        #$sarif_json->
327
1
1
53
1
        my @sarif_json_runs = @{$sarif_json->{'runs'}};
328
1
1
        foreach my $sarif_json_run (@sarif_json_runs) {
329
1
1
0
1
            my %sarif_json_run_hash=%{$sarif_json_run};
330
1
1
            next unless defined $sarif_json_run_hash{'tool'};
331
332
1
1
1
1
            my %sarif_json_run_tool_hash = %{$sarif_json_run_hash{'tool'}};
333
1
1
            next unless defined $sarif_json_run_tool_hash{'driver'};
334
335
1
1
0
2
            my %sarif_json_run_tool_driver_hash = %{$sarif_json_run_tool_hash{'driver'}};
336
1
1
            my $driver_name = $sarif_json_run_tool_driver_hash{'name'};
337            next unless defined $driver_name &&
338
1
5
                defined $sarif_json_run_tool_driver_hash{'rules'};
339
340
1
1
            my $driver_view_hash = $runs_base{$driver_name};
341
1
0
            next unless defined $driver_view_hash;
342
343
1
1
1
5
            my @sarif_json_run_tool_driver_rules = @{$sarif_json_run_tool_driver_hash{'rules'}};
344
1
2
            for my $driver_rule_number (0 .. scalar @sarif_json_run_tool_driver_rules) {
345
38
2485
                my $driver_rule = $sarif_json_run_tool_driver_rules[$driver_rule_number];
346
38
18
                my $driver_rule_id = $driver_rule->{'id'};
347                next unless defined $driver_rule_id &&
348
38
49
                    defined $driver_view_hash->{$driver_rule_id};
349
37
25
                $sarif_json_run_tool_driver_hash{'rules'}[$driver_rule_number] = $merger->merge($driver_view_hash->{$driver_rule_id}, $driver_rule);
350            }
351        }
352
1
2
        delete $sarif_template_hash->{'runs'};
353
1
1
        $sarif_json = $merger->merge($sarif_json, $sarif_template_hash);
354    }
355    {
356
1
1
1
532
1
1
        my @sarif_json_runs = @{$sarif_json->{'runs'}};
357
1
1
        foreach my $sarif_json_run (@sarif_json_runs) {
358
1
0
            my %sarif_json_run_automationDetails;
359
1
1
            $sarif_json_run_automationDetails{id} = $category;
360
1
1
            $sarif_json_run->{'automationDetails'} = \%sarif_json_run_automationDetails;
361        }
362    }
363
364
1
1
0
2
    my %sarif = %{$sarif_json};
365
366
1
2
    $sarif{'runs'}[0]{'tool'}{'driver'}{'version'} = $ENV{CHECK_SPELLING_VERSION};
367
368
1
38
    my $results = parse_warnings $ENV{warning_output};
369
1
2
    if ($results) {
370
1
1
        $sarif{'runs'}[0]{'results'} = $results;
371
1
0
        our %provenanceStringToIndex;
372
1
1
        my @provenanceList = keys %provenanceStringToIndex;
373
1
3
        generateVersionControlProvenance(\@provenanceList, $sarif{'runs'}[0]);
374
1
1
        my %codes;
375
1
1
        for my $result_ref (@$results) {
376
7
7
3
6
            my %result = %{$result_ref};
377
7
7
            $codes{$result{'ruleId'}} = 1;
378        }
379
1
1
        my $rules_ref = $sarif{'runs'}[0]{'tool'}{'driver'}{'rules'};
380
1
1
0
5
        my @rules = @{$rules_ref};
381
1
0
        my $missing_rule_definition_id = 'missing-rule-definition';
382
1
37
1
21
        my ($missing_rule_definition_ref) = grep { $_->{'id'} eq $missing_rule_definition_id } @rules;
383
1
37
1
19
        @rules = grep { defined $codes{$_->{'id'}} } @rules;
384
1
1
        my $code_index = 0;
385
1
1
0
2
        my %defined_codes = map { $_->{'id'} => $code_index++ } @rules;
386
1
2
1
1
        my @missing_codes = grep { !defined $defined_codes{$_}} keys %codes;
387
1
1
        my $missing_rule_definition_index;
388
1
1
        if (@missing_codes) {
389
1
0
            push @rules, $missing_rule_definition_ref;
390
1
1
            $missing_rule_definition_index = $defined_codes{$missing_rule_definition_id} = $code_index++;
391
1
29
            my $spellchecker = $ENV{spellchecker} || dirname(dirname(dirname(__FILE__)));
392
1
1
            my %hashes_needed_for_files = ();
393
1
1
            my %line_hashes = ();
394
1
0
            my %used_hashes = ();
395
1
1
            our %directoryToRepo;
396
1
0
            for my $missing_code (@missing_codes) {
397
1
1
                my $message = "No rule definition for `$missing_code`";
398
1
154241
                my $code_locations = `find '$spellchecker' -name '.git*' -prune -type f -name '*.sh' -o -name '*.pl' -o -name '*.pm' -o -name '*.t' -print0|xargs -0 grep -n '$missing_code' | perl -pe 's<^\./><>'`;
399
1
11
                my @locations;
400
1
9
                for my $line (split /\n/, $code_locations) {
401
1
3
                    chomp $line;
402
1
14
                    my ($file, $lineno, $code) = $line =~ /^(.+?):(\d+):(.+)$/;
403
1
7
                    next unless defined $file;
404
1
33
                    $code =~ /^(.*?)\b$missing_code\b/;
405
1
4
                    my $startColumn = length($1) + 1;
406
1
38
                    my $location = {
407                        'uri' => url_encode($file),
408                        'startLine' => $lineno,
409                        'startColumn' => $startColumn,
410                        'endColumn' => length($1) + length($missing_code) + 1,
411                    };
412
1
4
                    push @locations, $location;
413
1
1
                    my $encoded_file = url_encode $file;
414
1
5
                    $encoded_files{$encoded_file} = $file;
415
1
5
                    addToHashesNeededForFiles($file, $lineno, $startColumn, $message, \%hashes_needed_for_files);
416                }
417
1
4
                hashFiles(\%hashes_needed_for_files, \%line_hashes, \%directoryToRepo, \%used_hashes);
418
1
3
                my $fingerprintResults = fingerprintLocations(\@locations, \%encoded_files, \%encoded_files, \%line_hashes, $message, Digest::SHA::sha1_base64($message));
419
1
1
1
1
                my @locations_json = @{$fingerprintResults->{locations_json}};
420
1
1
1
1
                my @fingerprints = @{$fingerprintResults->{fingerprints}};
421
1
3
                my $locations_json_flat = join ',', @locations_json;
422
1
1
                my $partialFingerprints = '';
423
1
2
                my $locations = $locations_json_flat ? qq<, "locations": [ $locations_json_flat ]> : '';
424
1
1
                my $result_json = qq<{"ruleId": "$missing_rule_definition_id", $partialFingerprints "message": { "text": "$message" }$locations }>;
425
1
6
                my $result = decode_json $result_json;
426
1
1
989
6
                push @{$results}, $result;
427            }
428        }
429
1
3
        $sarif{'runs'}[0]{'tool'}{'driver'}{'rules'} = \@rules;
430
1
1
3
3
        for my $result_index (0 .. scalar @{$results}) {
431
9
4
            my $result = $results->[$result_index];
432
9
7
            my $ruleId = $result->{'ruleId'};
433
9
14
            next if defined $ruleId && defined $defined_codes{$ruleId};
434
2
134
            $result->{'ruleId'} = $missing_rule_definition_id;
435        }
436    }
437
438
1
2
    return encode_json \%sarif;
439}
440
4411;