File Coverage

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

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