diff --git a/README.md b/README.md index 2ab555ce..c15bb4fd 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ It performs static security analysis across multiple languages and frameworks: |----------|-------| | Python | [Bandit][Bandit], [Safety][Safety] | | Ruby | [Brakeman][Brakeman] | -| JavaScript | [Npm Audit][NpmAudit], [Yarn Audit][YarnAudit] | +| JavaScript | [Npm Audit][NpmAudit], [Yarn Audit][YarnAudit], [Pnpm Audit][PnpmAudit] | | Go | [Gosec][Gosec] | | Java | [SpotBugs][SpotBugs] + [Find Sec Bugs][FindSec] | | HCL | [TFSec][TFSec] | @@ -38,7 +38,7 @@ huskyci-client (runs inside code-analysis runner pod) huskyci-api (Kubernetes deployment, creates scanner pods) | v -Scanner pods (enry, bandit, gosec, gitleaks, npmaudit, etc.) +Scanner pods (enry, bandit, gosec, gitleaks, npmaudit, yarnaudit, pnpmaudit, etc.) | v Results collected, returned to client, reported to SonarQube @@ -135,6 +135,7 @@ Each security test can be disabled at runtime by setting its corresponding envir | `HUSKYCI_DISABLE_GOSEC` | Disable Go Gosec | | `HUSKYCI_DISABLE_NPMAUDIT` | Disable JavaScript Npm Audit | | `HUSKYCI_DISABLE_YARNAUDIT` | Disable JavaScript Yarn Audit | +| `HUSKYCI_DISABLE_PNPMAUDIT` | Disable JavaScript Pnpm Audit | | `HUSKYCI_DISABLE_BRAKEMAN` | Disable Ruby Brakeman | | `HUSKYCI_DISABLE_SPOTBUGS` | Disable Java SpotBugs | | `HUSKYCI_DISABLE_TFSEC` | Disable HCL TFSec | @@ -196,6 +197,7 @@ huskyCI is licensed under the [BSD 3-Clause License](LICENSE.md). [Gosec]: https://github.com/securego/gosec [NpmAudit]: https://docs.npmjs.com/cli/audit [YarnAudit]: https://yarnpkg.com/lang/en/docs/cli/audit/ +[PnpmAudit]: https://pnpm.io/cli/audit [Gitleaks]: https://github.com/gitleaks/gitleaks [SpotBugs]: https://spotbugs.github.io [FindSec]: https://find-sec-bugs.github.io diff --git a/api/config.yaml b/api/config.yaml index afb8ee74..721b116c 100644 --- a/api/config.yaml +++ b/api/config.yaml @@ -225,7 +225,7 @@ npmaudit: cat /tmp/errorNpmaudit fi else - if [ ! -f yarn.lock ]; then + if [ ! -f yarn.lock ] && [ ! -f pnpm-lock.yaml ]; then echo 'ERROR_PACKAGE_LOCK_NOT_FOUND' fi fi @@ -261,7 +261,7 @@ yarnaudit: cat /tmp/errorYarnAudit fi else - if [ ! -f package-lock.json ]; then + if [ ! -f package-lock.json ] && [ ! -f pnpm-lock.yaml ]; then echo 'ERROR_YARN_LOCK_NOT_FOUND' fi fi @@ -274,6 +274,45 @@ yarnaudit: default: true timeOutInSeconds: 360 +pnpmaudit: + name: pnpmaudit + image: huskyci/pnpmaudit + imageTag: "11.5.2" + cmd: |+ + mkdir -p ~/.ssh && + echo '%GIT_PRIVATE_SSH_KEY%' > ~/.ssh/huskyci_id_rsa && + chmod 600 ~/.ssh/huskyci_id_rsa && + echo "IdentityFile ~/.ssh/huskyci_id_rsa" >> /etc/ssh/ssh_config && + echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config && + GIT_TERMINAL_PROMPT=0 git clone -b "%GIT_BRANCH%" --single-branch --depth 1 "%GIT_REPO%" code --quiet 2> /tmp/errorGitClonePnpmAudit + if [ $? -eq 0 ]; then + cd code + if [ -f .npmrc ]; then + rm -f .npmrc + fi + if [ -f pnpm-lock.yaml ]; then + pnpm audit --json --prod > /tmp/results.json 2> /tmp/errorPnpmaudit + RC=$? + if [ $RC -eq 0 ] || [ $RC -eq 1 ]; then + cat /tmp/results.json + else + echo 'ERROR_RUNNING_PNPM_AUDIT' + cat /tmp/errorPnpmaudit + fi + else + if [ ! -f package-lock.json ] && [ ! -f yarn.lock ]; then + echo 'ERROR_PNPM_LOCK_NOT_FOUND' + fi + fi + else + echo "ERROR_CLONING" + cat /tmp/errorGitClonePnpmAudit + fi + type: Language + language: JavaScript + default: true + timeOutInSeconds: 360 + spotbugs: name: spotbugs image: huskyci/spotbugs diff --git a/api/context/context.go b/api/context/context.go index bd1c3e2d..9b5120b4 100644 --- a/api/context/context.go +++ b/api/context/context.go @@ -99,6 +99,7 @@ type APIConfig struct { BrakemanSecurityTest *types.SecurityTest NpmAuditSecurityTest *types.SecurityTest YarnAuditSecurityTest *types.SecurityTest + PnpmAuditSecurityTest *types.SecurityTest SpotBugsSecurityTest *types.SecurityTest GitleaksSecurityTest *types.SecurityTest SafetySecurityTest *types.SecurityTest @@ -155,6 +156,7 @@ func (dF DefaultConfig) SetOnceConfig() { BrakemanSecurityTest: dF.getSecurityTestConfig("brakeman"), NpmAuditSecurityTest: dF.getSecurityTestConfig("npmaudit"), YarnAuditSecurityTest: dF.getSecurityTestConfig("yarnaudit"), + PnpmAuditSecurityTest: dF.getSecurityTestConfig("pnpmaudit"), SpotBugsSecurityTest: dF.getSecurityTestConfig("spotbugs"), GitleaksSecurityTest: dF.getSecurityTestConfig("gitleaks"), SafetySecurityTest: dF.getSecurityTestConfig("safety"), diff --git a/api/context/context_test.go b/api/context/context_test.go index d30d1f61..c538436d 100644 --- a/api/context/context_test.go +++ b/api/context/context_test.go @@ -412,6 +412,16 @@ var _ = Describe("Context", func() { Default: fakeCaller.expectedBoolFromConfig, TimeOutInSeconds: fakeCaller.expectedIntFromConfig, }, + PnpmAuditSecurityTest: &types.SecurityTest{ + Name: fakeCaller.expectedStringFromConfig, + Image: fakeCaller.expectedStringFromConfig, + ImageTag: fakeCaller.expectedStringFromConfig, + Cmd: fakeCaller.expectedStringFromConfig, + Type: fakeCaller.expectedStringFromConfig, + Language: fakeCaller.expectedStringFromConfig, + Default: fakeCaller.expectedBoolFromConfig, + TimeOutInSeconds: fakeCaller.expectedIntFromConfig, + }, SafetySecurityTest: &types.SecurityTest{ Name: fakeCaller.expectedStringFromConfig, Image: fakeCaller.expectedStringFromConfig, diff --git a/api/securitytest/pnpmaudit.go b/api/securitytest/pnpmaudit.go new file mode 100644 index 00000000..cb24c082 --- /dev/null +++ b/api/securitytest/pnpmaudit.go @@ -0,0 +1,144 @@ +// Copyright 2019 Globo.com authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package securitytest + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/githubanotaai/huskyci-api/api/log" + "github.com/githubanotaai/huskyci-api/api/types" + "github.com/githubanotaai/huskyci-api/api/util" +) + +// PnpmAuditOutput is the struct that stores all pnpm audit output. +type PnpmAuditOutput struct { + Advisories map[string]PnpmAdvisory `json:"advisories"` + Metadata PnpmMetadata `json:"metadata"` +} + +// PnpmAdvisory is a single advisory from pnpm audit. +type PnpmAdvisory struct { + ID int `json:"id"` + Title string `json:"title"` + ModuleName string `json:"module_name"` + VulnerableVersions string `json:"vulnerable_versions"` + PatchedVersions string `json:"patched_versions"` + Severity string `json:"severity"` + CWE string `json:"cwe"` + GithubAdvisoryID string `json:"github_advisory_id"` + URL string `json:"url"` + Findings []PnpmFinding `json:"findings"` +} + +// PnpmFinding represents a specific finding of a vulnerable dependency. +type PnpmFinding struct { + Version string `json:"version"` + Paths []string `json:"paths"` + Dev bool `json:"dev"` + Optional bool `json:"optional"` + Bundled bool `json:"bundled"` +} + +// PnpmMetadata is the struct that holds vulnerabilities summary. +type PnpmMetadata struct { + Vulnerabilities PnpmVulnerabilitiesSummary `json:"vulnerabilities"` +} + +// PnpmVulnerabilitiesSummary is the struct that has all types of possible vulnerabilities. +type PnpmVulnerabilitiesSummary struct { + Info int `json:"info"` + Low int `json:"low"` + Moderate int `json:"moderate"` + High int `json:"high"` + Critical int `json:"critical"` +} + +func analyzePnpmaudit(pnpmAuditScan *SecTestScanInfo) error { + + pnpmAuditOutput := PnpmAuditOutput{} + pnpmAuditScan.FinalOutput = pnpmAuditOutput + + // if pnpm-lock was not found, a warning will be generated as a low vuln + pnpmLockNotFound := strings.Contains(pnpmAuditScan.Container.COutput, "ERROR_PNPM_LOCK_NOT_FOUND") + if pnpmLockNotFound { + pnpmAuditScan.PnpmLockNotFound = true + pnpmAuditScan.preparePnpmAuditVulns() + pnpmAuditScan.prepareContainerAfterScan() + return nil + } + + // nil cOutput states that no Issues were found (another lockfile present, silent skip). + if pnpmAuditScan.Container.COutput == "" { + pnpmAuditScan.prepareContainerAfterScan() + return nil + } + + // Unmarshal rawOutput into finalOutput. + if err := json.Unmarshal([]byte(pnpmAuditScan.Container.COutput), &pnpmAuditOutput); err != nil { + log.Error("analyzePnpmaudit", "PNPMAUDIT", 1014, pnpmAuditScan.Container.COutput, err) + pnpmAuditScan.ErrorFound = util.HandleScanError(pnpmAuditScan.Container.COutput, err) + pnpmAuditScan.prepareContainerAfterScan() + return pnpmAuditScan.ErrorFound + } + pnpmAuditScan.FinalOutput = pnpmAuditOutput + + pnpmAuditScan.preparePnpmAuditVulns() + pnpmAuditScan.prepareContainerAfterScan() + return nil +} + +func (pnpmAuditScan *SecTestScanInfo) preparePnpmAuditVulns() { + + huskyCIPnpmauditResults := types.HuskyCISecurityTestOutput{} + + if pnpmAuditScan.PnpmLockNotFound { + pnpmauditVuln := types.HuskyCIVulnerability{} + pnpmauditVuln.Language = "JavaScript" + pnpmauditVuln.SecurityTool = "PnpmAudit" + pnpmauditVuln.Severity = "low" + pnpmauditVuln.Title = "No pnpm-lock.yaml found." + pnpmauditVuln.Details = "It looks like your project doesn't have a pnpm-lock.yaml file. If you use pnpm to handle your dependencies, it would be a good idea to commit it so huskyCI can check for vulnerabilities." + + pnpmAuditScan.Vulnerabilities.LowVulns = append(pnpmAuditScan.Vulnerabilities.LowVulns, pnpmauditVuln) + return + } + + pnpmAuditOutput := pnpmAuditScan.FinalOutput.(PnpmAuditOutput) + + for _, advisory := range pnpmAuditOutput.Advisories { + pnpmauditVuln := types.HuskyCIVulnerability{} + pnpmauditVuln.Language = "JavaScript" + pnpmauditVuln.SecurityTool = "PnpmAudit" + pnpmauditVuln.File = "pnpm-lock.yaml" + pnpmauditVuln.Title = fmt.Sprintf("Vulnerable Dependency: %s %s (%s)", advisory.ModuleName, advisory.VulnerableVersions, advisory.Title) + pnpmauditVuln.VunerableBelow = advisory.VulnerableVersions + pnpmauditVuln.Code = advisory.ModuleName + pnpmauditVuln.Details = fmt.Sprintf("GHSA: %s\nCWE: %s\nURL: %s\nPatched: %s", advisory.GithubAdvisoryID, advisory.CWE, advisory.URL, advisory.PatchedVersions) + + for i, finding := range advisory.Findings { + pnpmauditVuln.Version += fmt.Sprintf("Finding %d:\n", i) + pnpmauditVuln.Version += fmt.Sprintf(" Version: %s\n", finding.Version) + for _, path := range finding.Paths { + pnpmauditVuln.Version += fmt.Sprintf(" Path: %s\n", path) + } + } + + switch advisory.Severity { + case "info", "low": + pnpmauditVuln.Severity = "low" + huskyCIPnpmauditResults.LowVulns = append(huskyCIPnpmauditResults.LowVulns, pnpmauditVuln) + case "moderate": + pnpmauditVuln.Severity = "medium" + huskyCIPnpmauditResults.MediumVulns = append(huskyCIPnpmauditResults.MediumVulns, pnpmauditVuln) + case "high", "critical": + pnpmauditVuln.Severity = "high" + huskyCIPnpmauditResults.HighVulns = append(huskyCIPnpmauditResults.HighVulns, pnpmauditVuln) + } + } + + pnpmAuditScan.Vulnerabilities = huskyCIPnpmauditResults +} diff --git a/api/securitytest/pnpmaudit_test.go b/api/securitytest/pnpmaudit_test.go new file mode 100644 index 00000000..a5020c84 --- /dev/null +++ b/api/securitytest/pnpmaudit_test.go @@ -0,0 +1,144 @@ +package securitytest + +import ( + "strings" + "testing" + + "github.com/githubanotaai/huskyci-api/api/types" +) + +func TestAnalyzePnpmaudit(t *testing.T) { + // Real pnpm audit --json --prod output with lodash@4.17.20 + pnpmOutput := `{"advisories":{"1106913":{"findings":[{"version":"4.17.20","paths":[".>lodash"],"dev":false,"optional":false,"bundled":false}],"id":1106913,"title":"Command Injection in lodash","module_name":"lodash","vulnerable_versions":"<4.17.21","patched_versions":">=4.17.21","severity":"high","cwe":"CWE-77, CWE-94","github_advisory_id":"GHSA-35jh-r3h4-6jhm","url":"https://github.com/advisories/GHSA-35jh-r3h4-6jhm"}},"metadata":{"vulnerabilities":{"info":0,"low":0,"moderate":0,"high":1,"critical":0},"dependencies":1,"devDependencies":0,"optionalDependencies":0,"totalDependencies":1}}` + + scan := &SecTestScanInfo{ + Container: types.Container{ + COutput: pnpmOutput, + }, + } + + err := analyzePnpmaudit(scan) + if err != nil { + t.Fatalf("analyzePnpmaudit returned error: %v", err) + } + + if len(scan.Vulnerabilities.HighVulns) != 1 { + t.Errorf("expected 1 high vuln, got %d", len(scan.Vulnerabilities.HighVulns)) + } + + vuln := scan.Vulnerabilities.HighVulns[0] + if vuln.SecurityTool != "PnpmAudit" { + t.Errorf("expected SecurityTool PnpmAudit, got %s", vuln.SecurityTool) + } + if vuln.Severity != "high" { + t.Errorf("expected severity high, got %s", vuln.Severity) + } + if vuln.File != "pnpm-lock.yaml" { + t.Errorf("expected file pnpm-lock.yaml, got %s", vuln.File) + } + if vuln.Language != "JavaScript" { + t.Errorf("expected language JavaScript, got %s", vuln.Language) + } +} + +func TestAnalyzePnpmauditEmpty(t *testing.T) { + // Empty COutput (another lockfile present, silent skip — e.g. npm repo) + scan := &SecTestScanInfo{ + Container: types.Container{ + COutput: "", + }, + } + + err := analyzePnpmaudit(scan) + if err != nil { + t.Fatalf("analyzePnpmaudit returned error for empty output: %v", err) + } + + if len(scan.Vulnerabilities.HighVulns) != 0 { + t.Errorf("expected 0 vulns for empty output, got high=%d", len(scan.Vulnerabilities.HighVulns)) + } + if len(scan.Vulnerabilities.MediumVulns) != 0 { + t.Errorf("expected 0 vulns for empty output, got medium=%d", len(scan.Vulnerabilities.MediumVulns)) + } + if len(scan.Vulnerabilities.LowVulns) != 0 { + t.Errorf("expected 0 vulns for empty output, got low=%d", len(scan.Vulnerabilities.LowVulns)) + } +} + +func TestAnalyzePnpmauditModerateSeverity(t *testing.T) { + // pnpm audit output with moderate severity + pnpmOutput := `{"advisories":{"1108258":{"findings":[{"version":"4.17.20","paths":[".>lodash"],"dev":false,"optional":false,"bundled":false}],"id":1108258,"title":"Regular Expression Denial of Service (ReDoS) in lodash","module_name":"lodash","vulnerable_versions":">=4.0.0 <4.17.21","patched_versions":">=4.17.21","severity":"moderate","cwe":"CWE-400, CWE-1333","github_advisory_id":"GHSA-29mw-wpgm-hmr9","url":"https://github.com/advisories/GHSA-29mw-wpgm-hmr9"}},"metadata":{"vulnerabilities":{"info":0,"low":0,"moderate":1,"high":0,"critical":0},"dependencies":1,"devDependencies":0,"optionalDependencies":0,"totalDependencies":1}}` + + scan := &SecTestScanInfo{ + Container: types.Container{ + COutput: pnpmOutput, + }, + } + + err := analyzePnpmaudit(scan) + if err != nil { + t.Fatalf("analyzePnpmaudit returned error: %v", err) + } + + if len(scan.Vulnerabilities.MediumVulns) != 1 { + t.Errorf("expected 1 medium vuln, got %d", len(scan.Vulnerabilities.MediumVulns)) + } + + vuln := scan.Vulnerabilities.MediumVulns[0] + if vuln.Severity != "medium" { + t.Errorf("expected severity medium, got %s", vuln.Severity) + } +} + +func TestAnalyzePnpmauditMultipleAdvisories(t *testing.T) { + // Two advisories: one high, one moderate + pnpmOutput := `{"advisories":{"1106913":{"findings":[{"version":"4.17.20","paths":[".>lodash"],"dev":false,"optional":false,"bundled":false}],"id":1106913,"title":"Command Injection in lodash","module_name":"lodash","vulnerable_versions":"<4.17.21","patched_versions":">=4.17.21","severity":"high","cwe":"CWE-77, CWE-94","github_advisory_id":"GHSA-35jh-r3h4-6jhm","url":"https://github.com/advisories/GHSA-35jh-r3h4-6jhm"},"1108258":{"findings":[{"version":"4.17.20","paths":[".>lodash"],"dev":false,"optional":false,"bundled":false}],"id":1108258,"title":"ReDoS in lodash","module_name":"lodash","vulnerable_versions":">=4.0.0 <4.17.21","patched_versions":">=4.17.21","severity":"moderate","cwe":"CWE-400","github_advisory_id":"GHSA-29mw-wpgm-hmr9","url":"https://github.com/advisories/GHSA-29mw-wpgm-hmr9"}},"metadata":{"vulnerabilities":{"info":0,"low":0,"moderate":1,"high":1,"critical":0},"dependencies":1}}` + + scan := &SecTestScanInfo{ + Container: types.Container{ + COutput: pnpmOutput, + }, + } + + err := analyzePnpmaudit(scan) + if err != nil { + t.Fatalf("analyzePnpmaudit returned error: %v", err) + } + + if len(scan.Vulnerabilities.HighVulns) != 1 { + t.Errorf("expected 1 high vuln, got %d", len(scan.Vulnerabilities.HighVulns)) + } + if len(scan.Vulnerabilities.MediumVulns) != 1 { + t.Errorf("expected 1 medium vuln, got %d", len(scan.Vulnerabilities.MediumVulns)) + } +} + +func TestAnalyzePnpmauditLockfileNotFound(t *testing.T) { + // ERROR_PNPM_LOCK_NOT_FOUND — no lockfile at all in the repo + scan := &SecTestScanInfo{ + Container: types.Container{ + COutput: "ERROR_PNPM_LOCK_NOT_FOUND", + }, + } + + err := analyzePnpmaudit(scan) + if err != nil { + t.Fatalf("analyzePnpmaudit returned error: %v", err) + } + + if !scan.PnpmLockNotFound { + t.Error("expected PnpmLockNotFound to be true") + } + + if len(scan.Vulnerabilities.LowVulns) != 1 { + t.Errorf("expected 1 low vuln for lockfile not found, got %d", len(scan.Vulnerabilities.LowVulns)) + } + + vuln := scan.Vulnerabilities.LowVulns[0] + if vuln.Severity != "low" { + t.Errorf("expected severity low, got %s", vuln.Severity) + } + if !strings.Contains(vuln.Title, "pnpm-lock.yaml") { + t.Errorf("expected title to mention pnpm-lock.yaml, got: %s", vuln.Title) + } +} diff --git a/api/securitytest/run.go b/api/securitytest/run.go index 16c2b1c6..0069e552 100644 --- a/api/securitytest/run.go +++ b/api/securitytest/run.go @@ -24,6 +24,12 @@ type RunAllInfo struct { FinalResult string ErrorFound error HuskyCIResults types.HuskyCIResults + + // Lockfile-not-found tracking for coalescing into a single HIGH vuln + // when no JS package manager lockfile exists in the repo. + NpmLockNotFound bool + YarnLockNotFound bool + PnpmLockNotFound bool } const bandit = "bandit" @@ -32,6 +38,7 @@ const safety = "safety" const gosec = "gosec" const npmaudit = "npmaudit" const yarnaudit = "yarnaudit" +const pnpmaudit = "pnpmaudit" const spotbugs = "spotbugs" const gitleaks = "gitleaks" const tfsec = "tfsec" @@ -158,6 +165,16 @@ func (results *RunAllInfo) runLanguageScans(ctx context.Context, enryScan SecTes results.mu.Lock() results.Containers = append(results.Containers, scan.Container) results.setVulns(*scan) + // Propagate lockfile-not-found flags for coalescing + if scan.PackageNotFound { + results.NpmLockNotFound = true + } + if scan.YarnLockNotFound { + results.YarnLockNotFound = true + } + if scan.PnpmLockNotFound { + results.PnpmLockNotFound = true + } results.mu.Unlock() return nil }) @@ -181,6 +198,8 @@ func (results *RunAllInfo) vulnOutput(securityTestName string) *types.HuskyCISec return &results.HuskyCIResults.JavaScriptResults.HuskyCINpmAuditOutput case yarnaudit: return &results.HuskyCIResults.JavaScriptResults.HuskyCIYarnAuditOutput + case pnpmaudit: + return &results.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput case spotbugs: return &results.HuskyCIResults.JavaResults.HuskyCISpotBugsOutput case gitleaks: @@ -229,6 +248,10 @@ func (results *RunAllInfo) setToAnalysis() { return } + // Coalesce: when all three JS package manager lockfiles are missing, + // replace the three individual LOW vulns with a single HIGH vuln. + results.coalesceJsLockfileErrors() + jsWarningFlag := false for _, container := range results.Containers { @@ -266,6 +289,35 @@ func getAllDefaultSecurityTests(typeOf, language string) ([]types.SecurityTest, return securityTests, nil } +// coalesceJsLockfileErrors checks if all three JS package manager lockfiles +// are missing from the repo. When all three scanners report lockfile-not-found, +// the three individual LOW vulns are replaced with a single HIGH vuln on the +// PnpmAudit output. This prevents noise and surfaces the real problem: no +// lockfile means HuskyCI cannot audit the repo's dependencies at all. +func (results *RunAllInfo) coalesceJsLockfileErrors() { + if !results.NpmLockNotFound || !results.YarnLockNotFound || !results.PnpmLockNotFound { + return + } + + // Clear the individual LOW vulns from all three outputs. + results.HuskyCIResults.JavaScriptResults.HuskyCINpmAuditOutput.LowVulns = nil + results.HuskyCIResults.JavaScriptResults.HuskyCIYarnAuditOutput.LowVulns = nil + results.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput.LowVulns = nil + + // Emit a single HIGH vuln on the PnpmAudit output. + highVuln := types.HuskyCIVulnerability{ + Language: "JavaScript", + SecurityTool: "PnpmAudit", + Severity: "high", + Title: "No lockfile found in the repository.", + Details: "It looks like your project doesn't have a package-lock.json, yarn.lock, or pnpm-lock.yaml file. huskyCI needs a lockfile to audit your dependencies for vulnerabilities. Please commit the lockfile of the package manager you use (npm, yarn, or pnpm).", + } + results.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput.HighVulns = append( + results.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput.HighVulns, + highVuln, + ) +} + func (results *RunAllInfo) setFinalResult() { // Logic to determine the final result based on scan results. // For example, if all scans passed, set FinalResult to "passed". diff --git a/api/securitytest/run_test.go b/api/securitytest/run_test.go index 8f6dc57b..0c28a42a 100644 --- a/api/securitytest/run_test.go +++ b/api/securitytest/run_test.go @@ -249,3 +249,69 @@ func TestStart_ConcurrentWritesDataRace(t *testing.T) { t.Errorf("expected at least 2 containers, got %d", len(results.Containers)) } } + +func TestCoalesceJsLockfileErrors_AllThreeMissing(t *testing.T) { + results := &RunAllInfo{ + NpmLockNotFound: true, + YarnLockNotFound: true, + PnpmLockNotFound: true, + } + // Pre-populate with simulated low vulns from all three scanners + results.HuskyCIResults.JavaScriptResults.HuskyCINpmAuditOutput.LowVulns = []types.HuskyCIVulnerability{ + {Title: "No package-lock.json found.", Severity: "low"}, + } + results.HuskyCIResults.JavaScriptResults.HuskyCIYarnAuditOutput.LowVulns = []types.HuskyCIVulnerability{ + {Title: "No yarn.lock found.", Severity: "low"}, + } + results.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput.LowVulns = []types.HuskyCIVulnerability{ + {Title: "No pnpm-lock.yaml found.", Severity: "low"}, + } + + results.coalesceJsLockfileErrors() + + // All low vulns should be cleared + if len(results.HuskyCIResults.JavaScriptResults.HuskyCINpmAuditOutput.LowVulns) != 0 { + t.Error("expected npm low vulns to be cleared") + } + if len(results.HuskyCIResults.JavaScriptResults.HuskyCIYarnAuditOutput.LowVulns) != 0 { + t.Error("expected yarn low vulns to be cleared") + } + if len(results.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput.LowVulns) != 0 { + t.Error("expected pnpm low vulns to be cleared") + } + + // A single HIGH vuln should be on the pnpm output + highVulns := results.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput.HighVulns + if len(highVulns) != 1 { + t.Fatalf("expected 1 high vuln, got %d", len(highVulns)) + } + if highVulns[0].Severity != "high" { + t.Errorf("expected severity high, got %s", highVulns[0].Severity) + } + if highVulns[0].SecurityTool != "PnpmAudit" { + t.Errorf("expected SecurityTool PnpmAudit, got %s", highVulns[0].SecurityTool) + } +} + +func TestCoalesceJsLockfileErrors_NotAllMissing(t *testing.T) { + // Only npm missing — should NOT coalesce + results := &RunAllInfo{ + NpmLockNotFound: true, + YarnLockNotFound: false, + PnpmLockNotFound: false, + } + results.HuskyCIResults.JavaScriptResults.HuskyCINpmAuditOutput.LowVulns = []types.HuskyCIVulnerability{ + {Title: "No package-lock.json found.", Severity: "low"}, + } + + results.coalesceJsLockfileErrors() + + // Low vuln should still be there (no coalescing) + if len(results.HuskyCIResults.JavaScriptResults.HuskyCINpmAuditOutput.LowVulns) != 1 { + t.Error("expected npm low vuln to remain when not all three missing") + } + // No high vuln should have been added + if len(results.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput.HighVulns) != 0 { + t.Error("expected no high vuln when not all three missing") + } +} diff --git a/api/securitytest/securitytest.go b/api/securitytest/securitytest.go index 50a32915..7df3ff4b 100644 --- a/api/securitytest/securitytest.go +++ b/api/securitytest/securitytest.go @@ -22,6 +22,7 @@ var securityTestAnalyze = map[string]func(scanInfo *SecTestScanInfo) error{ "gitauthors": analyzeGitAuthors, "gosec": analyzeGosec, "npmaudit": analyzeNpmaudit, + "pnpmaudit": analyzePnpmaudit, "yarnaudit": analyzeYarnaudit, "spotbugs": analyzeSpotBugs, "gitleaks": analyseGitleaks, @@ -49,6 +50,7 @@ type SecTestScanInfo struct { PackageNotFound bool YarnLockNotFound bool YarnErrorRunning bool + PnpmLockNotFound bool GitleaksErrorRunning bool GitleaksTimeout bool SecurityCodeScanErrorRunning bool diff --git a/api/types/types.go b/api/types/types.go index 40e49d86..65ead061 100644 --- a/api/types/types.go +++ b/api/types/types.go @@ -116,8 +116,9 @@ type PythonResults struct { // JavaScriptResults represents all JavaScript security tests results. type JavaScriptResults struct { - HuskyCINpmAuditOutput HuskyCISecurityTestOutput `bson:"npmauditoutput,omitempty" json:"npmauditoutput,omitempty"` - HuskyCIYarnAuditOutput HuskyCISecurityTestOutput `bson:"yarnauditoutput,omitempty" json:"yarnauditoutput,omitempty"` + HuskyCINpmAuditOutput HuskyCISecurityTestOutput `bson:"npmauditoutput,omitempty" json:"npmauditoutput,omitempty"` + HuskyCIYarnAuditOutput HuskyCISecurityTestOutput `bson:"yarnauditoutput,omitempty" json:"yarnauditoutput,omitempty"` + HuskyCIPnpmAuditOutput HuskyCISecurityTestOutput `bson:"pnpmauditoutput,omitempty" json:"pnpmauditoutput,omitempty"` } // JavaResults represents all Java security tests results. diff --git a/api/util/api/api.go b/api/util/api/api.go index 0dbc6b92..1f973cee 100644 --- a/api/util/api/api.go +++ b/api/util/api/api.go @@ -159,7 +159,7 @@ func (cH *CheckUtils) checkDB(configAPI *apiContext.APIConfig) error { func (cH *CheckUtils) checkEachSecurityTest(configAPI *apiContext.APIConfig) error { securityTests := []string{ "enry", "gitauthors", "gosec", "brakeman", "bandit", - "npmaudit", "yarnaudit", "spotbugs", "gitleaks", "safety", + "npmaudit", "yarnaudit", "pnpmaudit", "spotbugs", "gitleaks", "safety", "tfsec", "securitycodescan", "wizcli_secrets", "wizcli_iac_sast", "wizcli_vulns", } for _, securityTest := range securityTests { @@ -218,6 +218,8 @@ func checkSecurityTest(securityTestName string, configAPI *apiContext.APIConfig) securityTestConfig = *configAPI.NpmAuditSecurityTest case "yarnaudit": securityTestConfig = *configAPI.YarnAuditSecurityTest + case "pnpmaudit": + securityTestConfig = *configAPI.PnpmAuditSecurityTest case "spotbugs": securityTestConfig = *configAPI.SpotBugsSecurityTest case "gitleaks": diff --git a/cli/analysis/analysis.go b/cli/analysis/analysis.go index 13d2f90c..c1bb2d7e 100644 --- a/cli/analysis/analysis.go +++ b/cli/analysis/analysis.go @@ -172,7 +172,7 @@ func (a *Analysis) getAvailableSecurityTests(languages []string) map[string][]st case "Ruby": list[language] = []string{"huskyci/brakeman"} case "JavaScript": - list[language] = []string{"huskyci/npmaudit", "huskyci/yarnaudit"} + list[language] = []string{"huskyci/npmaudit", "huskyci/yarnaudit", "huskyci/pnpmaudit"} case "Java": list[language] = []string{"huskyci/spotbugs"} case "HCL": diff --git a/client/analysis/output.go b/client/analysis/output.go index 93885c93..fa09a239 100644 --- a/client/analysis/output.go +++ b/client/analysis/output.go @@ -48,6 +48,9 @@ func printSTDOUTOutput(analysis types.Analysis) { // yarnaudit printToolGroup("JavaScript - YarnAudit", outputJSON.JavaScriptResults.HuskyCIYarnAuditOutput, printSTDOUTOutputYarnAudit) + // pnpmaudit + printToolGroup("JavaScript - PnpmAudit", outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput, printSTDOUTOutputPnpmAudit) + // gitleaks printToolGroup("Generic - Gitleaks", outputJSON.GenericResults.HuskyCIGitleaksOutput, printSTDOUTOutputGitleaks) @@ -162,6 +165,17 @@ func prepareAllSummary(analysis types.Analysis) { outputJSON.Summary.YarnAuditSummary.FoundVuln = true } + // PnpmAudit summary + outputJSON.Summary.PnpmAuditSummary.LowVuln = len(outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput.LowVulns) + outputJSON.Summary.PnpmAuditSummary.MediumVuln = len(outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput.MediumVulns) + outputJSON.Summary.PnpmAuditSummary.HighVuln = len(outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput.HighVulns) + if len(outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput.LowVulns) > 0 || len(outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput.NoSecVulns) > 0 { + outputJSON.Summary.PnpmAuditSummary.FoundInfo = true + } + if len(outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput.MediumVulns) > 0 || len(outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput.HighVulns) > 0 { + outputJSON.Summary.PnpmAuditSummary.FoundVuln = true + } + // SpotBugs summary outputJSON.Summary.SpotBugsSummary.LowVuln = len(outputJSON.JavaResults.HuskyCISpotBugsOutput.LowVulns) outputJSON.Summary.SpotBugsSummary.MediumVuln = len(outputJSON.JavaResults.HuskyCISpotBugsOutput.MediumVulns) @@ -537,6 +551,20 @@ func printSTDOUTOutputYarnAudit(issues []types.HuskyCIVulnerability) { } } +func printSTDOUTOutputPnpmAudit(issues []types.HuskyCIVulnerability) { + for _, issue := range issues { + fmt.Println() + fmt.Printf("[HUSKYCI][!] Title: %s\n", issue.Title) + fmt.Printf("[HUSKYCI][!] Language: %s\n", issue.Language) + fmt.Printf("[HUSKYCI][!] Tool: %s\n", issue.SecurityTool) + fmt.Printf("[HUSKYCI][!] Severity: %s\n", issue.Severity) + fmt.Printf("[HUSKYCI][!] Code: %s\n", issue.Code) + fmt.Printf("[HUSKYCI][!] Version: %s\n", issue.Version) + fmt.Printf("[HUSKYCI][!] Vulnerable Below: %s\n", issue.VunerableBelow) + fmt.Printf("[HUSKYCI][!] Details: %s\n", issue.Details) + } +} + func printSTDOUTOutputSpotBugs(issues []types.HuskyCIVulnerability) { for _, issue := range issues { fmt.Println() diff --git a/client/analysis/output_pnpmaudit_test.go b/client/analysis/output_pnpmaudit_test.go new file mode 100644 index 00000000..283d7a8b --- /dev/null +++ b/client/analysis/output_pnpmaudit_test.go @@ -0,0 +1,32 @@ +package analysis + +import ( + "testing" + + "github.com/githubanotaai/huskyci-api/client/types" +) + +func TestPrintSTDOUTOutputPnpmAudit(t *testing.T) { + output := types.HuskyCISecurityTestOutput{ + HighVulns: []types.HuskyCIVulnerability{ + { + Language: "JavaScript", + SecurityTool: "PnpmAudit", + Severity: "high", + File: "pnpm-lock.yaml", + Code: "lodash", + Title: "Vulnerable Dependency: lodash <4.17.21 (Command Injection)", + VunerableBelow: "<4.17.21", + Version: "Finding 0:\n Version: 4.17.20\n Path: .>lodash\n", + Details: "GHSA: GHSA-35jh-r3h4-6jhm\nCWE: CWE-77\nURL: https://...", + }, + }, + } + // Verify no panic on valid output + printSTDOUTOutputPnpmAudit(output.HighVulns) +} + +func TestPrintSTDOUTOutputPnpmAuditEmpty(t *testing.T) { + // Verify no panic on empty output + printSTDOUTOutputPnpmAudit(nil) +} diff --git a/client/integration/sonarqube/sonarqube.go b/client/integration/sonarqube/sonarqube.go index 7e15186f..21df4296 100644 --- a/client/integration/sonarqube/sonarqube.go +++ b/client/integration/sonarqube/sonarqube.go @@ -54,6 +54,11 @@ func GenerateOutputFile(analysis types.Analysis, outputPath, outputFileName stri allVulns = append(allVulns, analysis.HuskyCIResults.JavaScriptResults.HuskyCIYarnAuditOutput.MediumVulns...) allVulns = append(allVulns, analysis.HuskyCIResults.JavaScriptResults.HuskyCIYarnAuditOutput.HighVulns...) + // pnpmaudit + allVulns = append(allVulns, analysis.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput.LowVulns...) + allVulns = append(allVulns, analysis.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput.MediumVulns...) + allVulns = append(allVulns, analysis.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput.HighVulns...) + // gitleaks allVulns = append(allVulns, analysis.HuskyCIResults.GenericResults.HuskyCIGitleaksOutput.LowVulns...) allVulns = append(allVulns, analysis.HuskyCIResults.GenericResults.HuskyCIGitleaksOutput.MediumVulns...) diff --git a/client/types/pnpmaudit.go b/client/types/pnpmaudit.go new file mode 100644 index 00000000..e98ba2e8 --- /dev/null +++ b/client/types/pnpmaudit.go @@ -0,0 +1,40 @@ +// Copyright 2019 Globo.com authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package types + +// PnpmAuditOutput is the struct that stores all pnpm audit output. +type PnpmAuditOutput struct { + Advisories map[string]PnpmAdvisory `json:"advisories"` + Metadata PnpmMetadata `json:"metadata"` +} + +// PnpmAdvisory is a single advisory from pnpm audit. +type PnpmAdvisory struct { + Findings []PnpmFinding `json:"findings"` + ID int `json:"id"` + Title string `json:"title"` + ModuleName string `json:"module_name"` + VulnerableVersions string `json:"vulnerable_versions"` + Severity string `json:"severity"` +} + +// PnpmFinding represents a specific finding of a vulnerable dependency. +type PnpmFinding struct { + Version string `json:"version"` +} + +// PnpmMetadata is the struct that holds vulnerabilities summary. +type PnpmMetadata struct { + Vulnerabilities PnpmVulnerabilitiesSummary `json:"vulnerabilities"` +} + +// PnpmVulnerabilitiesSummary holds the count of vulnerabilities by severity. +type PnpmVulnerabilitiesSummary struct { + Info int `json:"info"` + Low int `json:"low"` + Moderate int `json:"moderate"` + High int `json:"high"` + Critical int `json:"critical"` +} diff --git a/client/types/types.go b/client/types/types.go index a9f8e77c..9a4df727 100644 --- a/client/types/types.go +++ b/client/types/types.go @@ -135,8 +135,9 @@ type PythonResults struct { // JavaScriptResults represents all JavaScript security tests results. type JavaScriptResults struct { - HuskyCINpmAuditOutput HuskyCISecurityTestOutput `bson:"npmauditoutput,omitempty" json:"npmauditoutput,omitempty"` - HuskyCIYarnAuditOutput HuskyCISecurityTestOutput `bson:"yarnauditoutput,omitempty" json:"yarnauditoutput,omitempty"` + HuskyCINpmAuditOutput HuskyCISecurityTestOutput `bson:"npmauditoutput,omitempty" json:"npmauditoutput,omitempty"` + HuskyCIYarnAuditOutput HuskyCISecurityTestOutput `bson:"yarnauditoutput,omitempty" json:"yarnauditoutput,omitempty"` + HuskyCIPnpmAuditOutput HuskyCISecurityTestOutput `bson:"pnpmauditoutput,omitempty" json:"pnpmauditoutput,omitempty"` } // JavaResults represents all Java security tests results. @@ -185,6 +186,7 @@ type Summary struct { SafetySummary HuskyCISummary `json:"safetysummary,omitempty"` NpmAuditSummary HuskyCISummary `json:"npmauditsummary,omitempty"` YarnAuditSummary HuskyCISummary `json:"yarnauditsummary,omitempty"` + PnpmAuditSummary HuskyCISummary `json:"pnpmauditsummary,omitempty"` BrakemanSummary HuskyCISummary `json:"brakemansummary,omitempty"` SpotBugsSummary HuskyCISummary `json:"spotbugssummary,omitempty"` GitleaksSummary HuskyCISummary `json:"gitleakssummary,omitempty"` diff --git a/deployments/dockerfiles/pnpmaudit/Dockerfile b/deployments/dockerfiles/pnpmaudit/Dockerfile new file mode 100644 index 00000000..5fd28f20 --- /dev/null +++ b/deployments/dockerfiles/pnpmaudit/Dockerfile @@ -0,0 +1,12 @@ +# Dockerfile used to create "huskyci/pnpmaudit" image + +FROM node:alpine + +RUN apk update && apk upgrade \ + && apk add --no-cache alpine-sdk bash openssh-client \ + && apk add git + +RUN npm install -g pnpm@11.5.2 +RUN wget -O jq https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 +RUN chmod +x ./jq +RUN cp jq /usr/bin diff --git a/docs/plans/2026-06-08-pnpm-audit.md b/docs/plans/2026-06-08-pnpm-audit.md new file mode 100644 index 00000000..787b83b7 --- /dev/null +++ b/docs/plans/2026-06-08-pnpm-audit.md @@ -0,0 +1,730 @@ +# pnpm audit Security Test Implementation Plan + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. + +**Goal:** Add a new `pnpmaudit` security test that runs `pnpm audit` on JavaScript repos using pnpm as their package manager, alongside the existing `npmaudit` (npm) and `yarnaudit` (yarn) tests. + +**Architecture:** In-cmd detection — the pnpm audit cmd script checks for `pnpm-lock.yaml` and silently skips if not found, matching the existing npm/yarn pattern. Both npm and pnpm tests run in parallel for all JavaScript repos (via errgroup in `runLanguageScans`); the wrong package manager test produces empty output. This avoids changes to the language scan dispatch logic. + +**Tech Stack:** Go (parser + types), Docker (scanner image), Node.js (pnpm runtime), Bash (cmd script) + +**Key Design Decisions:** +- Test name: `pnpmaudit` (follows `npmaudit`/`yarnaudit` naming convention) +- Output struct uses `json:"advisories"` (pnpm queries the npm registry `/bulk` endpoint, which returns `advisories`, not `vulnerabilities`) +- Docker base image: `node:alpine` + `pnpm` installed via `npm install -g pnpm` +- Severity mapping: low→low, moderate→medium, high/critical→high (same as npm audit) +- SecurityTool label: `PnpmAudit` +- Language: JavaScript + +--- + +### Task 1: Output Validation — capture pnpm audit JSON format + +**Objective:** Verify the pnpm audit JSON output schema before writing any parser code. + +**Files:** +- Create: `docs/plans/pnpm-audit-output-sample.json` + +**Step 1: Create test repo with intentional vulnerabilities** + +```bash +mkdir -p /tmp/pnpm-test && cd /tmp/pnpm-test +cat > package.json << 'EOF' +{ + "name": "pnpm-test", + "version": "1.0.0", + "dependencies": { "lodash": "4.17.20" } +} +EOF +pnpm install +``` + +**Step 2: Capture real pnpm audit JSON output** + +```bash +pnpm audit --json --prod > /tmp/pnpm-audit-output.json 2>/dev/null +``` + +**Step 3: Inspect output schema** + +```bash +python3 -c " +import json +d = json.load(open('/tmp/pnpm-audit-output.json')) +print('Top-level keys:', list(d.keys())) +print('advisories type:', type(d['advisories'])) +if d['advisories']: + adv = list(d['advisories'].values())[0] + print('Advisory keys:', list(adv.keys())) + print('Severity:', adv['severity']) + print('Findings type:', type(adv['findings'])) +print('metadata keys:', list(d['metadata'].keys())) +print('vulnerabilities keys:', list(d['metadata']['vulnerabilities'].keys())) +" +``` + +**Step 4: Save sample output** + +```bash +cp /tmp/pnpm-audit-output.json docs/plans/pnpm-audit-output-sample.json +``` + +**Verification:** All keys match expected schema: `advisories`, `metadata.vulnerabilities`, advisory fields (`id`, `title`, `module_name`, `severity`, `vulnerable_versions`, `cwe`, `github_advisory_id`, `url`, `findings`). + +--- + +### Task 2: Add pnpmaudit config to config.yaml + +**Objective:** Define the pnpmaudit security test configuration. + +**Files:** +- Modify: `api/config.yaml` (after `yarnaudit` block, around line 300) + +**Step 1: Add pnpmaudit config block** + +Insert after the `yarnaudit` block in `api/config.yaml`: + +```yaml +pnpmaudit: + name: pnpmaudit + image: huskyci/pnpmaudit + imageTag: "11.5.2" + cmd: |+ + mkdir -p ~/.ssh && + echo '%GIT_PRIVATE_SSH_KEY%' > ~/.ssh/huskyci_id_rsa && + chmod 600 ~/.ssh/huskyci_id_rsa && + echo "IdentityFile ~/.ssh/huskyci_id_rsa" >> /etc/ssh/ssh_config && + echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config && + GIT_TERMINAL_PROMPT=0 git clone -b "%GIT_BRANCH%" --single-branch --depth 1 "%GIT_REPO%" code --quiet 2> /tmp/errorGitClonePnpmAudit + if [ $? -eq 0 ]; then + cd code + if [ -f .npmrc ]; then + rm -f .npmrc + fi + if [ -f pnpm-lock.yaml ]; then + pnpm audit --json --prod > /tmp/results.json 2> /tmp/errorPnpmaudit + RC=$? + if [ $RC -eq 0 ] || [ $RC -eq 1 ]; then + cat /tmp/results.json + else + echo 'ERROR_RUNNING_PNPM_AUDIT' + cat /tmp/errorPnpmaudit + fi + fi + else + echo "ERROR_CLONING" + cat /tmp/errorGitClonePnpmAudit + fi + type: Language + language: JavaScript + default: true + timeOutInSeconds: 360 +``` + +**Key details:** +- `image: huskyci/pnpmaudit` — new scanner image +- `imageTag: "11.5.2"` — pnpm version +- `pnpm-lock.yaml` check — silent skip if not found (matches npm audit pattern) +- Exit code 0 or 1 both produce JSON output (pnpm exits 1 when vulns found) +- Exit code ≥ 2 → error sentinel `ERROR_RUNNING_PNPM_AUDIT` + +**Verification:** YAML parses without errors. Check: `python3 -c "import yaml; yaml.safe_load(open('api/config.yaml'))"` + +--- + +### Task 3: Add PnpmAuditOutput to API types + +**Objective:** Add `HuskyCIPnpmAuditOutput` to `JavaScriptResults` struct. + +**Files:** +- Modify: `api/types/types.go:117-121` + +**Step 1: Add the field to JavaScriptResults** + +```go +// JavaScriptResults represents all JavaScript security tests results. +type JavaScriptResults struct { + HuskyCINpmAuditOutput HuskyCISecurityTestOutput `bson:"npmauditoutput,omitempty" json:"npmauditoutput,omitempty"` + HuskyCIYarnAuditOutput HuskyCISecurityTestOutput `bson:"yarnauditoutput,omitempty" json:"yarnauditoutput,omitempty"` + HuskyCIPnpmAuditOutput HuskyCISecurityTestOutput `bson:"pnpmauditoutput,omitempty" json:"pnpmauditoutput,omitempty"` +} +``` + +**Verification:** `go build ./api/types/` compiles without errors. + +--- + +### Task 4: Add PnpmAuditSecurityTest to API context + +**Objective:** Add config field and wiring for the pnpm audit security test. + +**Files:** +- Modify: `api/context/context.go` + +**Step 1: Add field to APIConfig struct** (around line 101, after YarnAuditSecurityTest) + +```go +PnpmAuditSecurityTest *types.SecurityTest +``` + +**Step 2: Add SetOnceConfig wiring** (around line 157, after YarnAuditSecurityTest) + +```go +PnpmAuditSecurityTest: dF.getSecurityTestConfig("pnpmaudit"), +``` + +**Verification:** `go build ./api/context/` compiles without errors. + +--- + +### Task 5: Add pnpmaudit to checkEachSecurityTest and checkSecurityTest + +**Objective:** Register pnpmaudit in the DB seeding and config validation path. + +**Files:** +- Modify: `api/util/api/api.go` + +**Step 1: Add to securityTests list in checkEachSecurityTest** (line ~162) + +Add `"pnpmaudit"` to the `securityTests` slice after `"yarnaudit"`. + +**Step 2: Add case to checkSecurityTest switch** (after yarnaudit case, line ~220) + +```go +case "pnpmaudit": + securityTestConfig = *configAPI.PnpmAuditSecurityTest +``` + +**Verification:** `go build ./api/util/api/` compiles. Check `api/util/api/api_test.go` for any struct literal references that need updating. + +--- + +### Task 6: Create pnpm audit analyzer (pnpmaudit.go) + +**Objective:** Write the Go parser for pnpm audit JSON output. + +**Files:** +- Create: `api/securitytest/pnpmaudit.go` + +**Step 1: Define parser structs** + +```go +package securitytest + +import ( + "encoding/json" + "fmt" + + "github.com/githubanotaai/huskyci-api/api/log" + "github.com/githubanotaai/huskyci-api/api/types" + "github.com/githubanotaai/huskyci-api/api/util" +) + +// PnpmAuditOutput is the struct that stores all pnpm audit output. +type PnpmAuditOutput struct { + Advisories map[string]PnpmAdvisory `json:"advisories"` + Metadata PnpmMetadata `json:"metadata"` +} + +// PnpmAdvisory is a single advisory from pnpm audit. +type PnpmAdvisory struct { + ID int `json:"id"` + Title string `json:"title"` + ModuleName string `json:"module_name"` + VulnerableVersions string `json:"vulnerable_versions"` + PatchedVersions string `json:"patched_versions"` + Severity string `json:"severity"` + CWE string `json:"cwe"` + GithubAdvisoryID string `json:"github_advisory_id"` + URL string `json:"url"` + Findings []PnpmFinding `json:"findings"` +} + +// PnpmFinding represents a specific finding of a vulnerable dependency. +type PnpmFinding struct { + Version string `json:"version"` + Paths []string `json:"paths"` + Dev bool `json:"dev"` + Optional bool `json:"optional"` + Bundled bool `json:"bundled"` +} + +// PnpmMetadata is the struct that holds vulnerabilities summary. +type PnpmMetadata struct { + Vulnerabilities PnpmVulnerabilitiesSummary `json:"vulnerabilities"` +} + +// PnpmVulnerabilitiesSummary is the struct that has all types of possible vulnerabilities. +type PnpmVulnerabilitiesSummary struct { + Info int `json:"info"` + Low int `json:"low"` + Moderate int `json:"moderate"` + High int `json:"high"` + Critical int `json:"critical"` +} +``` + +**Step 2: Write analyzePnpmaudit function** + +```go +func analyzePnpmaudit(pnpmAuditScan *SecTestScanInfo) error { + pnpmAuditOutput := PnpmAuditOutput{} + pnpmAuditScan.FinalOutput = pnpmAuditOutput + + // nil cOutput states that no Issues were found (pnpm-lock.yaml not present). + if pnpmAuditScan.Container.COutput == "" { + pnpmAuditScan.prepareContainerAfterScan() + return nil + } + + // Unmarshal rawOutput into finalOutput. + if err := json.Unmarshal([]byte(pnpmAuditScan.Container.COutput), &pnpmAuditOutput); err != nil { + log.Error("analyzePnpmaudit", "PNPMAUDIT", 1014, pnpmAuditScan.Container.COutput, err) + pnpmAuditScan.ErrorFound = util.HandleScanError(pnpmAuditScan.Container.COutput, err) + pnpmAuditScan.prepareContainerAfterScan() + return pnpmAuditScan.ErrorFound + } + pnpmAuditScan.FinalOutput = pnpmAuditOutput + + pnpmAuditScan.preparePnpmAuditVulns() + pnpmAuditScan.prepareContainerAfterScan() + return nil +} +``` + +**Step 3: Write preparePnpmAuditVulns method** + +```go +func (pnpmAuditScan *SecTestScanInfo) preparePnpmAuditVulns() { + huskyCIPnpmauditResults := types.HuskyCISecurityTestOutput{} + pnpmAuditOutput := pnpmAuditScan.FinalOutput.(PnpmAuditOutput) + + for _, advisory := range pnpmAuditOutput.Advisories { + pnpmauditVuln := types.HuskyCIVulnerability{} + pnpmauditVuln.Language = "JavaScript" + pnpmauditVuln.SecurityTool = "PnpmAudit" + pnpmauditVuln.File = "pnpm-lock.yaml" + pnpmauditVuln.Title = fmt.Sprintf("Vulnerable Dependency: %s %s (%s)", advisory.ModuleName, advisory.VulnerableVersions, advisory.Title) + pnpmauditVuln.VunerableBelow = advisory.VulnerableVersions + pnpmauditVuln.Code = advisory.ModuleName + pnpmauditVuln.Details = fmt.Sprintf("GHSA: %s\nCWE: %s\nURL: %s\nPatched: %s", advisory.GithubAdvisoryID, advisory.CWE, advisory.URL, advisory.PatchedVersions) + + for i, finding := range advisory.Findings { + pnpmauditVuln.Version += fmt.Sprintf("Finding %d:\n", i) + pnpmauditVuln.Version += fmt.Sprintf(" Version: %s\n", finding.Version) + for _, path := range finding.Paths { + pnpmauditVuln.Version += fmt.Sprintf(" Path: %s\n", path) + } + } + + switch advisory.Severity { + case "info", "low": + pnpmauditVuln.Severity = "low" + huskyCIPnpmauditResults.LowVulns = append(huskyCIPnpmauditResults.LowVulns, pnpmauditVuln) + case "moderate": + pnpmauditVuln.Severity = "medium" + huskyCIPnpmauditResults.MediumVulns = append(huskyCIPnpmauditResults.MediumVulns, pnpmauditVuln) + case "high", "critical": + pnpmauditVuln.Severity = "high" + huskyCIPnpmauditResults.HighVulns = append(huskyCIPnpmauditResults.HighVulns, pnpmauditVuln) + } + } + + pnpmAuditScan.Vulnerabilities = huskyCIPnpmauditResults +} +``` + +**Verification:** `go build ./api/securitytest/` compiles without errors. + +--- + +### Task 7: Wire pnpmaudit into dispatch map and run.go + +**Objective:** Register the analyzer in the dispatch map and output routing. + +**Files:** +- Modify: `api/securitytest/securitytest.go` (line ~24) +- Modify: `api/securitytest/run.go` + +**Step 1: Add to securityTestAnalyze dispatch map** + +After `"npmaudit": analyzeNpmaudit,`: +```go +"pnpmaudit": analyzePnpmaudit, +``` + +**Step 2: Add const in run.go** (line ~33, after yarnaudit) + +```go +const pnpmaudit = "pnpmaudit" +``` + +**Step 3: Add case to vulnOutput switch** (after yarnaudit case, line ~183) + +```go +case pnpmaudit: + return &results.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput +``` + +**Verification:** `go build ./api/securitytest/` compiles without errors. + +--- + +### Task 8: Write API parser unit tests + +**Objective:** Test the pnpm audit parser with real output. + +**Files:** +- Create: `api/securitytest/pnpmaudit_test.go` + +**Step 1: Write test fixture based on captured output** + +```go +package securitytest + +import ( + "testing" + + "github.com/githubanotaai/huskyci-api/api/types" +) + +func TestAnalyzePnpmaudit(t *testing.T) { + // Real pnpm audit --json --prod output with lodash@4.17.20 + pnpmOutput := `{"advisories":{"1106913":{"findings":[{"version":"4.17.20","paths":[".>lodash"],"dev":false,"optional":false,"bundled":false}],"id":1106913,"title":"Command Injection in lodash","module_name":"lodash","vulnerable_versions":"<4.17.21","patched_versions":">=4.17.21","severity":"high","cwe":"CWE-77, CWE-94","github_advisory_id":"GHSA-35jh-r3h4-6jhm","url":"https://github.com/advisories/GHSA-35jh-r3h4-6jhm"}},"metadata":{"vulnerabilities":{"info":0,"low":0,"moderate":0,"high":1,"critical":0},"dependencies":1,"devDependencies":0,"optionalDependencies":0,"totalDependencies":1}}` + + scan := &SecTestScanInfo{ + Container: types.Container{ + COutput: pnpmOutput, + }, + } + + err := analyzePnpmaudit(scan) + if err != nil { + t.Fatalf("analyzePnpmaudit returned error: %v", err) + } + + if len(scan.Vulnerabilities.HighVulns) != 1 { + t.Errorf("expected 1 high vuln, got %d", len(scan.Vulnerabilities.HighVulns)) + } + + vuln := scan.Vulnerabilities.HighVulns[0] + if vuln.SecurityTool != "PnpmAudit" { + t.Errorf("expected SecurityTool PnpmAudit, got %s", vuln.SecurityTool) + } + if vuln.Severity != "high" { + t.Errorf("expected severity high, got %s", vuln.Severity) + } + if vuln.File != "pnpm-lock.yaml" { + t.Errorf("expected file pnpm-lock.yaml, got %s", vuln.File) + } +} + +func TestAnalyzePnpmauditEmpty(t *testing.T) { + // Empty COutput (pnpm-lock.yaml not found → silent skip) + scan := &SecTestScanInfo{ + Container: types.Container{ + COutput: "", + }, + } + + err := analyzePnpmaudit(scan) + if err != nil { + t.Fatalf("analyzePnpmaudit returned error: %v", err) + } + + if len(scan.Vulnerabilities.HighVulns) != 0 { + t.Errorf("expected 0 vulns for empty output, got high=%d", len(scan.Vulnerabilities.HighVulns)) + } +} +``` + +**Step 2: Run tests** + +```bash +cd api/securitytest && go test -run TestAnalyzePnpmaudit -v +``` + +Expected: PASS + +--- + +### Task 9: Add PnpmAuditOutput to client types + +**Objective:** Mirror the API type changes in the client. + +**Files:** +- Modify: `client/types/types.go:137-140` + +**Step 1: Add field to client JavaScriptResults** + +```go +type JavaScriptResults struct { + HuskyCINpmAuditOutput HuskyCISecurityTestOutput `bson:"npmauditoutput,omitempty" json:"npmauditoutput,omitempty"` + HuskyCIYarnAuditOutput HuskyCISecurityTestOutput `bson:"yarnauditoutput,omitempty" json:"yarnauditoutput,omitempty"` + HuskyCIPnpmAuditOutput HuskyCISecurityTestOutput `bson:"pnpmauditoutput,omitempty" json:"pnpmauditoutput,omitempty"` +} +``` + +**Step 2: Create client-level pnpm audit output struct** + +Create: `client/types/pnpmaudit.go` + +```go +package types + +// PnpmAuditOutput is the struct that stores all pnpm audit output. +type PnpmAuditOutput struct { + Advisories map[string]PnpmAdvisory `json:"advisories"` + Metadata PnpmMetadata `json:"metadata"` +} + +// PnpmAdvisory is a single advisory from pnpm audit. +type PnpmAdvisory struct { + Findings []PnpmFinding `json:"findings"` + ID int `json:"id"` + Title string `json:"title"` + ModuleName string `json:"module_name"` + VulnerableVersions string `json:"vulnerable_versions"` + Severity string `json:"severity"` +} + +// PnpmFinding represents a specific finding of a vulnerable dependency. +type PnpmFinding struct { + Version string `json:"version"` +} + +// PnpmMetadata is the struct that holds vulnerabilities summary. +type PnpmMetadata struct { + Vulnerabilities PnpmVulnerabilitiesSummary `json:"vulnerabilities"` +} + +// PnpmVulnerabilitiesSummary holds the count of vulnerabilities by severity. +type PnpmVulnerabilitiesSummary struct { + Info int `json:"info"` + Low int `json:"low"` + Moderate int `json:"moderate"` + High int `json:"high"` + Critical int `json:"critical"` +} +``` + +**Verification:** `go build ./client/types/` compiles. + +--- + +### Task 10: Add pnpm audit client output formatting + +**Objective:** Add pnpm audit to the client's stdout output and summary. + +**Files:** +- Modify: `client/analysis/output.go` + +**Step 1: Add print call in printSTDOUTOutput** (after yarnaudit line ~49) + +```go +// pnpmaudit +printToolGroup("JavaScript - PnpmAudit", outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput, printSTDOUTOutputPnpmAudit) +``` + +**Step 2: Add printSTDOUTOutputPnpmAudit function** + +```go +func printSTDOUTOutputPnpmAudit(output types.HuskyCISecurityTestOutput) { + printDefaultOutput(output) +} +``` + +**Step 3: Add PnpmAuditSummary to Summary struct** (in `client/types/types.go`, after YarnAuditSummary) + +Add field: +```go +PnpmAuditSummary HuskyCISummary `json:"pnpmauditsummary,omitempty"` +``` + +**Step 4: Add summary aggregation** (after NpmAudit summary block, around line ~145) + +```go +// PnpmAudit summary +outputJSON.Summary.PnpmAuditSummary.LowVuln = len(outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput.LowVulns) +outputJSON.Summary.PnpmAuditSummary.MediumVuln = len(outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput.MediumVulns) +outputJSON.Summary.PnpmAuditSummary.HighVuln = len(outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput.HighVulns) +if len(outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput.LowVulns) > 0 || len(outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput.NoSecVulns) > 0 { + outputJSON.Summary.PnpmAuditSummary.FoundInfo = true +} +if len(outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput.MediumVulns) > 0 || len(outputJSON.JavaScriptResults.HuskyCIPnpmAuditOutput.HighVulns) > 0 { + outputJSON.Summary.PnpmAuditSummary.FoundVuln = true +} +``` + +**Step 5: Exit-code logic** + +No explicit exit-code logic needed — the client uses global `types.FoundVuln`/`types.FoundInfo` booleans set during summary aggregation (Step 4). When pnpm audit finds HIGH/MEDIUM vulns, `FoundVuln` becomes true and the client exits 190 automatically (matching npm/yarn audit behavior). + +**Verification:** `go build ./client/analysis/` compiles. + +--- + +### Task 11: Write client output tests + +**Objective:** Test the client-side pnpm audit output formatting. + +**Files:** +- Create: `client/analysis/output_pnpmaudit_test.go` + +**Step 1: Write test with pnpm audit findings** + +```go +package analysis + +import ( + "testing" + + "github.com/githubanotaai/huskyci-api/client/types" +) + +func TestPrintSTDOUTOutputPnpmAudit(t *testing.T) { + output := types.HuskyCISecurityTestOutput{ + HighVulns: []types.HuskyCIVulnerability{ + { + Language: "JavaScript", + SecurityTool: "PnpmAudit", + Severity: "high", + File: "pnpm-lock.yaml", + Code: "lodash", + Title: "Vulnerable Dependency: lodash <4.17.21 (Command Injection)", + }, + }, + } + // Verify no panic on nil/missing fields + printSTDOUTOutputPnpmAudit(output) +} +``` + +**Step 2: Run tests** + +```bash +cd client/analysis && go test -run TestPrintSTDOUTOutputPnpmAudit -v +``` + +Expected: PASS + +--- + +### Task 12: Add pnpm audit to SonarQube integration + +**Objective:** Include pnpm audit findings in the SonarQube external issues report. + +**Files:** +- Modify: `client/integration/sonarqube/sonarqube.go` + +**Step 1: Add collection block** (after yarnaudit block, line ~55) + +```go +// pnpmaudit +allVulns = append(allVulns, analysis.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput.LowVulns...) +allVulns = append(allVulns, analysis.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput.MediumVulns...) +allVulns = append(allVulns, analysis.HuskyCIResults.JavaScriptResults.HuskyCIPnpmAuditOutput.HighVulns...) +``` + +**Step 2: Check for SonarQube test fixture updates** + +Check `client/integration/sonarqube/testdata/` for any test fixtures that reference JavaScriptResults — add `pnpmauditoutput` if needed. + +**Verification:** `go build ./client/integration/sonarqube/` compiles. Run SonarQube tests: `go test ./client/integration/sonarqube/ -v` + +--- + +### Task 13: Create pnpmaudit Dockerfile + +**Objective:** Create the scanner container image with pnpm installed. + +**Files:** +- Create: `deployments/dockerfiles/pnpmaudit/Dockerfile` + +**Step 1: Write Dockerfile** + +```dockerfile +# Dockerfile used to create "huskyci/pnpmaudit" image + +FROM node:alpine + +RUN apk update && apk upgrade \ + && apk add --no-cache alpine-sdk bash openssh-client \ + && apk add git + +RUN npm install -g pnpm@11.5.2 +RUN wget -O jq https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 +RUN chmod +x ./jq +RUN cp jq /usr/bin +``` + +**Verification:** `docker build -f deployments/dockerfiles/pnpmaudit/Dockerfile -t huskyci/pnpmaudit:test .` (builds successfully) + +--- + +### Task 14: Final verification — build and test + +**Objective:** Verify the full API compiles and all tests pass. + +**Files:** N/A + +**Step 1: Build API** + +```bash +cd api && go build ./... +``` + +**Step 2: Run all securitytest tests** + +```bash +cd api/securitytest && go test -v -run "TestAnalyze" +``` + +**Step 3: Run all client tests** + +```bash +cd client && go test ./... +``` + +**Step 4: Check for missing references** + +```bash +cd ~/Gits/huskyci-api +grep -r "npmaudit" --include="*.go" | grep -v "_test.go" | grep -v "npmaudit.go" +``` + +Review all references — pnpm audit should be handled in all the same places. + +--- + +## Summary of All Touchpoints + +| # | File | Change | +|---|---|---| +| 1 | `api/config.yaml` | Add `pnpmaudit` test block | +| 2 | `api/types/types.go` | Add `HuskyCIPnpmAuditOutput` to `JavaScriptResults` | +| 3 | `api/context/context.go` | Add `PnpmAuditSecurityTest` field + wiring | +| 4 | `api/util/api/api.go` | Add to `checkEachSecurityTest` + `checkSecurityTest` switch | +| 5 | `api/securitytest/securitytest.go` | Add to `securityTestAnalyze` dispatch map | +| 6 | `api/securitytest/pnpmaudit.go` | **NEW**: analyzer + parser structs | +| 7 | `api/securitytest/run.go` | Add const + `vulnOutput` case | +| 8 | `api/securitytest/pnpmaudit_test.go` | **NEW**: parser unit tests | +| 9 | `client/types/types.go` | Add `HuskyCIPnpmAuditOutput` to `JavaScriptResults` | +| 10 | `client/types/pnpmaudit.go` | **NEW**: client-level pnpm audit types | +| 11 | `client/analysis/output.go` | Add print call, summary aggregation, exit-code logic | +| 12 | `client/analysis/output_pnpmaudit_test.go` | **NEW**: client output tests | +| 13 | `client/integration/sonarqube/sonarqube.go` | Add SonarQube collection for pnpm audit | +| 14 | `deployments/dockerfiles/pnpmaudit/Dockerfile` | **NEW**: scanner Docker image | + +## Container Build (Phase C — after code is merged) + +| Step | Action | +|---|---| +| 1 | `ifood-aws-login -r anotaai-platform-production:idp-aai-sec-team` | +| 2 | Docker ECR login | +| 3 | Build + push scanner image: `docker buildx build --platform linux/amd64 --builder huskyci-buildx -f deployments/dockerfiles/pnpmaudit/Dockerfile -t 939030204144.dkr.ecr.us-east-1.amazonaws.com/huskyci-pnpmaudit:f793155-amd64 --push .` | +| 4 | Create ECR repo: `aws ecr create-repository --repository-name huskyci-pnpmaudit --region us-east-1` | +| 5 | Build + push API image (config.yaml is baked in) | +| 6 | Build + push client image | +| 7 | Update k8s-infrastructure-live values.yaml | +| 8 | Update .github anotaai-sast.yml client image tag | diff --git a/docs/plans/pnpm-audit-output-sample.json b/docs/plans/pnpm-audit-output-sample.json new file mode 100644 index 00000000..c5f50045 --- /dev/null +++ b/docs/plans/pnpm-audit-output-sample.json @@ -0,0 +1,127 @@ +{ + "advisories": { + "1106913": { + "findings": [ + { + "version": "4.17.20", + "paths": [ + ".>lodash" + ], + "dev": false, + "optional": false, + "bundled": false + } + ], + "id": 1106913, + "title": "Command Injection in lodash", + "module_name": "lodash", + "vulnerable_versions": "<4.17.21", + "patched_versions": ">=4.17.21", + "severity": "high", + "cwe": "CWE-77, CWE-94", + "github_advisory_id": "GHSA-35jh-r3h4-6jhm", + "url": "https://github.com/advisories/GHSA-35jh-r3h4-6jhm" + }, + "1108258": { + "findings": [ + { + "version": "4.17.20", + "paths": [ + ".>lodash" + ], + "dev": false, + "optional": false, + "bundled": false + } + ], + "id": 1108258, + "title": "Regular Expression Denial of Service (ReDoS) in lodash", + "module_name": "lodash", + "vulnerable_versions": ">=4.0.0 <4.17.21", + "patched_versions": ">=4.17.21", + "severity": "moderate", + "cwe": "CWE-400, CWE-1333", + "github_advisory_id": "GHSA-29mw-wpgm-hmr9", + "url": "https://github.com/advisories/GHSA-29mw-wpgm-hmr9" + }, + "1112455": { + "findings": [ + { + "version": "4.17.20", + "paths": [ + ".>lodash" + ], + "dev": false, + "optional": false, + "bundled": false + } + ], + "id": 1112455, + "title": "Lodash has Prototype Pollution Vulnerability in `_.unset` and `_.omit` functions", + "module_name": "lodash", + "vulnerable_versions": ">=4.0.0 <=4.17.22", + "patched_versions": ">=4.17.23", + "severity": "moderate", + "cwe": "CWE-1321", + "github_advisory_id": "GHSA-xxjr-mmjv-4gpg", + "url": "https://github.com/advisories/GHSA-xxjr-mmjv-4gpg" + }, + "1115806": { + "findings": [ + { + "version": "4.17.20", + "paths": [ + ".>lodash" + ], + "dev": false, + "optional": false, + "bundled": false + } + ], + "id": 1115806, + "title": "lodash vulnerable to Code Injection via `_.template` imports key names", + "module_name": "lodash", + "vulnerable_versions": ">=4.0.0 <=4.17.23", + "patched_versions": ">=4.17.24", + "severity": "high", + "cwe": "CWE-94", + "github_advisory_id": "GHSA-r5fr-rjxr-66jc", + "url": "https://github.com/advisories/GHSA-r5fr-rjxr-66jc" + }, + "1115810": { + "findings": [ + { + "version": "4.17.20", + "paths": [ + ".>lodash" + ], + "dev": false, + "optional": false, + "bundled": false + } + ], + "id": 1115810, + "title": "lodash vulnerable to Prototype Pollution via array path bypass in `_.unset` and `_.omit`", + "module_name": "lodash", + "vulnerable_versions": "<=4.17.23", + "patched_versions": ">=4.17.24", + "severity": "moderate", + "cwe": "CWE-1321", + "github_advisory_id": "GHSA-f23m-r3pf-42rh", + "url": "https://github.com/advisories/GHSA-f23m-r3pf-42rh" + } + }, + "metadata": { + "vulnerabilities": { + "info": 0, + "low": 0, + "moderate": 3, + "high": 2, + "critical": 0 + }, + "dependencies": 1, + "devDependencies": 0, + "optionalDependencies": 0, + "totalDependencies": 1 + } +}