Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support SARIF output format #28

Merged
merged 10 commits into from
Nov 17, 2023
2 changes: 1 addition & 1 deletion .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
critic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Run Perl::Critic
uses: natanlao/perl-critic-action@v1.1
with:
Expand Down
121 changes: 39 additions & 82 deletions lib/Zarn/AST.pm
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,50 @@ package Zarn::AST {
use Getopt::Long;
use PPI::Find;
use PPI::Document;
use JSON::PP;
use Zarn::Sarif;
use JSON;

sub new {
my ($class, $parameters) = @_;
my ($file, $rules, $sarif_output);
my ($file, $rules, $sarif);

Getopt::Long::GetOptionsFromArray (
$parameters,
"file=s" => \$file,
"rules=s" => \$rules,
"sarif=s" => \$sarif_output
"sarif=s" => \$sarif
);

my $self = {
file => $file,
rules => $rules,
sarif_output => $sarif_output,
subset => []
file => $file,
rules => $rules,
sarif => $sarif,
document => undef,
sarif_report => undef
};

bless $self, $class;

if ($file && $rules) {
my $document = PPI::Document -> new($file);
$self -> {document} = PPI::Document->new($file);
$self -> {document} -> prune("PPI::Token::Pod");
$self -> {document} -> prune("PPI::Token::Comment");

$document -> prune("PPI::Token::Pod");
$document -> prune("PPI::Token::Comment");
$self -> {sarif_report} = Zarn::Sarif -> new() if $sarif;

foreach my $token (@{$document -> find("PPI::Token")}) {
foreach my $rule (@{$rules}) {
my @sample = $rule -> {sample} -> @*;
foreach my $token (@{$self -> {document}->find("PPI::Token")}) {
foreach my $rule (@{$self -> {rules}}) {
my @sample = $rule -> {sample}->@*;
my $category = $rule -> {category};
my $title = $rule -> {name};

if ($self -> matches_sample($token -> content(), \@sample)) {
$self -> process_sample_match($document, $category, $file, $title, $token);
$self -> process_sample_match($category, $title, $token);
}
}
}
}

if ($sarif_output) {
$self -> generate_sarif()
}

return 1;
}

Expand All @@ -61,84 +61,41 @@ package Zarn::AST {
}

sub process_sample_match {
my ($self, $document, $category, $file, $title, $token) = @_;
my ($self, $category, $title, $token) = @_;

my $next_element = $token -> snext_sibling;
my $next_element = $token->snext_sibling;

# this is a draft source-to-sink function
if (defined $next_element && ref $next_element && $next_element -> content() =~ /[\$\@\%](\w+)/) {
if (defined $next_element && ref $next_element && $next_element->content() =~ /[\$\@\%](\w+)/) {
# perform taint analysis
$self -> perform_taint_analysis($document, $category, $file, $title, $next_element);

$self->perform_taint_analysis($category, $title, $next_element);
}
}

sub perform_taint_analysis {
my ($self, $document, $category, $file, $title, $next_element) = @_;
my ($self, $category, $title, $next_element) = @_;

my $var_token = $document -> find_first(
sub { $_[1] -> isa("PPI::Token::Symbol") and $_[1] -> content eq "\$$1" }
my $var_token = $self -> {document} -> find_first(
sub { $_[1] -> isa("PPI::Token::Symbol") and $_[1]->content eq "\$$1" }
);

if ($var_token && $var_token -> can("parent")) {
if (($var_token->parent -> isa("PPI::Token::Operator") || $var_token -> parent -> isa("PPI::Statement::Expression"))) {
my ($line, $rowchar) = @{$var_token -> location};
print "[$category] - FILE:$file \t Potential: $title. \t Line: $line:$rowchar.\n";

# collect the subset to generate SARIF output
my $info = {
category => $category,
title => $title,
file => $file,
line => $line,
row => $rowchar
};
push @{$self->{subset}}, $info;

}
}
}

sub generate_sarif {
my ($self) = @_;
my $output_file = $self -> {sarif_output};
my $sarif_data = {
"\$schema" => "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
version => "2.1.0",
runs => [{
tool => {
driver => {
name => "ZARN",
version => "0.0.5"
}
},
results => []
}]
};

foreach my $info (@{$self -> {subset}}) {
my $result = {
message => {
text => $info -> {title}
},
locations => [{
physicalLocation => {
artifactLocation => {
uri => $info -> {file}
},
region => {
startLine => $info -> {line},
endLine => $info -> {row}
}
if (($var_token -> parent -> isa("PPI::Token::Operator") || $var_token -> parent -> isa("PPI::Statement::Expression"))) {
my ($line, $rowchar) = @{ $var_token -> location };
print "[$category] - FILE:" . $self -> {file} . "\t Potential: $title. \t Line: $line:$rowchar.\n";

if ($self->{sarif}) {
$self->{sarif_report}->add_vulnerability(0, $title, $self->{file}, $line);
my $sarif_output = encode_json($self->{sarif_report}->prepare_for_json());

if ($self->{sarif} ne '') {
open my $fh, '>', $self->{sarif} or die "Cannot open file $self->{sarif}: $!";
print $fh $sarif_output;
close $fh;
}
}]
};
push @{$sarif_data -> {runs}[0]{results}}, $result;
}
}
}

open(my $fh, '>', $output_file) or die "Cannot open file '$output_file': $!";
print $fh encode_json($sarif_data);
close($fh);
}
}

Expand Down
63 changes: 63 additions & 0 deletions lib/Zarn/Sarif.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package Zarn::Sarif {
use strict;
use warnings;
use JSON;

sub new {
my ($class) = @_;
my $self = {
"version" => "2.1.0",
"runs" => []
};
bless $self, $class;
return $self;
}

sub add_run {
my ($self, $tool_name, $tool_info_uri) = @_;
push @{$self->{runs}}, {
"tool" => {
"driver" => {
"name" => $tool_name,
"informationUri" => $tool_info_uri
}
},
"results" => []
};
}

sub add_vulnerability {
my ($self, $run_index, $vulnerability_title, $file_uri, $line) = @_;
my $result = _create_result($vulnerability_title, $file_uri, $line);
push @{$self -> {runs} -> [$run_index] -> {results}}, $result;
}

sub prepare_for_json {
my ($self) = @_;
return {%$self};
}

sub _create_result {
my ($vulnerability_title, $file_uri, $line) = @_;
return {
"ruleId" => $vulnerability_title,
"message" => {
"text" => "Vulnerability found: $vulnerability_title"
},
"locations" => [
{
"physicalLocation" => {
"artifactLocation" => {
"uri" => $file_uri
},
"region" => {
"startLine" => $line
}
}
}
]
};
}

1;
}
28 changes: 14 additions & 14 deletions zarn.pl
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@

sub main {
my $rules = "rules/default.yml";
my ($source, $ignore, $sarif_output);
my $sarif = 0;

my ($source, $ignore);

Getopt::Long::GetOptions (
"r|rules=s" => \$rules,
"s|source=s" => \$source,
"i|ignore=s" => \$ignore,
"srf|sarif=s" => \$sarif_output
"r|rules=s" => \$rules,
"s|source=s" => \$source,
"i|ignore=s" => \$ignore,
"srf|sarif=s" => \$sarif
);

if (!$source) {
Expand All @@ -30,25 +32,23 @@ sub main {
\r\t-s, --source Configure a source directory to do static analysis
\r\t-r, --rules Define YAML file with rules
\r\t-i, --ignore Define a file or directory to ignore
\r\t-srf, --sarif Define the SARIF output file
\r\t-h, --help To see help menu of a module\n
\r\t-h, --help To see help menu of a module
\r\t-srf, --sarif Get output in SARIF format\n
";

exit 1;
}

my @rules = Zarn::Rules -> new($rules);
my @files = Zarn::Files -> new($source, $ignore);
my $sarif = $sarif_output;

foreach my $file (@files) {
if (@rules) {
if ($sarif) {
my $analysis = Zarn::AST -> new(["--file" => $file, "--rules" => @rules, "--sarif" => $sarif_output]);
}
else {
my $analysis = Zarn::AST -> new(["--file" => $file, "--rules" => @rules]);
}
my $analysis = Zarn::AST -> new ([
"--file" => $file,
"--rules" => @rules,
"--sarif" => $sarif
]);
}
}
}
Expand Down
Loading