File Coverage

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

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