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

Add vulcan-repository-sctrl check #770

Open
wants to merge 1 commit into
base: checkshttp
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Currently there's no vendoring provided for this project.
* **vulcan-nuclei** - Runs [Nuclei](https://github.com/projectdiscovery/nuclei) scanner tool with selected [templates](https://github.com/projectdiscovery/nuclei-templates/)
* **vulcan-prowler** - Checks compliance against CIS AWS Foundations Benchmark
* **vulcan-results-load-test** - Internal testing check, not for production
* **vulcan-repository-sctrl** - Checks whether a Git repository implements security controls.
* **vulcan-retirejs** - Checks for vulnerabilities in JS frontend dependencies
* **vulcan-semgrep** - Runs [Semgrep](https://github.com/returntocorp/semgrep) scanner tool for detect security issues in code
* **vulcan-sleep** - Internal testing check, not for production
Expand Down
1 change: 1 addition & 0 deletions cmd/vulcan-repository-sctrl/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
vulcan-repository-sctrl
14 changes: 14 additions & 0 deletions cmd/vulcan-repository-sctrl/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2024 Adevinta

FROM semgrep/semgrep:1.97.0-nonroot

# Override entrypoint
ENTRYPOINT ["/usr/bin/env"]

# Install check
ARG TARGETOS
ARG TARGETARCH
COPY ${TARGETOS}/${TARGETARCH}/vulcan-repository-sctrl /
COPY custom-rules /custom-rules

CMD ["/vulcan-repository-sctrl"]
5 changes: 5 additions & 0 deletions cmd/vulcan-repository-sctrl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# vulcan-repository-sctrl

Checks if a code repository has security controls in place.

Based on [Semgrep](https://github.com/returntocorp/semgrep)
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
rules:
- id: repository-with-lava-scan
message: 'Action using lava scan binary'
severity: INFO
languages:
- yaml
paths:
include:
- ".github/**/*.yaml"
- ".github/**/*.yml"
patterns:
- pattern-regex: 'lava scan'
- id: repository-with-lava-action
message: 'Action running Lava'
severity: INFO
languages:
- yaml
paths:
include:
- ".github/**/*.yaml"
- ".github/**/*.yml"
patterns:
- patterns:
- pattern: |
...
uses: "$USES"
- metavariable-pattern:
metavariable: $USES
language: generic
pattern-either:
- pattern: lava-internal-action
- pattern: lava-action
- id: repository-with-sonarqube-action
message: 'Action running Sonarqube'
severity: INFO
languages:
- yaml
paths:
include:
- ".github/**/*.yaml"
- ".github/**/*.yml"
patterns:
- pattern: |
...
uses: "$USES"
- metavariable-pattern:
metavariable: $USES
language: generic
pattern: code-quality-action
- id: repository-with-trivy-action
message: 'Action running Trivy'
severity: INFO
languages:
- yaml
paths:
include:
- ".github/**/*.yaml"
- ".github/**/*.yml"
patterns:
- pattern: |
...
uses: "$USES"
- metavariable-pattern:
metavariable: $USES
language: generic
pattern: trivy-action
- id: repository-with-snyk-action
message: 'Action running Snyk'
severity: INFO
languages:
- yaml
paths:
include:
- ".github/**/*.yaml"
- ".github/**/*.yml"
patterns:
- pattern: |
...
uses: "$USES"
- metavariable-pattern:
metavariable: $USES
language: generic
pattern: snyk/actions
- id: repository-with-govulncheck
message: 'Action running Govulncheck'
severity: INFO
languages:
- yaml
paths:
include:
- ".github/**/*.yaml"
- ".github/**/*.yml"
patterns:
- pattern: |
...
uses: "$USES"
- metavariable-pattern:
metavariable: $USES
language: generic
pattern: govulncheck-action
166 changes: 166 additions & 0 deletions cmd/vulcan-repository-sctrl/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
Copyright 2024 Adevinta
*/

package main

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"

"github.com/sirupsen/logrus"
)

const (
// GitHubAPI defines the default public GitHub base URL.
GitHubAPI = "https://api.github.com"

// GitHubEntepriseAPIPath defines the default GitHub Enterprise API path.
GitHubEntepriseAPIPath = "/api/v3"

// DefaultMaxRetries defines the default number of retries for the HTTP request.
DefaultMaxRetries = 3

// DefaultBackoffDuration defines the default backoff duration for the HTTP request.
DefaultBackoffDuration = 5 * time.Second
)

// RSA represents the repository security and analysis information from the GitHub API.
type RSA struct {
SecurityAndAnalysis struct {
SecretScanning struct {
Status string `json:"status"`
} `json:"secret_scanning"`
SecretScanningPushProtection struct {
Status string `json:"status"`
} `json:"secret_scanning_push_protection"`
DependabotSecurityUpdates struct {
Status string `json:"status"`
} `json:"dependabot_security_updates"`
SecretScanningNonProviderPatterns struct {
Status string `json:"status"`
} `json:"secret_scanning_non_provider_patterns"`
SecretScanningValidityChecks struct {
Status string `json:"status"`
} `json:"secret_scanning_validity_checks"`
} `json:"security_and_analysis"`
}

func checkDependabot(ctx context.Context, logger *logrus.Entry, target string) ([]map[string]string, error) {
findingRows := []map[string]string{}

// Get the repository security information.
rsa, err := getRepoSecurityWithRetry(ctx, target, DefaultMaxRetries, DefaultBackoffDuration)
if err != nil {
return findingRows, err
}
logger.WithField("security_and_analysis", rsa).Info("repository security and analysis")

// If the token does not have access to the repository security settings, return an error.
if rsa.SecurityAndAnalysis.DependabotSecurityUpdates.Status == "" {
return findingRows, fmt.Errorf("unable to obtain repository security information")
}

// Check if Dependabot security updates are enabled.
if rsa.SecurityAndAnalysis.DependabotSecurityUpdates.Status != "enabled" {
return findingRows, nil
}

link := strings.TrimSuffix(target, ".git") + "/settings/security_analysis"
row := map[string]string{
"Control": "Dependabot is enabled",
"Link": fmt.Sprintf("(Link)[%s]", link),
}
findingRows = append(findingRows, row)

return findingRows, nil
}

func getRepoSecurityWithRetry(ctx context.Context, target string, maxRetries int, backoff time.Duration) (RSA, error) {
var err error
var rsa RSA
var statusCode int

for attempt := 0; attempt <= maxRetries; attempt++ {
rsa, statusCode, err = getRepoSecurity(ctx, target)
if err == nil {
return rsa, nil
}
if !strings.HasPrefix(err.Error(), "unexpected status code") {
return rsa, err
}

if statusCode >= 500 || statusCode == 429 {
time.Sleep(backoff)
backoff *= 2
continue
}
break
}

return rsa, fmt.Errorf("failed after %d attempts with error: %w", maxRetries, err)
}

func getRepoSecurity(ctx context.Context, target string) (RSA, int, error) {
var rsa RSA
targetURL, err := url.Parse(target)
if err != nil {
return rsa, 0, fmt.Errorf("unable to parse target as URL: %w", err)
}

targetURL.Path = strings.TrimSuffix(targetURL.Path, ".git")
splitPath := strings.Split(targetURL.Path, "/")
org, repo := splitPath[1], splitPath[2]

var url, token string
switch {
// Public GitHub.
case targetURL.Host == "github.com":
url = fmt.Sprintf("%s/repos/%s/%s", GitHubAPI, org, repo)
token = os.Getenv("GITHUB_API_TOKEN")
// Private GitHub Enterprise.
case strings.HasPrefix(target, os.Getenv("GITHUB_ENTERPRISE_ENDPOINT")):
url = fmt.Sprintf("%s://%s%s/repos/%s/%s", targetURL.Scheme, targetURL.Host, GitHubEntepriseAPIPath, org, repo)
token = os.Getenv("GITHUB_ENTERPRISE_TOKEN")
default:
return rsa, 0, fmt.Errorf("unsupported code repository URL: %s", target)
}

client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return rsa, 0, err
}

req.Header.Set("Accept", "application/vnd.github+json")
if token != "" {
req.Header.Set("Authorization", "token "+token)
}

resp, err := client.Do(req)
if err != nil {
return rsa, resp.StatusCode, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return rsa, resp.StatusCode, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return rsa, resp.StatusCode, fmt.Errorf("failed to read response body: %w", err)
}
if err := json.Unmarshal(body, &rsa); err != nil {
return rsa, resp.StatusCode, fmt.Errorf("failed to unmarshal response: %w", err)
}

return rsa, resp.StatusCode, nil
}
11 changes: 11 additions & 0 deletions cmd/vulcan-repository-sctrl/local.toml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[Check]
Target = "https://github.com/adevinta/vulcan-checks.git"
AssetType = "GitRepository"
# Options = '{"depth": 1, "branch":"", "rule_config_path":""}'

#[RequiredVars]
#GITHUB_ENTERPRISE_ENDPOINT = "THE_ENDPOINT"
#GITHUB_ENTERPRISE_TOKEN = "THE_TOKEN"

#[Log]
#LogLevel = "DEBUG"
Loading