diff --git a/.github/workflows/deploy-image.yml b/.github/workflows/deploy-image.yml index 3e72124..6c6c2f3 100644 --- a/.github/workflows/deploy-image.yml +++ b/.github/workflows/deploy-image.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - develop jobs: build-and-deploy: @@ -29,4 +28,4 @@ jobs: password: ${{ secrets.LESIS_DEPLOY }} - name: Push Docker image to GitHub Container Registry - run: docker push docker.pkg.github.com/${{ github.repository }}/security-gate:latest + run: docker push docker.pkg.github.com/${{ github.repository }}/security-gate:latest \ No newline at end of file diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 3596ff7..9a77bbd 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -15,4 +15,4 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build the Docker image - run: docker build . --file Dockerfile --tag security-gate:$(date +%s) + run: docker build . --file Dockerfile --tag security-gate:$(date +%s) \ No newline at end of file diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 2ff3bdb..4573e30 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -3,6 +3,7 @@ name: Linter on: push: branches: + - main - develop pull_request: branches: @@ -17,4 +18,4 @@ jobs: - name: Run Perl::Critic uses: natanlao/perl-critic-action@v1.1 with: - files: critic + files: critic \ No newline at end of file diff --git a/.github/workflows/security-gate.yml b/.github/workflows/security-gate.yml index f8b35df..4c53d9d 100644 --- a/.github/workflows/security-gate.yml +++ b/.github/workflows/security-gate.yml @@ -1,4 +1,4 @@ -name: Security Gate - Instriq +name: Security Gate - LESIS on: push: @@ -38,4 +38,7 @@ jobs: -c "$MAX_CRITICAL" \ -h "$MAX_HIGH" \ -m "$MAX_MEDIUM" \ - -l "$MAX_LOW" + -l "$MAX_LOW" \ + --dependency-alerts + --secrets-alerts + --code-alerts \ No newline at end of file diff --git a/.github/workflows/zarn.yml b/.github/workflows/zarn.yml index 67655c7..5f077c2 100644 --- a/.github/workflows/zarn.yml +++ b/.github/workflows/zarn.yml @@ -22,4 +22,4 @@ jobs: - name: Send result to Github Security uses: github/codeql-action/upload-sarif@v3 with: - sarif_file: result.sarif + sarif_file: result.sarif \ No newline at end of file diff --git a/.perlcriticrc b/.perlcriticrc index 5e2bb23..61ab588 100644 --- a/.perlcriticrc +++ b/.perlcriticrc @@ -1,4 +1,4 @@ -severity = 4 +severity = 3 [-TestingAndDebugging::RequireUseStrict] -[-TestingAndDebugging::RequireUseWarnings] +[-TestingAndDebugging::RequireUseWarnings] \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index a3aaef1..31be951 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,7 +2,7 @@ License ============== The MIT License (MIT) -Copyright (c) 2023 | Instriq.io +Copyright (c) 2023 - 2024 | www.lesis.lat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d4c1283..ae0dccd 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - +

