Skip to content
Merged
2 changes: 1 addition & 1 deletion api/analysis/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func StartAnalysis(RID string, repository types.Repository) {

log.Info("StartAnalysisTest", apiHost, 2012, RID)

if err := enryScan.New(RID, repository.URL, repository.Branch, enryScan.SecurityTestName, repository.LanguageExclusions, apiHost); err != nil {
if err := enryScan.New(RID, repository.URL, repository.Branch, enryScan.SecurityTestName, repository.LanguageExclusions, repository.ChangedFiles, apiHost); err != nil {
log.Error(logActionStart, logInfoAnalysis, 2011, err)
return
}
Expand Down
304 changes: 264 additions & 40 deletions api/config.yaml

Large diffs are not rendered by default.

33 changes: 19 additions & 14 deletions api/kubernetes/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ func (k Kubernetes) CreatePod(image, cmd, podName, securityTestName string) (str

ctx := goContext.Background()

envVars := []core.EnvVar{
{
Name: "http_proxy",
Value: k.ProxyAddress,
},
{
Name: "https_proxy",
Value: k.ProxyAddress,
},
{
Name: "no_proxy",
Value: k.NoProxyAddresses,
},
}
if isDeltaScanEnabled(securityTestName) {
envVars = append(envVars, core.EnvVar{Name: "HUSKYCI_DELTA_SCAN", Value: "true"})
}

podToCreate := &core.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Expand All @@ -102,20 +120,7 @@ func (k Kubernetes) CreatePod(image, cmd, podName, securityTestName string) (str
"-c",
cmd,
},
Env: []core.EnvVar{
{
Name: "http_proxy",
Value: k.ProxyAddress,
},
{
Name: "https_proxy",
Value: k.ProxyAddress,
},
{
Name: "no_proxy",
Value: k.NoProxyAddresses,
},
},
Env: envVars,
},
},
NodeSelector: k.NodeSelector,
Expand Down
14 changes: 14 additions & 0 deletions api/kubernetes/huskykube.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package kubernetes

import (
"fmt"
"os"
"regexp"
"strings"

Expand All @@ -31,6 +32,19 @@ func configureImagePath(image, tag string) (string, string) {
return canonicalURL, fullContainerImage
}

// getScannerConfig reads a scanner-specific configuration from environment variables.
// It looks for HUSKYCI_SCANNER_<UPPERNAME>_<KEY> (e.g. HUSKYCI_SCANNER_GITLEAKS_DELTA_SCAN).
func getScannerConfig(securityTestName, key string) string {
envVar := "HUSKYCI_SCANNER_" + strings.ToUpper(securityTestName) + "_" + key
return os.Getenv(envVar)
}

// isDeltaScanEnabled returns true if delta scanning is enabled for the given security test
// via the HUSKYCI_SCANNER_<NAME>_DELTA_SCAN environment variable.
func isDeltaScanEnabled(securityTestName string) bool {
return strings.ToLower(getScannerConfig(securityTestName, "DELTA_SCAN")) == "true"
}

// KubeRun starts a new pod and returns its output and an error.
func KubeRun(image, imageTag, cmd, securityTestName, id string, podSchedulingTimeoutInSeconds, timeOutInSeconds int) (string, string, error) {

Expand Down
86 changes: 86 additions & 0 deletions api/kubernetes/huskykube_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package kubernetes

import (
"os"
"testing"
)

func TestGetScannerConfig(t *testing.T) {
t.Setenv("HUSKYCI_SCANNER_GITLEAKS_DELTA_SCAN", "true")
t.Setenv("HUSKYCI_SCANNER_TFSEC_TIMEOUT", "300")

result := getScannerConfig("gitleaks", "DELTA_SCAN")
if result != "true" {
t.Errorf("expected 'true', got '%s'", result)
}

result = getScannerConfig("tfsec", "TIMEOUT")
if result != "300" {
t.Errorf("expected '300', got '%s'", result)
}
}

func TestGetScannerConfig_Missing(t *testing.T) {
_ = os.Unsetenv("HUSKYCI_SCANNER_NONEXISTENT_DELTA_SCAN")

result := getScannerConfig("nonexistent", "DELTA_SCAN")
if result != "" {
t.Errorf("expected empty string for missing env var, got '%s'", result)
}
}

func TestIsDeltaScanEnabled_True(t *testing.T) {
t.Setenv("HUSKYCI_SCANNER_GITLEAKS_DELTA_SCAN", "true")

if !isDeltaScanEnabled("gitleaks") {
t.Error("expected isDeltaScanEnabled to return true when env var is 'true'")
}
}

func TestIsDeltaScanEnabled_TrueCaseInsensitive(t *testing.T) {
t.Setenv("HUSKYCI_SCANNER_GITLEAKS_DELTA_SCAN", "TRUE")

if !isDeltaScanEnabled("gitleaks") {
t.Error("expected isDeltaScanEnabled to return true when env var is 'TRUE' (case-insensitive)")
}
}

func TestIsDeltaScanEnabled_TrueMixedCase(t *testing.T) {
t.Setenv("HUSKYCI_SCANNER_GITLEAKS_DELTA_SCAN", "True")

if !isDeltaScanEnabled("gitleaks") {
t.Error("expected isDeltaScanEnabled to return true when env var is 'True'")
}
}

func TestIsDeltaScanEnabled_False(t *testing.T) {
t.Setenv("HUSKYCI_SCANNER_GITLEAKS_DELTA_SCAN", "false")

if isDeltaScanEnabled("gitleaks") {
t.Error("expected isDeltaScanEnabled to return false when env var is 'false'")
}
}

func TestIsDeltaScanEnabled_Unset(t *testing.T) {
_ = os.Unsetenv("HUSKYCI_SCANNER_GITLEAKS_DELTA_SCAN")

if isDeltaScanEnabled("gitleaks") {
t.Error("expected isDeltaScanEnabled to return false when env var is unset")
}
}

func TestIsDeltaScanEnabled_Empty(t *testing.T) {
t.Setenv("HUSKYCI_SCANNER_GITLEAKS_DELTA_SCAN", "")

if isDeltaScanEnabled("gitleaks") {
t.Error("expected isDeltaScanEnabled to return false when env var is empty")
}
}

func TestIsDeltaScanEnabled_WrongValue(t *testing.T) {
t.Setenv("HUSKYCI_SCANNER_GITLEAKS_DELTA_SCAN", "1")

if isDeltaScanEnabled("gitleaks") {
t.Error("expected isDeltaScanEnabled to return false when env var is '1' (not 'true')")
}
}
7 changes: 7 additions & 0 deletions api/routes/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ func ReceiveRequest(c echo.Context) error {
}
repository.URL = sanitizedRepoURL

// step-01b: Validate changed files (no shell metacharacters)
if err := util.CheckMaliciousChangedFiles(repository.ChangedFiles); err != nil {
log.Error(logActionReceiveRequest, logInfoAnalysis, 1017, repository.ChangedFiles)
reply := map[string]interface{}{"success": false, "error": "invalid changed files"}
return c.JSON(http.StatusBadRequest, reply)
}

// step-02: is this repository already in MongoDB?
repositoryQuery := map[string]interface{}{"repositoryURL": repository.URL}
_, err = apiContext.APIConfiguration.DBInstance.FindOneDBRepository(repositoryQuery)
Expand Down
4 changes: 2 additions & 2 deletions api/securitytest/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (results *RunAllInfo) runGenericScans(ctx context.Context, enryScan SecTest
default:
}

scan, err := runner.newScan(enryScan.RID, enryScan.URL, enryScan.Branch, testName, nil, enryScan.DockerHost)
scan, err := runner.newScan(enryScan.RID, enryScan.URL, enryScan.Branch, testName, nil, enryScan.ChangedFiles, enryScan.DockerHost)
if err != nil {
return err
}
Expand Down Expand Up @@ -153,7 +153,7 @@ func (results *RunAllInfo) runLanguageScans(ctx context.Context, enryScan SecTes
default:
}

scan, err := runner.newScan(enryScan.RID, enryScan.URL, enryScan.Branch, testName, nil, enryScan.DockerHost)
scan, err := runner.newScan(enryScan.RID, enryScan.URL, enryScan.Branch, testName, nil, enryScan.ChangedFiles, enryScan.DockerHost)
if err != nil {
return err
}
Expand Down
8 changes: 4 additions & 4 deletions api/securitytest/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func TestStart_FirstErrorCancelsRemaining(t *testing.T) {

runner := &mockRunner{
genericTests: mockGenericTests,
newScanFunc: func(RID, URL, branch, name string, le map[string]bool, dh string) (*SecTestScanInfo, error) {
newScanFunc: func(RID, URL, branch, name string, le map[string]bool, cf, dh string) (*SecTestScanInfo, error) {
return &SecTestScanInfo{
RID: RID,
SecurityTestName: name,
Expand Down Expand Up @@ -151,7 +151,7 @@ func TestStart_ConcurrentErrorsNoPanic(t *testing.T) {

runner := &mockRunner{
genericTests: mockGenericTests,
newScanFunc: func(RID, URL, branch, name string, le map[string]bool, dh string) (*SecTestScanInfo, error) {
newScanFunc: func(RID, URL, branch, name string, le map[string]bool, cf, dh string) (*SecTestScanInfo, error) {
return &SecTestScanInfo{
RID: RID, SecurityTestName: name,
Container: types.Container{CID: "cid-" + name},
Expand Down Expand Up @@ -181,7 +181,7 @@ func TestStart_AllScansPass(t *testing.T) {

runner := &mockRunner{
genericTests: mockGenericTests,
newScanFunc: func(RID, URL, branch, name string, le map[string]bool, dh string) (*SecTestScanInfo, error) {
newScanFunc: func(RID, URL, branch, name string, le map[string]bool, cf, dh string) (*SecTestScanInfo, error) {
return &SecTestScanInfo{
RID: RID, SecurityTestName: name,
Container: types.Container{CID: "cid-" + name, CResult: "passed", CStatus: "finished"},
Expand Down Expand Up @@ -209,7 +209,7 @@ func TestStart_ConcurrentWritesDataRace(t *testing.T) {

runner := &mockRunner{
genericTests: mockGenericTests,
newScanFunc: func(RID, URL, branch, name string, le map[string]bool, dh string) (*SecTestScanInfo, error) {
newScanFunc: func(RID, URL, branch, name string, le map[string]bool, cf, dh string) (*SecTestScanInfo, error) {
scan := &SecTestScanInfo{
RID: RID,
SecurityTestName: name,
Expand Down
12 changes: 6 additions & 6 deletions api/securitytest/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import "github.com/githubanotaai/huskyci-api/api/types"
type scanRunner interface {
listGenericTests() ([]types.SecurityTest, error)
listLanguageTests(language string) ([]types.SecurityTest, error)
newScan(RID, URL, branch, securityTestName string, languageExclusions map[string]bool, dockerHost string) (*SecTestScanInfo, error)
newScan(RID, URL, branch, securityTestName string, languageExclusions map[string]bool, changedFiles, dockerHost string) (*SecTestScanInfo, error)
startScan(scan *SecTestScanInfo) error
}

Expand All @@ -22,9 +22,9 @@ func (realRunner) listLanguageTests(language string) ([]types.SecurityTest, erro
return getAllDefaultSecurityTests("Language", language)
}

func (realRunner) newScan(RID, URL, branch, securityTestName string, languageExclusions map[string]bool, dockerHost string) (*SecTestScanInfo, error) {
func (realRunner) newScan(RID, URL, branch, securityTestName string, languageExclusions map[string]bool, changedFiles, dockerHost string) (*SecTestScanInfo, error) {
scan := &SecTestScanInfo{}
return scan, scan.New(RID, URL, branch, securityTestName, languageExclusions, dockerHost)
return scan, scan.New(RID, URL, branch, securityTestName, languageExclusions, changedFiles, dockerHost)
}

func (realRunner) startScan(scan *SecTestScanInfo) error {
Expand All @@ -35,7 +35,7 @@ func (realRunner) startScan(scan *SecTestScanInfo) error {
type mockRunner struct {
genericTests []types.SecurityTest
languageTests []types.SecurityTest
newScanFunc func(RID, URL, branch, securityTestName string, languageExclusions map[string]bool, dockerHost string) (*SecTestScanInfo, error)
newScanFunc func(RID, URL, branch, securityTestName string, languageExclusions map[string]bool, changedFiles, dockerHost string) (*SecTestScanInfo, error)
startScanFunc func(scan *SecTestScanInfo) error
}

Expand All @@ -47,9 +47,9 @@ func (m *mockRunner) listLanguageTests(language string) ([]types.SecurityTest, e
return m.languageTests, nil
}

func (m *mockRunner) newScan(RID, URL, branch, securityTestName string, languageExclusions map[string]bool, dockerHost string) (*SecTestScanInfo, error) {
func (m *mockRunner) newScan(RID, URL, branch, securityTestName string, languageExclusions map[string]bool, changedFiles, dockerHost string) (*SecTestScanInfo, error) {
if m.newScanFunc != nil {
return m.newScanFunc(RID, URL, branch, securityTestName, languageExclusions, dockerHost)
return m.newScanFunc(RID, URL, branch, securityTestName, languageExclusions, changedFiles, dockerHost)
}
return &SecTestScanInfo{}, nil
}
Expand Down
13 changes: 9 additions & 4 deletions api/securitytest/securitytest.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type SecTestScanInfo struct {
Branch string
SecurityTestName string
LanguageExclusions map[string]bool
ChangedFiles string
ErrorFound error
ReqNotFound bool
WarningFound bool
Expand All @@ -66,11 +67,12 @@ type SecTestScanInfo struct {
}

// New creates a new huskyCI scan based given RID, URL, Branch and a securityTest name and returns an error.
func (scanInfo *SecTestScanInfo) New(RID, URL, branch, securityTestName string, languageExclusions map[string]bool, dockerHost string) error {
func (scanInfo *SecTestScanInfo) New(RID, URL, branch, securityTestName string, languageExclusions map[string]bool, changedFiles, dockerHost string) error {
scanInfo.RID = RID
scanInfo.URL = URL
scanInfo.Branch = branch
scanInfo.LanguageExclusions = languageExclusions
scanInfo.ChangedFiles = changedFiles
scanInfo.SecurityTestName = securityTestName
scanInfo.DockerHost = dockerHost

Expand Down Expand Up @@ -119,7 +121,7 @@ func (scanInfo *SecTestScanInfo) Start() error {
func (scanInfo *SecTestScanInfo) dockerRun(timeOutInSeconds int) error {
image := scanInfo.Container.SecurityTest.Image
imageTag := scanInfo.Container.SecurityTest.ImageTag
cmd := util.HandleCmd(scanInfo.URL, scanInfo.Branch, scanInfo.Container.SecurityTest.Cmd)
cmd := util.HandleCmd(scanInfo.URL, scanInfo.Branch, scanInfo.Container.SecurityTest.Cmd, scanInfo.ChangedFiles)
cmd = util.HandleGitURLSubstitution(cmd)
finalCMD := util.HandlePrivateSSHKey(cmd)
CID, cOutput, err := huskydocker.DockerRun(image, imageTag, finalCMD, scanInfo.DockerHost, timeOutInSeconds)
Expand All @@ -134,7 +136,7 @@ func (scanInfo *SecTestScanInfo) dockerRun(timeOutInSeconds int) error {
func (scanInfo *SecTestScanInfo) kubeRun(timeOutInSeconds int) error {
image := scanInfo.Container.SecurityTest.Image
imageTag := scanInfo.Container.SecurityTest.ImageTag
cmd := util.HandleCmd(scanInfo.URL, scanInfo.Branch, scanInfo.Container.SecurityTest.Cmd)
cmd := util.HandleCmd(scanInfo.URL, scanInfo.Branch, scanInfo.Container.SecurityTest.Cmd, scanInfo.ChangedFiles)
cmd = util.HandleGitURLSubstitution(cmd)
finalCMD := util.HandlePrivateSSHKey(cmd)
podSchedulingTimeoutInSeconds := apiContext.APIConfiguration.KubernetesConfig.PodSchedulingTimeout
Expand All @@ -149,11 +151,14 @@ func (scanInfo *SecTestScanInfo) kubeRun(timeOutInSeconds int) error {

func (scanInfo *SecTestScanInfo) analyze() error {
errorCloning := strings.Contains(scanInfo.Container.COutput, "ERROR_CLONING")
if errorCloning {
errorSparseCheckout := strings.Contains(scanInfo.Container.COutput, "ERROR_SPARSE_CHECKOUT")
if errorCloning || errorSparseCheckout {
hint := extractGitCloneFailureHint(scanInfo.Container.COutput)
var errorMsg error
if hint != "" {
errorMsg = fmt.Errorf("error cloning: %s", hint)
} else if errorSparseCheckout {
errorMsg = errors.New("error during sparse checkout")
} else {
errorMsg = errors.New("error cloning")
}
Expand Down
1 change: 1 addition & 0 deletions api/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Repository struct {
Branch string `json:"repositoryBranch"`
LanguageExclusions map[string]bool `json:"languageExclusions"`
CreatedAt time.Time `bson:"createdAt" json:"createdAt"`
ChangedFiles string `json:"changedFiles"`
}

// SecurityTest is the struct that stores all data from the security tests to be executed.
Expand Down
33 changes: 33 additions & 0 deletions api/util/changedfiles_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package util

import "testing"

func TestCheckMaliciousChangedFiles_Empty(t *testing.T) {
if err := CheckMaliciousChangedFiles(""); err != nil {
t.Errorf("expected nil for empty string, got: %v", err)
}
}

func TestCheckMaliciousChangedFiles_Valid(t *testing.T) {
valid := "src/main.go\nsrc/utils.go\nREADME.md\npath/to/file_test.go"
if err := CheckMaliciousChangedFiles(valid); err != nil {
t.Errorf("expected nil for valid paths, got: %v", err)
}
}

func TestCheckMaliciousChangedFiles_ShellInjection(t *testing.T) {
malicious := []string{
"$(whoami)",
"`id`",
"file; rm -rf /",
"file | cat /etc/passwd",
"file&",
}

for _, m := range malicious {
err := CheckMaliciousChangedFiles(m)
if err == nil {
t.Errorf("expected error for malicious input %q, got nil", m)
}
}
}
Loading
Loading