diff --git a/lib/SecurityGate/Engine/Secrets.pm b/lib/SecurityGate/Engine/Secrets.pm index 77703ab..deb44dd 100644 --- a/lib/SecurityGate/Engine/Secrets.pm +++ b/lib/SecurityGate/Engine/Secrets.pm @@ -5,61 +5,62 @@ package SecurityGate::Engine::Secrets { use Mojo::JSON; sub new { - my ($class, $token, $repository) = @_; + my ($class, $token, $repository, $severity_limits) = @_; - my $endpoint = "https://api.github.com/repos/$repository/secret-scanning/alerts"; - my $userAgent = Mojo::UserAgent -> new(); - my $request = $userAgent -> get($endpoint, {Authorization => "Bearer $token"}) -> result(); + my $endpoint = "https://api.github.com/repos/$repository/secret-scanning/alerts"; + my $userAgent = Mojo::UserAgent -> new(); + my $request = $userAgent -> get($endpoint, {Authorization => "Bearer $token"}) -> result(); - if ($request -> code() == 200) { - my $data = $request -> json(); - my $open_alerts = 0; - my @alert_details; + if ($request -> code() == 200) { + my $data = $request -> json(); + my $open_alerts = 0; + my @alert_details; - foreach my $alert (@$data) { - if ($alert -> {state} eq "open") { - $open_alerts++; + foreach my $alert (@$data) { + if ($alert -> {state} eq "open") { + $open_alerts++; - my $locations_endpoint = "https://api.github.com/repos/$repository/secret-scanning/alerts/$alert -> {number}/locations"; - my $locations_request = $userAgent -> get($locations_endpoint, {Authorization => "Bearer $token"}) -> result(); + my $locations_endpoint = "https://api.github.com/repos/$repository/secret-scanning/alerts/$alert -> {number}/locations"; + my $locations_request = $userAgent -> get($locations_endpoint, {Authorization => "Bearer $token"}) -> result(); - if ($locations_request -> code() == 200) { - my $locations = $locations_request -> json(); - - push @alert_details, { - alert_number => $alert -> {number}, - locations => $locations, - }; + if ($locations_request -> code() == 200) { + my $locations = $locations_request -> json(); + + push @alert_details, { + alert_number => $alert -> {number}, + locations => $locations, + }; + } + } } - } - } - if ($open_alerts > 0) { - print "[!] Total of open secret scanning alerts: $open_alerts\n"; - - foreach my $detail (@alert_details) { - print "[-] Alert $detail -> {alert_number} found in the following locations:\n"; - - foreach my $location (@{$detail -> {locations}}) { - print " File: $location -> {path}, Start line: $location -> {start_line}\n"; + print "[!] Total of open secret scanning alerts: $open_alerts\n"; + + foreach my $detail (@alert_details) { + print "[-] Alert " . $detail -> {alert_number} . " found in the following locations:\n"; + + foreach my $location (@{$detail -> {locations}}) { + print " File: " . $location -> {path} . ", Start line: " . $location -> {start_line} . "\n"; + } } - } - print "[+] Secret scanning alert(s) found. Blocking pipeline.\n"; - return 1; + my $threshold = $severity_limits -> {high}; + if ($open_alerts > $threshold) { + print "[+] More than $threshold secret scanning alerts found. Blocking pipeline.\n"; + return 1; + } + + else { + print "[-] Number of secret scanning alerts ($open_alerts) is within the acceptable limit ($threshold).\n"; + return 0; + } } else { - print "[-] No secret scanning alerts found.\n"; - return 0; + print "Error: Unable to fetch secret scanning alerts. HTTP status code: " . $request -> code() . "\n"; + return 1; } - } - - else { - print "Error: Unable to fetch secret scanning alerts. HTTP status code: " . $request -> code() . "\n"; - return 1; - } - } + } } -1; \ No newline at end of file +1; diff --git a/lib/SecurityGate/Utils/Helper.pm b/lib/SecurityGate/Utils/Helper.pm index db31acf..58c73a5 100644 --- a/lib/SecurityGate/Utils/Helper.pm +++ b/lib/SecurityGate/Utils/Helper.pm @@ -1,24 +1,27 @@ package SecurityGate::Utils::Helper { use strict; - use warnings; + use warnings; - sub new { - return " - \rSecurity Gate v0.1.0 - \rCore Commands - \r==================== - \r\tCommand Description - \r\t------- ----------- - \r\t-t, --token GitHub token - \r\t-r, --repo GitHub repository, organization/repository-name - \r\t-c, --critical Critical severity limit - \r\t-h, --high High severity limit - \r\t-m, --medium Medium severity limit - \r\t-l, --low Low severity limit - \r\t--dependency-alerts Check dependency alerts - \r\t--secret-alerts Check secret scanning alerts - \r\t--code-alerts Check code scanning alerts\n\n"; - } + sub new { + return <<"EOT"; + +Security Gate v0.0.3 +Core Commands +============== +Command Description +------- ----------- +-t, --token GitHub token +-r, --repo GitHub repository +-c, --critical Critical severity limit +-h, --high High severity limit +-m, --medium Medium severity limit +-l, --low Low severity limit +--dependency-alerts Check for dependency alerts +--secret-scanning-alerts Check for secret scanning alerts +--code-scanning-alerts Check for code scanning alerts + +EOT + } } -1; \ No newline at end of file +1; diff --git a/security-gate.pl b/security-gate.pl index 256966c..de12b6d 100644 --- a/security-gate.pl +++ b/security-gate.pl @@ -31,7 +31,7 @@ sub main { my %alert_checks = ( 'dependency-alerts' => sub { SecurityGate::Engine::Dependencies -> new($token, $repository, \%severity_limits) }, - 'secret-alerts' => sub { SecurityGate::Engine::Secrets -> new($token, $repository) }, + 'secret-alerts' => sub { SecurityGate::Engine::Secrets -> new($token, $repository, \%severity_limits) }, 'code-alerts' => sub { SecurityGate::Engine::Code -> new($token, $repository, \%severity_limits) } ); @@ -66,4 +66,4 @@ sub main { else { exit main(); -} \ No newline at end of file +} diff --git a/tests/code-api-request-error.t b/tests/code-api-request-error.t new file mode 100644 index 0000000..b5b5fc4 --- /dev/null +++ b/tests/code-api-request-error.t @@ -0,0 +1,64 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use Test::Exception; +use Test::MockObject; +use Test::Output; +use Capture::Tiny qw(capture_stdout); + +{ + package Mojo::UserAgent; + use Test::MockObject; + + my $mock_response; + + sub new { + my $class = shift; + return Test::MockObject -> new -> mock('get', sub { + my ($self, $url, $headers) = @_; + return Test::MockObject -> new -> mock('result', sub { + return $mock_response; + }); + }); + } + + sub set_mock_response { + my ($class, $response) = @_; + $mock_response = $response; + return $mock_response; + } +} + +use lib '../lib'; +use SecurityGate::Engine::Code; + +subtest 'API request error' => sub { + plan tests => 2; + + my $mock_response = Mojo::UserAgent -> set_mock_response(Test::MockObject -> new); + $mock_response -> set_always('code', 401); + + my %severity_limits = ( + critical => 0, + high => 0, + medium => 0, + low => 0 + ); + + my $result; + my $error_message = qr/Error: \s Unable \s to \s fetch \s code \s scanning \s alerts\./x; + my $status_code = qr/\s HTTP \s status \s code: \s 401/x; + my $full_error_pattern = qr/$error_message$status_code/x; + + stdout_like( + sub { $result = SecurityGate::Engine::Code -> new('test_token', 'test_repo', \%severity_limits) }, + $full_error_pattern, + 'Correct error message for API request failure' + ); + + is($result, 1, 'Returns 1 when API request fails'); +}; + +done_testing(); diff --git a/tests/dependencies-api-error-handling.t b/tests/dependencies-api-error-handling.t new file mode 100644 index 0000000..8f5364a --- /dev/null +++ b/tests/dependencies-api-error-handling.t @@ -0,0 +1,57 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use Test::Exception; +use Test::MockObject; +use Test::Output; + +{ + package Mojo::UserAgent; + use Test::MockObject; + + my $mock_response; + + sub new { + my $class = shift; + return Test::MockObject -> new -> mock('get', sub { + my ($self, $url, $headers) = @_; + return Test::MockObject -> new -> mock('result', sub { + return $mock_response; + }); + }); + } + + sub set_mock_response { + my ($class, $response) = @_; + $mock_response = $response; + return; + } +} + +use lib '../lib'; +use SecurityGate::Engine::Dependencies; + +subtest 'API error handling' => sub { + plan tests => 1; + + my $mock_response = Test::MockObject -> new; + Mojo::UserAgent -> set_mock_response($mock_response); + $mock_response -> set_always('code', 401); + + my %severity_limits = ( + critical => 0, + high => 0, + medium => 0, + low => 0 + ); + + is( + SecurityGate::Engine::Dependencies -> new('invalid_token', 'test_repo', \%severity_limits), + 1, + 'Returns 1 when API request fails' + ); +}; + +done_testing(); diff --git a/tests/dependencies-severity-counting.t b/tests/dependencies-severity-counting.t new file mode 100644 index 0000000..8f6cc56 --- /dev/null +++ b/tests/dependencies-severity-counting.t @@ -0,0 +1,63 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use Test::Exception; +use Test::MockObject; +use Test::Output; + +{ + package Mojo::UserAgent; + use Test::MockObject; + + my $mock_response; + + sub new { + my $class = shift; + return Test::MockObject -> new -> mock('get', sub { + my ($self, $url, $headers) = @_; + return Test::MockObject -> new -> mock('result', sub { + return $mock_response; + }); + }); + } + + sub set_mock_response { + my ($class, $response) = @_; + $mock_response = $response; + return; + } +} + +use lib '../lib'; +use SecurityGate::Engine::Dependencies; + +subtest 'Severity counting' => sub { + plan tests => 1; + + my $mock_response = Test::MockObject -> new; + Mojo::UserAgent -> set_mock_response($mock_response); + $mock_response -> set_always('code', 200); + $mock_response -> set_always('json', [ + { state => 'open', security_vulnerability => { severity => 'high' } }, + { state => 'open', security_vulnerability => { severity => 'critical' } }, + { state => 'open', security_vulnerability => { severity => 'medium' } }, + { state => 'closed', security_vulnerability => { severity => 'low' } }, + ]); + + my %severity_limits = ( + critical => 0, + high => 0, + medium => 0, + low => 0 + ); + + stdout_like( + sub { SecurityGate::Engine::Dependencies -> new('test_token', 'test_repo', \%severity_limits) }, + qr/critical:\ 1.*high:\ 1.*medium:\ 1.*low:\ 0/xs, + 'Severity counts are correct' + ); +}; + +done_testing(); diff --git a/tests/dependencies-threshold-checking.t b/tests/dependencies-threshold-checking.t new file mode 100644 index 0000000..df84c3d --- /dev/null +++ b/tests/dependencies-threshold-checking.t @@ -0,0 +1,74 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use Test::Exception; +use Test::MockObject; +use Test::Output; + +{ + package Mojo::UserAgent; + use Test::MockObject; + + my $mock_response; + + sub new { + my $class = shift; + return Test::MockObject -> new -> mock('get', sub { + my ($self, $url, $headers) = @_; + return Test::MockObject -> new -> mock('result', sub { + return $mock_response; + }); + }); + } + + sub set_mock_response { + my ($class, $response) = @_; + $mock_response = $response; + return; + } +} + +use lib '../lib'; +use SecurityGate::Engine::Dependencies; + +subtest 'Threshold checking' => sub { + plan tests => 2; + + my $mock_response = Test::MockObject -> new; + Mojo::UserAgent -> set_mock_response($mock_response); + $mock_response -> set_always('code', 200); + $mock_response -> set_always('json', [ + { state => 'open', security_vulnerability => { severity => 'critical' } }, + { state => 'open', security_vulnerability => { severity => 'critical' } }, + ]); + + my %severity_limits_exceeded = ( + critical => 1, + high => 0, + medium => 0, + low => 0 + ); + + my %severity_limits_not_exceeded = ( + critical => 2, + high => 0, + medium => 0, + low => 0 + ); + + is( + SecurityGate::Engine::Dependencies -> new('test_token', 'test_repo', \%severity_limits_exceeded), + 1, + 'Returns 1 when threshold is exceeded' + ); + + is( + SecurityGate::Engine::Dependencies -> new('test_token', 'test_repo', \%severity_limits_not_exceeded), + 0, + 'Returns 0 when threshold is not exceeded' + ); +}; + +done_testing(); diff --git a/tests/helper-output.t b/tests/helper-output.t new file mode 100644 index 0000000..70443e4 --- /dev/null +++ b/tests/helper-output.t @@ -0,0 +1,21 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use Test::Exception; +use lib '../lib'; +use SecurityGate::Utils::Helper; + +subtest 'Helper output' => sub { + my $helper_output = SecurityGate::Utils::Helper->new(); + + like($helper_output, qr/Security\ Gate\ v0\.0\.3/x, 'Helper output contains version'); + like($helper_output, qr/-t,\ --token/x, 'Helper output contains token option'); + like($helper_output, qr/-r,\ --repo/x, 'Helper output contains repo option'); + like($helper_output, qr/--dependency-alerts/x, 'Helper output contains dependency alerts option'); + like($helper_output, qr/--secret-scanning-alerts/x, 'Helper output contains secret scanning alerts option'); + like($helper_output, qr/--code-scanning-alerts/x, 'Helper output contains code scanning alerts option'); +}; + +done_testing(); diff --git a/tests/no-open-code-scanning-alerts.t b/tests/no-open-code-scanning-alerts.t new file mode 100644 index 0000000..9afdb40 --- /dev/null +++ b/tests/no-open-code-scanning-alerts.t @@ -0,0 +1,61 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use Test::Exception; +use Test::MockObject; +use Test::Output; +use Capture::Tiny qw(capture_stdout); + +{ + package Mojo::UserAgent; + use Test::MockObject; + + my $mock_response; + + sub new { + my $class = shift; + return Test::MockObject -> new -> mock('get', sub { + my ($self, $url, $headers) = @_; + return Test::MockObject -> new -> mock('result', sub { + return $mock_response; + }); + }); + } + + sub set_mock_response { + my ($class, $response) = @_; + $mock_response = $response; + return $mock_response; + } +} + +use lib '../lib'; +use SecurityGate::Engine::Code; + +subtest 'No open code scanning alerts' => sub { + plan tests => 2; + + my $mock_response = Mojo::UserAgent -> set_mock_response(Test::MockObject -> new); + $mock_response -> set_always('code', 200); + $mock_response -> set_always('json', []); + + my %severity_limits = ( + critical => 0, + high => 0, + medium => 0, + low => 0 + ); + + my $result; + stdout_like( + sub { $result = SecurityGate::Engine::Code -> new('test_token', 'test_repo', \%severity_limits) }, + qr/\[!\] \s Total \s of \s open \s code \s scanning \s alerts: \s 0/x, + 'Correct output for no open alerts' + ); + + is($result, 0, 'Returns 0 when no open alerts are found'); +}; + +done_testing(); diff --git a/tests/no-open-secret-scanning-alerts.t b/tests/no-open-secret-scanning-alerts.t new file mode 100644 index 0000000..0956440 --- /dev/null +++ b/tests/no-open-secret-scanning-alerts.t @@ -0,0 +1,90 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use Test::Exception; +use Test::MockObject; +use Test::Output; +use Capture::Tiny qw(capture_stdout); + +BEGIN { + use lib '../lib'; + use_ok('SecurityGate::Engine::Secrets') || print "Bail out!\n"; +} + +{ + package MockMojoUserAgent; + use Test::MockObject; + + my $mock_response; + my $locations_response; + + sub new { + my $class = shift; + return Test::MockObject -> new -> mock('get', sub { + my ($self, $url, $headers) = @_; + return Test::MockObject -> new -> mock('result', sub { + if ($url =~ m{locations$}xsm) { + return $locations_response; + } + return $mock_response; + }); + }); + } + + sub setup_mock_response { + my ($code, $json) = @_; + $mock_response = Test::MockObject -> new; + $mock_response -> set_always('code', $code); + $mock_response -> set_always('json', $json); + return; + } + + sub setup_locations_response { + my ($code, $json) = @_; + $locations_response = Test::MockObject -> new; + $locations_response -> set_always('code', $code); + $locations_response -> set_always('json', $json); + return; + } +} + +*Mojo::UserAgent::new = \&MockMojoUserAgent::new; + +subtest 'No open secret scanning alerts' => sub { + plan tests => 2; + + MockMojoUserAgent::setup_mock_response(200, [ + { state => 'closed' }, + { state => 'closed' }, + ]); + + my %severity_limits = ( + critical => 0, + high => 1, + medium => 0, + low => 0 + ); + + my $result; + my $expected_output_part1 = qr/\[!\]\ Total\ of\ open\ secret\ scanning\ alerts:\ 0/xsm; + my $expected_output_part2_part1 = qr/\[-\]\ Number\ of\ secret\ scanning\ alerts\ \(/xsm; + my $expected_output_part2_part2 = qr/0\)\ is\ within\ the\ acceptable\ limit\ \(/xsm; + my $expected_output_part2_part3 = qr/1\)\./xsm; + my $expected_output_part2 = qr/$expected_output_part2_part1$expected_output_part2_part2$expected_output_part2_part3/xsm; + + my $expected_output = qr/$expected_output_part1.*$expected_output_part2/xsm; + + stdout_like( + sub { $result = SecurityGate::Engine::Secrets -> new('test_token', 'test_repo', \%severity_limits) }, + $expected_output, + 'Correct output for no open alerts' + ); + + is($result, 0, 'Returns 0 when no open alerts are found'); +}; + +done_testing(); + +1; diff --git a/tests/open-code-scanning-alerts-exceeding-limits.t b/tests/open-code-scanning-alerts-exceeding-limits.t new file mode 100644 index 0000000..cbe062b --- /dev/null +++ b/tests/open-code-scanning-alerts-exceeding-limits.t @@ -0,0 +1,69 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use Test::Exception; +use Test::MockObject; +use Test::Output; +use Capture::Tiny qw(capture_stdout); + +{ + package Mojo::UserAgent; + use Test::MockObject; + + my $mock_response; + + sub new { + my $class = shift; + return Test::MockObject -> new -> mock('get', sub { + my ($self, $url, $headers) = @_; + return Test::MockObject -> new -> mock('result', sub { + return $mock_response; + }); + }); + } + + sub set_mock_response { + my ($class, $response) = @_; + $mock_response = $response; + return $mock_response; + } +} + +use lib '../lib'; +use SecurityGate::Engine::Code; + +subtest 'Open code scanning alerts exceeding limits' => sub { + plan tests => 3; + + my $mock_response = Mojo::UserAgent -> set_mock_response(Test::MockObject -> new); + $mock_response -> set_always('code', 200); + $mock_response -> set_always('json', [ + { state => 'open', rule => { severity => 'high' } }, + { state => 'open', rule => { severity => 'high' } }, + { state => 'open', rule => { severity => 'medium' } }, + ]); + + my %severity_limits = ( + critical => 0, + high => 1, + medium => 1, + low => 0 + ); + + my ($output, $result); + $output = capture_stdout { + $result = SecurityGate::Engine::Code -> new('test_token', 'test_repo', \%severity_limits); + }; + + like($output, qr/\[!\] \s Total \s of \s open \s code \s scanning \s alerts: \s 3/x, + 'Output contains correct total alerts'); + + like($output, qr/\[\+\] \s More \s than \s \d+ \s \w+ \s code \s scanning \s alerts \s found/x, + 'Output contains correct severity alert count'); + + is($result, 1, 'Returns 1 when open alerts exceed limits'); +}; + +done_testing(); diff --git a/tests/open-code-scanning-alerts-within-limits.t b/tests/open-code-scanning-alerts-within-limits.t new file mode 100644 index 0000000..fb09c1e --- /dev/null +++ b/tests/open-code-scanning-alerts-within-limits.t @@ -0,0 +1,68 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use Test::Exception; +use Test::MockObject; +use Test::Output; +use Capture::Tiny qw(capture_stdout); + +{ + package Mojo::UserAgent; + use Test::MockObject; + + my $mock_response; + + sub new { + my $class = shift; + return Test::MockObject -> new -> mock('get', sub { + my ($self, $url, $headers) = @_; + return Test::MockObject -> new -> mock('result', sub { + return $mock_response; + }); + }); + } + + sub set_mock_response { + my ($class, $response) = @_; + $mock_response = $response; + return $mock_response; + } +} + +use lib '../lib'; +use SecurityGate::Engine::Code; + +subtest 'Open code scanning alerts within limits' => sub { + plan tests => 2; + + my $mock_response = Mojo::UserAgent -> set_mock_response(Test::MockObject -> new); + $mock_response -> set_always('code', 200); + $mock_response -> set_always('json', [ + { state => 'open', rule => { severity => 'high' } }, + { state => 'open', rule => { severity => 'medium' } }, + ]); + + my %severity_limits = ( + critical => 0, + high => 1, + medium => 1, + low => 0 + ); + + my $result; + my $total_pattern = qr/\[!\] \s Total \s of \s open \s code \s scanning \s alerts: \s 2/x; + my $severity_pattern = qr/(?:\[-\] \s (?:low|medium|high|critical): \s \d+\s*)+/x; + my $full_pattern = qr/$total_pattern.*$severity_pattern/sx; + + stdout_like( + sub { $result = SecurityGate::Engine::Code -> new('test_token', 'test_repo', \%severity_limits) }, + $full_pattern, + 'Correct output for open alerts within limits' + ); + + is($result, 0, 'Returns 0 when open alerts are within limits'); +}; + +done_testing(); diff --git a/tests/open-secret-scanning-alerts-exceeding-limits.t b/tests/open-secret-scanning-alerts-exceeding-limits.t new file mode 100644 index 0000000..9343b6e --- /dev/null +++ b/tests/open-secret-scanning-alerts-exceeding-limits.t @@ -0,0 +1,94 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use Test::Exception; +use Test::MockObject; +use Test::Output; +use Capture::Tiny qw(capture_stdout); + +BEGIN { + use lib '../lib'; + use_ok('SecurityGate::Engine::Secrets') || print "Bail out!\n"; +} + +{ + package MockMojoUserAgent; + use Test::MockObject; + + my $mock_response; + my $locations_response; + + sub new { + my $class = shift; + return Test::MockObject -> new -> mock('get', sub { + my ($self, $url, $headers) = @_; + return Test::MockObject -> new -> mock('result', sub { + if ($url =~ m{locations$}xsm) { + return $locations_response; + } + return $mock_response; + }); + }); + } + + sub setup_mock_response { + my ($code, $json) = @_; + $mock_response = Test::MockObject -> new; + $mock_response -> set_always('code', $code); + $mock_response -> set_always('json', $json); + return; + } + + sub setup_locations_response { + my ($code, $json) = @_; + $locations_response = Test::MockObject -> new; + $locations_response -> set_always('code', $code); + $locations_response -> set_always('json', $json); + return; + } +} + +*Mojo::UserAgent::new = \&MockMojoUserAgent::new; + +subtest 'Open secret scanning alerts exceeding limits' => sub { + plan tests => 5; + + MockMojoUserAgent::setup_mock_response(200, [ + { state => 'open', number => 1 }, + { state => 'open', number => 2 }, + ]); + + MockMojoUserAgent::setup_locations_response(200, [ + { path => 'file1.txt', start_line => 10 }, + { path => 'file2.txt', start_line => 20 }, + ]); + + my %severity_limits = ( + critical => 0, + high => 1, + medium => 0, + low => 0 + ); + + my $result; + my $output = capture_stdout { + $result = SecurityGate::Engine::Secrets -> new('test_token', 'test_repo', \%severity_limits); + }; + + like($output, qr{\[!\]\ Total\ of\ open\ secret\ scanning\ alerts:\ 2}xsm, 'Correct total number of alerts'); + like($output, qr{\[-\]\ Alert\ 1\ found\ in\ the\ following\ locations:}xsm, 'Alert 1 details present'); + like($output, qr{\[-\]\ Alert\ 2\ found\ in\ the\ following\ locations:}xsm, 'Alert 2 details present'); + + my $match_plus_sign = qr/\[\+\]/xsm; + my $match_more_than = qr/\ More\ than\ \d+/xsm; + my $match_alerts_found = qr/\ secret\ scanning\ alerts?\ found\./xsm; + my $match_blocking = qr/\ Blocking\ pipeline\./xsm; + like($output, qr{$match_plus_sign$match_more_than$match_alerts_found$match_blocking}xsm, 'Blocking message present'); + is($result, 1, 'Returns 1 when open alerts exceed limit'); +}; + +done_testing(); + +1; diff --git a/tests/open-secret-scanning-alerts-within-limits.t b/tests/open-secret-scanning-alerts-within-limits.t new file mode 100644 index 0000000..9deba7c --- /dev/null +++ b/tests/open-secret-scanning-alerts-within-limits.t @@ -0,0 +1,95 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use Test::Exception; +use Test::MockObject; +use Test::Output; +use Capture::Tiny qw(capture_stdout); + +BEGIN { + use lib '../lib'; + use_ok('SecurityGate::Engine::Secrets') || print "Bail out!\n"; +} + +{ + package MockMojoUserAgent; + use Test::MockObject; + + my $mock_response; + my $locations_response; + + sub new { + my $class = shift; + return Test::MockObject -> new -> mock('get', sub { + my ($self, $url, $headers) = @_; + return Test::MockObject -> new -> mock('result', sub { + if ($url =~ m{locations$}xsm) { + return $locations_response; + } + return $mock_response; + }); + }); + } + + sub setup_mock_response { + my ($code, $json) = @_; + $mock_response = Test::MockObject -> new; + $mock_response -> set_always('code', $code); + $mock_response -> set_always('json', $json); + return; + } + + sub setup_locations_response { + my ($code, $json) = @_; + $locations_response = Test::MockObject -> new; + $locations_response -> set_always('code', $code); + $locations_response -> set_always('json', $json); + return; + } +} + +*Mojo::UserAgent::new = \&MockMojoUserAgent::new; + +subtest 'Open secret scanning alerts within limits' => sub { + plan tests => 2; + + MockMojoUserAgent::setup_mock_response(200, [ + { state => 'open', number => 1 }, + ]); + + MockMojoUserAgent::setup_locations_response(200, [ + { path => 'file1.txt', start_line => 10 }, + ]); + + my %severity_limits = ( + critical => 0, + high => 1, + medium => 0, + low => 0 + ); + + my $result; + my $expected_output_part1 = qr/\[!\]\ Total\ of\ open\ secret\ scanning\ alerts:\ 1/xsm; + my $expected_output_part2 = qr/\[-\]\ Alert\ 1\ found\ in\ the\ following\ locations:/xsm; + my $expected_output_part3 = qr/File:\ file1\.txt,\ Start\ line:\ 10/xsm; + my $expected_output_part4_part1 = qr/\[-\]\ Number\ of\ secret\ scanning\ alerts\ \(/xsm; + my $expected_output_part4_part2 = qr/1\)\ is\ within\ the\ acceptable\ limit\ \(/xsm; + my $expected_output_part4_part3 = qr/1\)\./xsm; + my $expected_output_part4 = qr/$expected_output_part4_part1$expected_output_part4_part2$expected_output_part4_part3/xsm; + + my $expected_output = qr/$expected_output_part1.*$expected_output_part2.*$expected_output_part3.*$expected_output_part4/xsm; + + stdout_like( + sub { $result = SecurityGate::Engine::Secrets -> new('test_token', 'test_repo', \%severity_limits) }, + $expected_output, + 'Correct output for open alerts within limit' + ); + + is($result, 0, 'Returns 0 when open alerts are within limit'); +}; + +done_testing(); + +1; diff --git a/tests/secrets-api-error-handling.t b/tests/secrets-api-error-handling.t new file mode 100644 index 0000000..1b60aef --- /dev/null +++ b/tests/secrets-api-error-handling.t @@ -0,0 +1,83 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use Test::Exception; +use Test::MockObject; +use Test::Output; +use Capture::Tiny qw(capture_stdout); + +BEGIN { + use lib '../lib'; + use_ok('SecurityGate::Engine::Secrets') || print "Bail out!\n"; +} + +{ + package MockMojoUserAgent; + use Test::MockObject; + + my $mock_response; + my $locations_response; + + sub new { + my $class = shift; + return Test::MockObject -> new -> mock('get', sub { + my ($self, $url, $headers) = @_; + return Test::MockObject -> new -> mock('result', sub { + if ($url =~ m{locations$}xsm) { + return $locations_response; + } + return $mock_response; + }); + }); + } + + sub setup_mock_response { + my ($code, $json) = @_; + $mock_response = Test::MockObject -> new; + $mock_response -> set_always('code', $code); + $mock_response -> set_always('json', $json); + return; + } + + sub setup_locations_response { + my ($code, $json) = @_; + $locations_response = Test::MockObject -> new; + $locations_response -> set_always('code', $code); + $locations_response -> set_always('json', $json); + return; + } +} + +*Mojo::UserAgent::new = \&MockMojoUserAgent::new; + +subtest 'API error handling' => sub { + plan tests => 2; + + MockMojoUserAgent::setup_mock_response(401, undef); + + my %severity_limits = ( + critical => 0, + high => 1, + medium => 0, + low => 0 + ); + + my $result; + my $expected_error_output_part1 = qr/Error:\ Unable\ to\ fetch\ secret\ scanning\ alerts\./xsm; + my $expected_error_output_part2 = qr/\ HTTP\ status\ code:\ 401/xsm; + my $expected_error_output = qr/$expected_error_output_part1.*$expected_error_output_part2/xsm; + + stdout_like( + sub { $result = SecurityGate::Engine::Secrets -> new('invalid_token', 'test_repo', \%severity_limits) }, + $expected_error_output, + 'Correct error message for API failure' + ); + + is($result, 1, 'Returns 1 when API request fails'); +}; + +done_testing(); + +1; diff --git a/tests/security-gate.t b/tests/security-gate.t index 8020512..146bf85 100644 --- a/tests/security-gate.t +++ b/tests/security-gate.t @@ -6,6 +6,8 @@ use Test::More; use Test::Exception; use Test::MockObject; use Test::Output; +use Carp qw(croak); +use Capture::Tiny qw(capture); use Mojo::JSON qw(encode_json); local $ENV{TEST_MODE} = 1; @@ -20,7 +22,7 @@ subtest 'Command-line argument parsing' => sub { local @ARGV = (); stdout_like( sub { main() }, - qr/Security Gate v0\.0\.3/, + qr/Security\ Gate\ v0\.0\.3/x, 'Help message displayed when no arguments provided' ); }; @@ -43,23 +45,23 @@ subtest 'Severity counting' => sub { local @ARGV = ('-t', 'test_token', '-r', 'test_repo'); stdout_like( sub { main() }, - qr/critical: 1.*high: 1.*medium: 1.*low: 0/s, + qr/critical:\ 1.*high:\ 1.*medium:\ 1.*low:\ 0/xs, 'Severity counts are correct' ); }; subtest 'Threshold checking' => sub { - my $mock_response = Test::MockObject -> new(); - $mock_response -> set_always('code', 200); - $mock_response -> set_always('json', [ + my $mock_response = Test::MockObject -> new(); + $mock_response -> set_always('code', 200); + $mock_response -> set_always('json', [ { state => 'open', security_vulnerability => { severity => 'critical' } }, { state => 'open', security_vulnerability => { severity => 'critical' } }, ]); - my $mock_tx = Test::MockObject -> new(); - $mock_tx -> set_always('result', $mock_response); + my $mock_tx = Test::MockObject -> new(); + $mock_tx -> set_always('result', $mock_response); - $mock_ua -> set_always('get', $mock_tx); + $mock_ua -> set_always('get', $mock_tx); local @ARGV = ('-t', 'test_token', '-r', 'test_repo', '-c', '1'); is( @@ -90,21 +92,26 @@ subtest 'Output formatting' => sub { $mock_ua -> set_always('get', $mock_tx); local @ARGV = ('-t', 'test_token', '-r', 'test_repo'); + + my $total_alerts_re = qr/\[!\]\ Total\ of\ security\ alerts:/x; + my $critical_alerts_re = qr/\[-\]\ critical:\ 1/x; + my $high_alerts_re = qr/\[-\]\ high:\ 1/x; + stdout_like( sub { main() }, - qr/\[!\] Total of security alerts:.*\[-\] critical: 1.*\[-\] high: 1/s, + qr/$total_alerts_re.*$critical_alerts_re.*$high_alerts_re/xs, 'Output is correctly formatted' ); }; subtest 'Invalid token or repository' => sub { - my $mock_response = Test::MockObject -> new(); - $mock_response -> set_always('code', 401); + my $mock_response = Test::MockObject -> new(); + $mock_response -> set_always('code', 401); - my $mock_tx = Test::MockObject -> new(); - $mock_tx -> set_always('result', $mock_response); + my $mock_tx = Test::MockObject -> new(); + $mock_tx -> set_always('result', $mock_response); - $mock_ua -> set_always('get', $mock_tx); + $mock_ua -> set_always('get', $mock_tx); local @ARGV = ('-t', 'invalid_token', '-r', 'invalid_repo'); is( @@ -115,14 +122,14 @@ subtest 'Invalid token or repository' => sub { }; subtest 'Empty response from GitHub API' => sub { - my $mock_response = Test::MockObject -> new(); - $mock_response -> set_always('code', 200); - $mock_response -> set_always('json', []); + my $mock_response = Test::MockObject -> new(); + $mock_response -> set_always('code', 200); + $mock_response -> set_always('json', []); - my $mock_tx = Test::MockObject -> new(); - $mock_tx -> set_always('result', $mock_response); + my $mock_tx = Test::MockObject -> new(); + $mock_tx -> set_always('result', $mock_response); - $mock_ua -> set_always('get', $mock_tx); + $mock_ua -> set_always('get', $mock_tx); local @ARGV = ('-t', 'test_token', '-r', 'test_repo'); is( @@ -148,22 +155,25 @@ subtest 'Multiple severity thresholds' => sub { local @ARGV = ('-t', 'test_token', '-r', 'test_repo', '-c', '0', '-h', '0', '-m', '0', '-l', '0'); - my $stdout; - { - local *STDOUT; - open STDOUT, '>', \$stdout; - - my $result = main(); - - diag("STDOUT: $stdout"); - diag("Result: $result"); - - is( - $result, - 1, - 'Script exits with non-zero code when multiple thresholds are exceeded' - ); - } + my ($stdout, $stderr, $result) = capture { + main(); + }; + + diag("STDOUT: $stdout"); + diag("STDERR: $stderr"); + diag("Result: $result"); + + is( + $result, + 1, + 'Script exits with non-zero code when multiple thresholds are exceeded' + ); + + like( + $stdout, + qr/Total\ of\ security\ alerts:/x, + 'Output contains expected content' + ); }; -done_testing(); \ No newline at end of file +done_testing();