@@ -15,7 +15,7 @@ ### Summary -This is a project that allows you to use a Security Gate within Github, using Actions and your project's Security Alerts as an information base. Currently only Dependabot Alerts are supported, soon we will have support for Secrets and Security Advisories Alerts. +This is a project that allows you to use a Security Gate within Github, using Actions and your project's Security Alerts as an information base. Currently alerts from DependaBot, Code Scanning and Secret Scanning are supported. You can define a vulnerability policy based on impact i.e. the number of vulnerabilities per threat, and automatically block your CI/CD pipeline if these policies are not met. This ensures that your application has greater protection, preventing codes that contain known threats from being deployed in production. @@ -27,7 +27,7 @@ You need to create a token with read access to Security Alerts and configure it In your repository, create a YAML file at: ```.github/workflows/security-gate.yml``` with this content: ```yaml -name: Security Gate - Instriq +name: Security Gate - LESIS on: push: @@ -78,17 +78,20 @@ $ sudo cpanm --installdeps . # Basic usage $ perl security-gate.pl --help -Security Gate v0.0.3 +Security Gate v0.1.0 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 +==================== + 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-alerts Check for secret scanning alerts + --code-alerts Check for code scanning alerts ``` --- diff --git a/SECURITY.md b/SECURITY.md index a6c27a8..e6dccc4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security Policy -If you find a security issue, please DO NOT submit it via the issue tracker! Instead, please follow responsible disclosure practices and send information about security issues directly to [security@instriq.io](mailto:security@instriq.io) so that a proper assessment can be made and a fix prepared before a wide announcement. You will receive an acknowledgement within 24 hours. +If you find a security issue, please DO NOT submit it via the issue tracker! Instead, please follow responsible disclosure practices and send information about security issues directly to [security@lesis.lat](mailto:security@lesis.lat) so that a proper assessment can be made and a fix prepared before a wide announcement. You will receive an acknowledgement within 24 hours. Even in cases where you have limited or incomplete information, or you're not sure whether or not a problem constitutes a security issue, please make contact as soon as possible. We can work together to investigate, debug, and assess. diff --git a/action.yml b/action.yml index 1b29128..1ac4cbc 100644 --- a/action.yml +++ b/action.yml @@ -40,4 +40,4 @@ runs: --critical $MAX_CRITICAL \ --high $MAX_HIGH \ --medium $MAX_MEDIUM \ - --low $MAX_LOW + --low $MAX_LOW \ No newline at end of file diff --git a/lib/SecurityGate/Engine/Code.pm b/lib/SecurityGate/Engine/Code.pm new file mode 100644 index 0000000..9b2e549 --- /dev/null +++ b/lib/SecurityGate/Engine/Code.pm @@ -0,0 +1,59 @@ +package SecurityGate::Engine::Code { + use strict; + use warnings; + use Mojo::UserAgent; + use Mojo::JSON; + + sub new { + my ($class, $token, $repository, $severity_limits) = @_; + my $alerts_endpoint = "https://api.github.com/repos/$repository/code-scanning/alerts"; + + my $userAgent = Mojo::UserAgent -> new(); + my $alerts_request = $userAgent -> get($alerts_endpoint, {Authorization => "Bearer $token"}) -> result(); + + if ($alerts_request -> code() == 200) { + my $alerts_data = $alerts_request -> json(); + my $open_alerts = 0; + my %severity_counts = map {$_ => 0} keys %$severity_limits; + + foreach my $alert (@$alerts_data) { + if ($alert -> {state} eq "open") { + $open_alerts++; + + my $severity = $alert -> {rule} -> {severity}; + $severity_counts{$severity}++ if exists $severity_counts{$severity}; + } + } + + print "[!] Total of open code scanning alerts: $open_alerts\n"; + + foreach my $severity (keys %severity_counts) { + print "[-] $severity: $severity_counts{$severity}\n"; + } + + my $threshold_exceeded = 0; + + foreach my $severity (keys %severity_counts) { + if ($severity_counts{$severity} > $severity_limits -> {$severity}) { + print "[+] More than $severity_limits->{$severity} $severity code scanning alerts found.\n"; + + $threshold_exceeded = 1; + } + } + + if ($threshold_exceeded) { + return 1; + } + } + + else { + print "Error: Unable to fetch code scanning alerts. HTTP status code: " . $alerts_request -> code() . "\n"; + + return 1; + } + + return 0; + } +} + +1; \ No newline at end of file diff --git a/lib/SecurityGate/Engine/Dependencies.pm b/lib/SecurityGate/Engine/Dependencies.pm new file mode 100644 index 0000000..951829d --- /dev/null +++ b/lib/SecurityGate/Engine/Dependencies.pm @@ -0,0 +1,57 @@ +package SecurityGate::Engine::Dependencies { + use strict; + use warnings; + use Mojo::UserAgent; + use Mojo::JSON; + use Exporter 'import'; + + our @EXPORT_OK = qw(@SEVERITIES); + our @SEVERITIES = ("critical", "high", "medium", "low"); + + sub new { + my ($class, $token, $repository, $severity_limits) = @_; + + my %severity_counts = map { $_ => 0 } @SEVERITIES; + + my $endpoint = "https://api.github.com/repos/$repository/dependabot/alerts"; + my $userAgent = Mojo::UserAgent -> new(); + my $request = $userAgent -> get($endpoint, {Authorization => "Bearer $token"}) -> result(); + + if ($request -> code() == 200) { + my $data = $request -> json(); + + foreach my $alert (@$data) { + if ($alert -> {state} eq "open") { + my $severity = $alert -> {security_vulnerability} -> {severity}; + $severity_counts{$severity}++; + } + } + + print "[!] Total of security alerts:\n\n"; + + foreach my $severity (@SEVERITIES) { + print "[-] $severity: $severity_counts{$severity}\n"; + } + + print "\n"; + + my $threshold_exceeded = 0; + + foreach my $severity (@SEVERITIES) { + if ($severity_counts{$severity} > $severity_limits -> {$severity}) { + print "[+] More than $severity_limits->{$severity} $severity security alerts found.\n"; + $threshold_exceeded = 1; + } + } + + return $threshold_exceeded; + } + + else { + print "Error: Unable to fetch alerts. HTTP status code: " . $request -> code() . "\n"; + return 1; + } + } +} + +1; \ No newline at end of file diff --git a/lib/SecurityGate/Engine/Secrets.pm b/lib/SecurityGate/Engine/Secrets.pm new file mode 100644 index 0000000..77703ab --- /dev/null +++ b/lib/SecurityGate/Engine/Secrets.pm @@ -0,0 +1,65 @@ +package SecurityGate::Engine::Secrets { + use strict; + use warnings; + use Mojo::UserAgent; + use Mojo::JSON; + + sub new { + my ($class, $token, $repository) = @_; + + 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; + + 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(); + + 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 "[+] Secret scanning alert(s) found. Blocking pipeline.\n"; + return 1; + } + + else { + print "[-] No secret scanning alerts found.\n"; + return 0; + } + } + + else { + print "Error: Unable to fetch secret scanning alerts. HTTP status code: " . $request -> code() . "\n"; + return 1; + } + } +} + +1; \ No newline at end of file diff --git a/lib/SecurityGate/Utils/Helper.pm b/lib/SecurityGate/Utils/Helper.pm new file mode 100644 index 0000000..05920a0 --- /dev/null +++ b/lib/SecurityGate/Utils/Helper.pm @@ -0,0 +1,24 @@ +package SecurityGate::Utils::Helper { + use strict; + 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 for dependency alerts + \r\t--secret-alerts Check for secret scanning alerts + \r\t--code-alerts Check for code scanning alerts\n\n"; + } +} + +1; \ No newline at end of file diff --git a/security-gate.pl b/security-gate.pl index d96bcab..256966c 100644 --- a/security-gate.pl +++ b/security-gate.pl @@ -3,111 +3,67 @@ use 5.030; use strict; use warnings; +use lib "./lib/"; +use SecurityGate::Engine::Dependencies qw(@SEVERITIES); +use SecurityGate::Engine::Secrets; +use SecurityGate::Engine::Code; +use SecurityGate::Utils::Helper; use Getopt::Long; -use Mojo::JSON; -use Mojo::UserAgent; sub main { - my ($token, $repository, @severity); - my @severities = ("critical", "high", "medium", "low"); - - my %severity_counts = ( - critical => 0, - high => 0, - medium => 0, - low => 0 - ); - - my %severity_limits = ( - critical => 0, - high => 0, - medium => 0, - low => 0 - ); + my ($token, $repository, $dependency_alerts, $secret_alerts, $code_alerts); + my %severity_limits = map {$_ => 0} @SEVERITIES; Getopt::Long::GetOptions( - "t|token=s" => \$token, - "r|repo=s" => \$repository, - "c|critical=i" => \$severity_limits{critical}, - "h|high=i" => \$severity_limits{high}, - "m|medium=i" => \$severity_limits{medium}, - "l|low=i" => \$severity_limits{low} + "t|token=s" => \$token, + "r|repo=s" => \$repository, + "c|critical=i" => \$severity_limits{critical}, + "h|high=i" => \$severity_limits{high}, + "m|medium=i" => \$severity_limits{medium}, + "l|low=i" => \$severity_limits{low}, + "dependency-alerts" => \$dependency_alerts, + "secret-alerts" => \$secret_alerts, + "code-alerts" => \$code_alerts ); if ($token && $repository) { - my $endpoint = "https://api.github.com/repos/$repository/dependabot/alerts"; - my $userAgent = Mojo::UserAgent -> new(); - my $request = $userAgent -> get($endpoint, {Authorization => "Bearer $token"}) -> result(); + my $result = 0; - if ($request -> code() == 200) { - my $data = $request -> json(); + my %alert_checks = ( + 'dependency-alerts' => sub { SecurityGate::Engine::Dependencies -> new($token, $repository, \%severity_limits) }, + 'secret-alerts' => sub { SecurityGate::Engine::Secrets -> new($token, $repository) }, + 'code-alerts' => sub { SecurityGate::Engine::Code -> new($token, $repository, \%severity_limits) } + ); - foreach my $alert (@$data) { - if ($alert -> {state} eq "open") { - my $severity = $alert -> {security_vulnerability} -> {severity}; - $severity_counts{$severity}++; - } + for my $alert_type (keys %alert_checks) { + if ($alert_type eq 'dependency-alerts' && $dependency_alerts) { + $result += $alert_checks{$alert_type}->(); } - print "[!] Total of security alerts:\n\n"; - - foreach my $severity (@severities) { - print "[-] $severity: $severity_counts{$severity}\n"; - } - - print "\n"; - - print "Debug: Severity counts: " . join(", ", map {"$_: $severity_counts{$_}"} @severities) . "\n"; - print "Debug: Severity limits: " . join(", ", map {"$_: $severity_limits{$_}"} @severities) . "\n"; - - my $threshold_exceeded = 0; - foreach my $severity (@severities) { - print "Debug: Checking $severity - Count: $severity_counts{$severity}, Limit: $severity_limits{$severity}\n"; - if ($severity_counts{$severity} > $severity_limits{$severity}) { - print "[+] More than $severity_limits{$severity} $severity security alerts found.\n"; - $threshold_exceeded = 1; - } + elsif ($alert_type eq 'secret-alerts' && $secret_alerts) { + $result += $alert_checks{$alert_type}->(); } - print "Debug: Threshold exceeded: $threshold_exceeded\n"; - - if ($threshold_exceeded) { - print "Finalizing the process with error.\n"; - return 1; + elsif ($alert_type eq 'code-alerts' && $code_alerts) { + $result += $alert_checks{$alert_type}->(); } } - else { - print "Error: Unable to fetch alerts. HTTP status code: " . $request->code() . "\n"; - return 1; - } + return $result; + } - return 0; - } - else { - print " - \rSecurity Gate v0.0.3 - \rCore Commands - \r============== - \r\tCommand Description - \r\t------- ----------- - \r\t-t, --token GitHub token - \r\t-r, --repo GitHub repository - \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 - \n"; - + print SecurityGate::Utils::Helper->new(); return 1; } + + return 0; } if ($ENV{TEST_MODE}) { main(); } - + else { exit main(); -} +} \ No newline at end of file diff --git a/tests/security-gate.t b/tests/security-gate.t index afbe190..8020512 100644 --- a/tests/security-gate.t +++ b/tests/security-gate.t @@ -166,4 +166,4 @@ subtest 'Multiple severity thresholds' => sub { } }; -done_testing(); +done_testing(); \ No newline at end of file