diff --git a/.perlcriticrc b/.perlcriticrc index 61ab588..a25c6d3 100644 --- a/.perlcriticrc +++ b/.perlcriticrc @@ -1,4 +1,5 @@ -severity = 3 +severity = 1 [-TestingAndDebugging::RequireUseStrict] -[-TestingAndDebugging::RequireUseWarnings] \ No newline at end of file +[-TestingAndDebugging::RequireUseWarnings] +[-CodeLayout::RequireTidyCode] \ No newline at end of file diff --git a/.perltidyrc b/.perltidyrc index f8a793c..6817d69 100644 --- a/.perltidyrc +++ b/.perltidyrc @@ -3,4 +3,8 @@ --continuation-indentation=4 --square-bracket-tightness=2 --tight-secret-operators ---maximum-consecutive-blank-lines=1 \ No newline at end of file +--maximum-consecutive-blank-lines=1 +--nocuddled-else +--nooutdent-long-quotes + +# perltidy -nst -b filename.pl \ No newline at end of file diff --git a/cpanfile b/cpanfile index 1cad3e8..8d2e1a5 100644 --- a/cpanfile +++ b/cpanfile @@ -2,4 +2,5 @@ requires "JSON"; requires "File::Find::Rule", "0.34"; requires "Getopt::Long", "2.54"; requires "YAML::Tiny", "1.73"; -requires "PPI::Document"; \ No newline at end of file +requires "PPI::Document"; +requires "List::Util"; \ No newline at end of file diff --git a/lib/Zarn/Engine/AST.pm b/lib/Zarn/Engine/AST.pm index 8e04e5f..75b3a06 100644 --- a/lib/Zarn/Engine/AST.pm +++ b/lib/Zarn/Engine/AST.pm @@ -4,7 +4,7 @@ package Zarn::Engine::AST { use PPI::Find; use Getopt::Long; use PPI::Document; - + our $VERSION = '0.0.6'; sub new { @@ -13,14 +13,14 @@ package Zarn::Engine::AST { Getopt::Long::GetOptionsFromArray ( $parameters, - "file=s" => \$file + 'file=s' => \$file ); if ($file) { my $document = PPI::Document -> new($file); - $document -> prune("PPI::Token::Pod"); - $document -> prune("PPI::Token::Comment"); + $document -> prune('PPI::Token::Pod'); + $document -> prune('PPI::Token::Comment'); return $document; } diff --git a/lib/Zarn/Engine/Source_to_Sink.pm b/lib/Zarn/Engine/Source_to_Sink.pm index cbdde68..c4d07c9 100644 --- a/lib/Zarn/Engine/Source_to_Sink.pm +++ b/lib/Zarn/Engine/Source_to_Sink.pm @@ -3,6 +3,7 @@ package Zarn::Engine::Source_to_Sink { use warnings; use PPI::Find; use Getopt::Long; + use List::Util 'any'; use PPI::Document; use Zarn::Engine::Taint_Analysis; @@ -14,26 +15,26 @@ package Zarn::Engine::Source_to_Sink { Getopt::Long::GetOptionsFromArray ( $parameters, - "ast=s" => \$ast, - "rules=s" => \$rules + 'ast=s' => \$ast, + 'rules=s' => \$rules ); if ($ast && $rules) { - foreach my $token (@{$ast -> find("PPI::Token")}) { + foreach my $token (@{$ast -> find('PPI::Token')}) { foreach my $rule (@{$rules}) { my @sample = $rule -> {sample} -> @*; my $category = $rule -> {category}; my $title = $rule -> {name}; my $message = $rule -> {message}; - if (grep {my $content = $_; scalar(grep {$content =~ m/$_/xms} @sample)} $token -> content()) { + if (any { my $content = $_; scalar(any { $content =~ m/$_/xms } @sample) } $token -> content()) { 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+)/xms) { my $taint_analysis = Zarn::Engine::Taint_Analysis -> new ([ - "--ast" => $ast, - "--token" => $1, + '--ast' => $ast, + '--token' => $1, ]); if ($taint_analysis) { @@ -57,7 +58,7 @@ package Zarn::Engine::Source_to_Sink { return @results; } - + return 0; } } diff --git a/lib/Zarn/Engine/Taint_Analysis.pm b/lib/Zarn/Engine/Taint_Analysis.pm index 72276ca..2fd8971 100644 --- a/lib/Zarn/Engine/Taint_Analysis.pm +++ b/lib/Zarn/Engine/Taint_Analysis.pm @@ -4,7 +4,9 @@ package Zarn::Engine::Taint_Analysis { use PPI::Find; use Getopt::Long; use PPI::Document; - + use List::Util 'any'; + + our $VERSION = '0.0.1'; sub new { @@ -13,31 +15,31 @@ package Zarn::Engine::Taint_Analysis { Getopt::Long::GetOptionsFromArray ( $parameters, - "ast=s" => \$ast, - "token=s" => \$token + 'ast=s' => \$ast, + 'token=s' => \$token ); if ($ast && $token) { - my $var_token = $ast -> find_first ( - sub { - $_[1] -> isa("PPI::Token::Symbol") and + my $var_token = $ast -> find_first ( + sub { + $_[1] -> isa('PPI::Token::Symbol') and ($_[1] -> content eq "\$$token") # or $_[1] -> content eq "\@$1" or $_[1] -> content eq "\%$1" } ); - if ($var_token && $var_token -> can("parent")) { + if ($var_token && $var_token -> can('parent')) { my @childrens = $var_token -> parent -> children; - + # verifyng if the variable is a fixed string or a number - if (grep { - $_ -> isa("PPI::Token::Quote::Double") || - $_ -> isa("PPI::Token::Quote::Single") || - $_ -> isa("PPI::Token::Number") + if (any { + $_ -> isa('PPI::Token::Quote::Double') || + $_ -> isa('PPI::Token::Quote::Single') || + $_ -> isa('PPI::Token::Number') } @childrens) { return 0; } - if (($var_token -> parent -> isa("PPI::Token::Operator") || $var_token -> parent -> isa("PPI::Statement::Expression"))) { + if (($var_token -> parent -> isa('PPI::Token::Operator') || $var_token -> parent -> isa('PPI::Statement::Expression'))) { return $var_token -> location; } } diff --git a/lib/Zarn/Helper/Files.pm b/lib/Zarn/Helper/Files.pm index 62b0be5..36fcdeb 100644 --- a/lib/Zarn/Helper/Files.pm +++ b/lib/Zarn/Helper/Files.pm @@ -3,24 +3,33 @@ package Zarn::Helper::Files { use warnings; use File::Find::Rule; - our $VERSION = '0.0.2'; + our $VERSION = '0.0.3'; sub new { my ($self, $source, $ignore) = @_; if ($source) { my $rule = File::Find::Rule -> new(); + my $exclude_rule = $rule -> new(); - $rule -> or ( - $rule -> new -> directory -> name(".git", $ignore) -> prune -> discard, - $rule -> new - ); + $exclude_rule = $exclude_rule -> directory(); + $exclude_rule = $exclude_rule -> name('.git', $ignore); + $exclude_rule = $exclude_rule -> prune(); + $exclude_rule = $exclude_rule -> discard(); + + my $file_rule = $rule -> new(); + $rule -> or ($exclude_rule, $file_rule); $rule -> file -> nonempty; - $rule -> name("*.pm", "*.t", "*.pl"); + $rule -> name('*.pm', '*.t', '*.pl'); my @files = $rule -> in($source); + if (!@files) { + print "[!] Could not identify any files in: $source.\n"; + return 1; + } + return @files; } diff --git a/lib/Zarn/Helper/Sarif.pm b/lib/Zarn/Helper/Sarif.pm index c4f2257..aa1fda3 100644 --- a/lib/Zarn/Helper/Sarif.pm +++ b/lib/Zarn/Helper/Sarif.pm @@ -8,14 +8,14 @@ package Zarn::Helper::Sarif { my ($self, @vulnerabilities) = @_; my $sarif_data = { - "\$schema" => "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", - version => "2.1.0", + "\$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", - informationUri => "https://github.com/htrgouvea/zarn", - version => "0.1.0" + name => 'ZARN', + informationUri =>'"https://github.com/htrgouvea/zarn', + version => '0.1.0' } }, results => [] diff --git a/samples/code-injection.pl b/samples/code-injection.pl deleted file mode 100644 index 129bcf7..0000000 --- a/samples/code-injection.pl +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/perl - -use 5.018; -use strict; -use warnings; - -sub main { - my $name = $ARGV[0]; - - system ("echo Hello World! $name"); -} - -exit main(); \ No newline at end of file diff --git a/samples/false-positive.pl b/samples/false-positive.pl deleted file mode 100644 index 18eddd3..0000000 --- a/samples/false-positive.pl +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/perl - -use 5.018; -use strict; -use warnings; - -sub main { - my $name = "Zarn"; - - system ("echo Hello World! $name"); -} - -exit main(); \ No newline at end of file diff --git a/tests/Files.t b/tests/Files.t new file mode 100644 index 0000000..5a842b7 --- /dev/null +++ b/tests/Files.t @@ -0,0 +1,52 @@ +use strict; +use warnings; +use Test::More; +use File::Temp qw(tempdir); +use File::Path qw(make_path); +use File::Spec; +use File::Basename; +use File::Find; +use File::Slurp; +use Zarn::Helper::Files; + +my $temp_dir = tempdir(CLEANUP => 1); + +my @dirs = ( + File::Spec->catdir($temp_dir, 'dir1'), + File::Spec->catdir($temp_dir, 'dir2', '.git'), +); + +my @files = ( + File::Spec->catfile($temp_dir, 'dir1', 'file1.pm'), + File::Spec->catfile($temp_dir, 'dir1', 'file2.t'), + File::Spec->catfile($temp_dir, 'dir1', 'file3.pl'), + File::Spec->catfile($temp_dir, 'dir2', 'file4.pm'), + File::Spec->catfile($temp_dir, 'dir2', 'file5.txt'), + File::Spec->catfile($temp_dir, 'dir2', '.git', 'file6.pm'), +); + +foreach my $dir (@dirs) { + make_path($dir); +} + +foreach my $file (@files) { + write_file($file, "use strict;\n"); +} + +my @expected_files = ( + File::Spec->catfile($temp_dir, 'dir1', 'file1.pm'), + File::Spec->catfile($temp_dir, 'dir1', 'file2.t'), + File::Spec->catfile($temp_dir, 'dir1', 'file3.pl'), + File::Spec->catfile($temp_dir, 'dir2', 'file4.pm'), +); + +my @found_files = Zarn::Helper::Files->new($temp_dir, '.git'); +@found_files = sort @found_files; +@expected_files = sort @expected_files; + +is_deeply(\@found_files, \@expected_files, 'Perl files correctly found in the source directory'); + +my $no_source = Zarn::Helper::Files->new(); +is($no_source, 0, 'Returns 0 when no source directory is provided'); + +done_testing(); \ No newline at end of file diff --git a/tests/Rules.t b/tests/Rules.t new file mode 100644 index 0000000..fe95846 --- /dev/null +++ b/tests/Rules.t @@ -0,0 +1,29 @@ +use strict; +use warnings; +use Test::More; +use Zarn::Helper::Rules; +use File::Temp qw(tempfile); + +my $yaml_content = <<'END_YAML'; +--- +rules: + - rule1 + - rule2 + - rule3 +END_YAML + + +my ($fh, $filename) = tempfile(); +print $fh $yaml_content; +close $fh; + +my @expected_rules = ('rule1', 'rule2', 'rule3'); +my @rules = Zarn::Helper::Rules->new($filename); + +my @flattened_rules = map { @$_ } @rules; +is_deeply(\@flattened_rules, \@expected_rules, 'Rules correctly loaded from YAML file'); + +my $no_rules = Zarn::Helper::Rules->new(); +is($no_rules, 0, 'Returns 0 when no rules file is provided'); + +done_testing(); \ No newline at end of file diff --git a/tools/graph.pl b/tools/graph.pl deleted file mode 100644 index 57aa019..0000000 --- a/tools/graph.pl +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env perl - -use 5.018; -use strict; -use warnings; -use Devel::Graph; - -sub main { - my $file = $ARGV[0]; - - if ($file) { - my $grapher = Devel::Graph -> new(); - my $decompose = $grapher -> decompose ($file); - - print $decompose -> as_ascii(); - } - - return 0; -} - -exit main(); \ No newline at end of file diff --git a/tools/view-ast.pl b/tools/view-ast.pl deleted file mode 100644 index 44efe68..0000000 --- a/tools/view-ast.pl +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env perl - -use 5.030; -use strict; -use warnings; -use PPI; -use Data::Dumper; - -sub main { - my $file = $ARGV[0]; - - if ($file) { - my $document = PPI::Document -> new($file); - print Dumper($document); - } -} - -main(); \ No newline at end of file diff --git a/zarn.pl b/zarn.pl index 0dbe00c..4ea7a6f 100755 --- a/zarn.pl +++ b/zarn.pl @@ -5,7 +5,7 @@ use warnings; use Carp; use JSON; -use lib "./lib/"; +use lib './lib/'; use Getopt::Long; use Zarn::Engine::AST; use Zarn::Helper::Files; @@ -16,45 +16,47 @@ our $VERSION = '0.1.0'; sub main { - my $rules = "rules/default.yml"; - my ($source, $ignore, $sarif, @results); - - Getopt::Long::GetOptions ( - "r|rules=s" => \$rules, - "s|source=s" => \$source, - "i|ignore=s" => \$ignore, - "srf|sarif=s" => \$sarif + my $rules = 'rules/default.yml'; + my ( $source, $ignore, $sarif, @results ); + + Getopt::Long::GetOptions( + 'r|rules=s' => \$rules, + 's|source=s' => \$source, + 'i|ignore=s' => \$ignore, + 'srf|sarif=s' => \$sarif ); - if (!$source) { + if ( !$source ) { print "\nZarn v0.1.0" - . "\nCore Commands" - . "\n==============\n" - . "\tCommand Description\n" - . "\t------- -----------\n" - . "\t-s, --source Configure a source directory to do static analysis\n" - . "\t-r, --rules Define YAML file with rules\n" - . "\t-i, --ignore Define a file or directory to ignore\n" - . "\t-srf, --sarif Define the SARIF output file\n" - . "\t-h, --help To see help menu of a module\n\n"; + . "\nCore Commands" + . "\n==============\n" + . "\tCommand Description\n" + . "\t------- -----------\n" + . "\t-s, --source Configure a source directory to do static analysis\n" + . "\t-r, --rules Define YAML file with rules\n" + . "\t-i, --ignore Define a file or directory to ignore\n" + . "\t-srf, --sarif Define the SARIF output file\n" + . "\t-h, --help To see help menu of a module\n\n"; return 0; } - my @rules = Zarn::Helper::Rules -> new($rules); - my @files = Zarn::Helper::Files -> new($source, $ignore); + my @rules = Zarn::Helper::Rules->new($rules); + my @files = Zarn::Helper::Files->new($source, $ignore); foreach my $file (@files) { if (@rules) { - my $ast = Zarn::Engine::AST -> new (["--file" => $file]); + my $ast = Zarn::Engine::AST->new( ['--file' => $file] ); - my @analysis = Zarn::Engine::Source_to_Sink -> new ([ - "--ast" => $ast, - "--rules" => @rules - ]); + my @analysis = Zarn::Engine::Source_to_Sink->new( + [ + '--ast' => $ast, + '--rules' => @rules + ] + ); if (@analysis) { - $analysis[0] -> {'file'} = $file; + $analysis[0]->{'file'} = $file; } push @results, @analysis; @@ -62,28 +64,27 @@ sub main { } foreach my $result (@results) { - my $category = $result -> {category}; - my $file = $result -> {file}; - my $title = $result -> {title}; - my $line_sink = $result -> {line_sink}; - my $rowchar_sink = $result -> {rowchar_sink}; - my $line_source = $result -> {line_source}; - my $rowchar_source = $result -> {rowchar_source}; - - print "[$category] - FILE:$file \t Potential: $title. \t Dangerous function on line: $line_sink:$rowchar_sink \t Data point possibility controlled: $line_source:$rowchar_source\n"; + my $category = $result->{category}; + my $file = $result->{file}; + my $title = $result->{title}; + my $line_sink = $result->{line_sink}; + my $rowchar_sink = $result->{rowchar_sink}; + my $line_source = $result->{line_source}; + my $rowchar_source = $result->{rowchar_source}; + + print + "[$category] - FILE:$file \t Potential: $title. \t Dangerous function on line: $line_sink:$rowchar_sink \t Data point possibility controlled: $line_source:$rowchar_source\n"; } if ($sarif) { - my $sarif_data = Zarn::Helper::Sarif -> new (@results); + my $sarif_data = Zarn::Helper::Sarif->new(@results); - open(my $output, '>', $sarif) or croak "Cannot open file '$sarif': $!"; - + open( my $output, '>', $sarif ) or croak "Cannot open the $sarif file\n"; print $output encode_json($sarif_data); - - close($output); + close($output) or die "Error to close the file\n"; } return 0; } -main(); +main(); \ No newline at end of file