diff --git a/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/PolicyEvalException.java b/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/PolicyEvalException.java deleted file mode 100644 index 799c91b3..00000000 --- a/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/PolicyEvalException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.stackrox.jenkins.plugins; - -public class PolicyEvalException extends Exception { - public PolicyEvalException(String message) { - super(message); - } -} diff --git a/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/StackroxBuilder.java b/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/StackroxBuilder.java index a7207151..2c018e93 100644 --- a/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/StackroxBuilder.java +++ b/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/StackroxBuilder.java @@ -14,6 +14,7 @@ import com.stackrox.jenkins.plugins.data.ImageCheckResults; import com.stackrox.jenkins.plugins.data.PolicyViolation; import com.stackrox.jenkins.plugins.jenkins.RunConfig; +import com.stackrox.jenkins.plugins.jenkins.ScanTask; import com.stackrox.jenkins.plugins.report.ReportGenerator; import com.stackrox.jenkins.plugins.services.ApiClientFactory; import com.stackrox.jenkins.plugins.services.DetectionService; @@ -34,7 +35,10 @@ import hudson.util.FormValidation; import hudson.util.Secret; import jenkins.model.Jenkins; +import jenkins.security.MasterToSlaveCallable; import jenkins.tasks.SimpleBuildStep; +import lombok.AllArgsConstructor; +import lombok.Data; import lombok.Getter; import lombok.Setter; import net.sf.json.JSONObject; @@ -49,7 +53,11 @@ import javax.annotation.Nonnull; import javax.net.ssl.SSLException; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.Serializable; import java.net.SocketException; import java.net.UnknownHostException; import java.util.List; @@ -109,30 +117,27 @@ public void perform( @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws IOException, InterruptedException { - runConfig = RunConfig.create(listener.getLogger(), run.getCharacteristicEnvVars().get("BUILD_TAG"), workspace, getImages()); - try { - List results = checkImages(); + List results = workspace.act(new ScanTask( + run.getCharacteristicEnvVars().get("BUILD_TAG"), workspace.getRemote(), getImages(), + getPortalAddress(), getApiToken(), getCaCertPEM(), getTLSValidationMode())); + + runConfig = RunConfig.create(listener.getLogger(), run.getCharacteristicEnvVars().get("BUILD_TAG"), workspace, getImages(), + getPortalAddress(), getApiToken(), getCaCertPEM(), getTLSValidationMode()); + + ReportGenerator.generateBuildReport(results, runConfig.getReportsDir()); prepareArtifacts(run, workspace, launcher, listener); run.addAction(new ViewStackroxResultsAction(results, run)); - cleanupJenkinsWorkspace(); - if (enforcedPolicyViolationExists(results)) { - throw new PolicyEvalException( - "At least one image violated at least one enforced system policy. Marking StackRox Image Security plugin build step failed. Check the report for additional details."); + if (this.failOnPolicyEvalFailure && enforcedPolicyViolationExists(results)) { + throw new AbortException("At least one image violated at least one enforced system policy. Marking StackRox Image Security plugin build step failed. Check the report for additional details."); } - } catch (IOException e) { if (this.failOnCriticalPluginError) { throw new AbortException(String.format("Fatal error: %s. Aborting ...", e.getMessage())); } runConfig.getLog().println("Marking StackRox Image Security plugin build step as successful despite error."); - } catch (PolicyEvalException e) { - if (this.failOnPolicyEvalFailure) { - throw new AbortException(e.getMessage()); - } - runConfig.getLog().println("Marking StackRox Image Security plugin build step as successful despite enforced policy violations."); } runConfig.getLog().println("No system policy violations found. Marking StackRox Image Security plugin build step as successful."); @@ -144,29 +149,6 @@ private void prepareArtifacts(Run run, FilePath workspace, Launcher launch artifactArchiver.perform(run, workspace, launcher, listener); } - private List checkImages() throws IOException { - List results = Lists.newArrayList(); - - ApiClient apiClient = ApiClientFactory.newApiClient( - getPortalAddress(), getApiToken().getPlainText(), getCaCertPEM(), getTLSValidationMode()); - ImageService imageService = new ImageService(apiClient); - DetectionService detectionService = new DetectionService(apiClient); - - for (String name : runConfig.getImageNames()) { - runConfig.getLog().printf("Checking image %s...%n", name); - - List cves = imageService.getImageScanResults(name); - List violatedPolicies = detectionService.getPolicyViolations(name); - results.add(new ImageCheckResults(name, cves, violatedPolicies)); - } - - results.sort((result1, result2) -> { - //descending order - return Boolean.compare(result1.isStatusPass(), result2.isStatusPass()); - }); - return results; - } - // Runs an image scan and the build time policy checks on the image private boolean enforcedPolicyViolationExists(List results) { @@ -178,17 +160,6 @@ private boolean enforcedPolicyViolationExists(List results) { return false; } - private void cleanupJenkinsWorkspace() { - runConfig.getLog().println("Cleaning up the workspace ..."); - - try { - runConfig.getBaseWorkDir().deleteRecursive(); - runConfig.getReportsDir().deleteRecursive(); - } catch (IOException | InterruptedException e) { - runConfig.getLog().println("WARN: Failed to cleanup."); - } - } - @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); diff --git a/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/jenkins/RunConfig.java b/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/jenkins/RunConfig.java index 1d1a9aa4..f27b5ecd 100644 --- a/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/jenkins/RunConfig.java +++ b/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/jenkins/RunConfig.java @@ -1,8 +1,12 @@ package com.stackrox.jenkins.plugins.jenkins; +import com.stackrox.jenkins.plugins.services.ApiClientFactory; + import hudson.AbortException; import hudson.FilePath; +import hudson.util.Secret; import lombok.Data; +import org.kohsuke.stapler.DataBoundSetter; import javax.annotation.Nonnull; import java.io.File; @@ -14,6 +18,8 @@ @Data public class RunConfig { + private static final long serialVersionUID = 1L; + private static final String IMAGE_LIST_FILENAME = "rox_images_to_scan"; private static final String REPORTS_DIR_NAME = "rox_image_security_reports/"; @@ -23,8 +29,18 @@ public class RunConfig { private final List imageNames; private final String artifactsRelativePath; - public static RunConfig create(PrintStream log, String buildTag, FilePath workspace, List images) throws AbortException { - try { + private final String portalAddress; + private final Secret apiToken; + private final String caCertPEM; + + private final ApiClientFactory.StackRoxTlsValidationMode TLSValidationMode; + + + public static RunConfig createForTest(PrintStream log, String buildTag, FilePath workspace, List images) throws IOException, InterruptedException { + return create(log, buildTag, workspace, images, "", Secret.fromString(""), "", ApiClientFactory.StackRoxTlsValidationMode.INSECURE_ACCEPT_ANY); + } + public static RunConfig create(PrintStream log, String buildTag, FilePath workspace, List images, + String portalAddress, Secret apiToken, String caCertPEM, ApiClientFactory.StackRoxTlsValidationMode tlsValidationMode) throws IOException, InterruptedException { FilePath baseWorkDir = new FilePath(workspace, buildTag); FilePath reportsDir = new FilePath(workspace, REPORTS_DIR_NAME); @@ -35,11 +51,12 @@ public static RunConfig create(PrintStream log, String buildTag, FilePath worksp baseWorkDir, reportsDir, imageNames, - REPORTS_DIR_NAME + REPORTS_DIR_NAME, + portalAddress, + apiToken, + caCertPEM, + tlsValidationMode ); - } catch (IOException | InterruptedException e) { - throw new AbortException(String.format("Error in creating a run configuration: %s", e.getMessage())); - } } @Nonnull diff --git a/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/jenkins/ScanTask.java b/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/jenkins/ScanTask.java new file mode 100644 index 00000000..5a448200 --- /dev/null +++ b/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/jenkins/ScanTask.java @@ -0,0 +1,99 @@ +package com.stackrox.jenkins.plugins.jenkins; + +import com.google.common.collect.Lists; + +import com.stackrox.invoker.ApiClient; +import com.stackrox.jenkins.plugins.data.CVE; +import com.stackrox.jenkins.plugins.data.ImageCheckResults; +import com.stackrox.jenkins.plugins.data.PolicyViolation; +import com.stackrox.jenkins.plugins.services.ApiClientFactory; +import com.stackrox.jenkins.plugins.services.DetectionService; +import com.stackrox.jenkins.plugins.services.ImageService; + +import hudson.FilePath; +import hudson.util.Secret; +import jenkins.security.MasterToSlaveCallable; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.io.Serializable; +import java.nio.file.Path; +import java.util.List; + +@Data +@AllArgsConstructor +public class ScanTask extends MasterToSlaveCallable, IOException> implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String buildTag; + private final String workspace; + private final List images; + private final String portalAddress; + private final Secret apiToken; + private final String caCertPEM; + private final ApiClientFactory.StackRoxTlsValidationMode tlsValidationMode; + + + @Override + public List call() throws IOException { + RunConfig runConfig = null; + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + PrintStream errorOut = new PrintStream(byteArrayOutputStream); + PrintStream log = new PrintStream(errorOut); + + File folder = new File(workspace); + try { + runConfig = RunConfig.create(log, buildTag, new FilePath(folder), images, + portalAddress, apiToken, caCertPEM, tlsValidationMode); + } catch (InterruptedException e) { + throw new IOException(e); + } + List imageCheckResults = checkImages(runConfig); + cleanupJenkinsWorkspace(runConfig); + return imageCheckResults; + + } + + private List checkImages(RunConfig runConfig) throws IOException { + List results = Lists.newArrayList(); + + ApiClient apiClient = ApiClientFactory.newApiClient( + runConfig.getPortalAddress(), + runConfig.getApiToken().getPlainText(), + runConfig.getCaCertPEM(), + runConfig.getTLSValidationMode() + ); + ImageService imageService = new ImageService(apiClient); + DetectionService detectionService = new DetectionService(apiClient); + + for (String name : runConfig.getImageNames()) { + runConfig.getLog().printf("Checking image %s...%n", name); + + List cves = imageService.getImageScanResults(name); + List violatedPolicies = detectionService.getPolicyViolations(name); + results.add(new ImageCheckResults(name, cves, violatedPolicies)); + } + + results.sort((result1, result2) -> { + //descending order + return Boolean.compare(result1.isStatusPass(), result2.isStatusPass()); + }); + return results; + } + + private void cleanupJenkinsWorkspace(RunConfig runConfig) { + runConfig.getLog().println("Cleaning up the workspace ..."); + + try { + runConfig.getBaseWorkDir().deleteRecursive(); + runConfig.getReportsDir().deleteRecursive(); + } catch (IOException | InterruptedException e) { + runConfig.getLog().println("WARN: Failed to cleanup."); + } + } +} diff --git a/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/report/ReportGenerator.java b/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/report/ReportGenerator.java index 8d23ccff..8b2b890d 100644 --- a/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/report/ReportGenerator.java +++ b/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/report/ReportGenerator.java @@ -22,7 +22,7 @@ public class ReportGenerator { - private static final String[] CVES_HEADER = {"COMPONENT", "VERSION", "CVE", "FIXABLE", "SEVERITY", "CVSS SCORE", "SCORE TYPE", "LINK"}; + private static final String[] CVES_HEADER = {"COMPONENT", "VERSION", "CVE", "SEVERITY", "FIXABLE", "CVSS SCORE", "SCORE TYPE", "LINK"}; private static final String[] VIOLATED_POLICIES_HEADER = {"POLICY", "SEVERITY", "DESCRIPTION", "VIOLATION", "REMEDIATION", "ENFORCED"}; private static final String CVES_FILENAME = "cves.csv"; private static final String POLICY_VIOLATIONS_FILENAME = "policyViolations.csv"; diff --git a/stackrox-container-image-scanner/src/test/java/com/stackrox/jenkins/plugins/jenkins/RunConfigTest.java b/stackrox-container-image-scanner/src/test/java/com/stackrox/jenkins/plugins/jenkins/RunConfigTest.java index 8a3672b3..30290299 100644 --- a/stackrox-container-image-scanner/src/test/java/com/stackrox/jenkins/plugins/jenkins/RunConfigTest.java +++ b/stackrox-container-image-scanner/src/test/java/com/stackrox/jenkins/plugins/jenkins/RunConfigTest.java @@ -1,15 +1,26 @@ package com.stackrox.jenkins.plugins.jenkins; import com.google.common.collect.ImmutableList; + +import com.stackrox.jenkins.plugins.StackroxBuilder; + +import com.stackrox.jenkins.plugins.services.ApiClientFactory; + import hudson.AbortException; import hudson.FilePath; +import hudson.util.Secret; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.io.PrintStream; import java.nio.file.Path; import java.util.Collections; @@ -30,20 +41,20 @@ class RunConfigTest { @Test void createShouldFailWhenNoImagesSpecifiedAndFileDoesNotExist() { - Exception exception = assertThrows(AbortException.class, () -> RunConfig.create(LOG, BUILD_TAG, new FilePath(folder.toFile()), Collections.emptyList())); - assertTrue(exception.getMessage().contains("Error in creating a run configuration: rox_images_to_scan not found at")); + Exception exception = assertThrows(IOException.class, () -> RunConfig.createForTest(LOG, BUILD_TAG, new FilePath(folder.toFile()), Collections.emptyList())); + assertTrue(exception.getMessage().contains("rox_images_to_scan not found")); } @Test void createShouldFailWhenNoImagesToScan() throws IOException { assertTrue(imagesToScanFile().createNewFile()); - Exception exception = assertThrows(AbortException.class, () -> RunConfig.create(LOG, BUILD_TAG, new FilePath(folder.toFile()), Collections.emptyList())); - assertEquals("Error in creating a run configuration: no images to scan", exception.getMessage()); + Exception exception = assertThrows(IOException.class, () -> RunConfig.createForTest(LOG, BUILD_TAG, new FilePath(folder.toFile()), Collections.emptyList())); + assertEquals("no images to scan", exception.getMessage()); } @Test void createShouldReturnListOfProvidedImagesAndCreateRequiredDirs() throws IOException, InterruptedException { - RunConfig runConfig = RunConfig.create(LOG, BUILD_TAG, new FilePath(folder.toFile()), ImmutableList.of("A", "B", "C")); + RunConfig runConfig = RunConfig.createForTest(LOG, BUILD_TAG, new FilePath(folder.toFile()), ImmutableList.of("A", "B", "C")); assertEquals(ImmutableList.of("A", "B", "C"), runConfig.getImageNames()); assertTrue(runConfig.getReportsDir().exists()); assertFalse(runConfig.getBaseWorkDir().exists()); @@ -55,13 +66,41 @@ void createShouldReturnListOfFromFileAndCreateRequiredDirs() throws IOException, FileWriter writer = new FileWriter(imagesToScan); writer.write("A\nB\nC\n"); writer.close(); - RunConfig runConfig = RunConfig.create(LOG, BUILD_TAG, new FilePath(folder.toFile()), Collections.emptyList()); + RunConfig runConfig = RunConfig.createForTest(LOG, BUILD_TAG, new FilePath(folder.toFile()), Collections.emptyList()); assertEquals(ImmutableList.of("A", "B", "C"), runConfig.getImageNames()); assertTrue(runConfig.getReportsDir().exists()); assertTrue(runConfig.getBaseWorkDir().exists()); assertEquals("rox_image_security_reports/", runConfig.getArtifactsRelativePath()); } + @Test + void shouldBeSerializable() throws IOException, InterruptedException, ClassNotFoundException { + File imagesToScan = imagesToScanFile(); + FileWriter writer = new FileWriter(imagesToScan); + writer.write("A\nB\nC\n"); + writer.close(); + + ScanTask original = new ScanTask(BUILD_TAG, folder.toString(), ImmutableList.of( "A", "B", "C"), "", Secret.fromString(""), "", ApiClientFactory.StackRoxTlsValidationMode.VALIDATE); + + FileOutputStream fileOutputStream + = new FileOutputStream("yourfile.txt"); + ObjectOutputStream objectOutputStream + = new ObjectOutputStream(fileOutputStream); + objectOutputStream.writeObject(original); + objectOutputStream.flush(); + objectOutputStream.close(); + + FileInputStream fileInputStream + = new FileInputStream("yourfile.txt"); + ObjectInputStream objectInputStream + = new ObjectInputStream(fileInputStream); + ScanTask copy = (ScanTask) objectInputStream.readObject(); + objectInputStream.close(); + + assertEquals(ImmutableList.of("A", "B", "C"), copy.getImages()); + assertEquals(BUILD_TAG, copy.getBuildTag()); + } + private File imagesToScanFile() { File workDir = new File(folder.toFile(), BUILD_TAG); workDir.mkdirs(); diff --git a/stackrox-container-image-scanner/src/test/resources/report/jenkins.lts/cves.csv b/stackrox-container-image-scanner/src/test/resources/report/jenkins.lts/cves.csv index fd3c2e79..2e88057d 100644 --- a/stackrox-container-image-scanner/src/test/resources/report/jenkins.lts/cves.csv +++ b/stackrox-container-image-scanner/src/test/resources/report/jenkins.lts/cves.csv @@ -1,4 +1,4 @@ -COMPONENT,VERSION,CVE,FIXABLE,SEVERITY,CVSS SCORE,SCORE TYPE,LINK +COMPONENT,VERSION,CVE,SEVERITY,FIXABLE,CVSS SCORE,SCORE TYPE,LINK util-linux,2.25.2-6,CVE-2015-5224,IMPORTANT,false,9.8,V3,https://security-tracker.debian.org/tracker/CVE-2015-5224 gcc-4.8,4.8.4-1,CVE-2017-11671,MODERATE,false,4.0,V3,https://security-tracker.debian.org/tracker/CVE-2017-11671 bzip2,1.0.6-7,CVE-2016-3189,LOW,true,6.5,V3,https://security-tracker.debian.org/tracker/CVE-2016-3189 diff --git a/stackrox-container-image-scanner/src/test/resources/report/nginx.latest/cves.csv b/stackrox-container-image-scanner/src/test/resources/report/nginx.latest/cves.csv index 66296acd..8e6b91a8 100644 --- a/stackrox-container-image-scanner/src/test/resources/report/nginx.latest/cves.csv +++ b/stackrox-container-image-scanner/src/test/resources/report/nginx.latest/cves.csv @@ -1,3 +1,3 @@ -COMPONENT,VERSION,CVE,FIXABLE,SEVERITY,CVSS SCORE,SCORE TYPE,LINK +COMPONENT,VERSION,CVE,SEVERITY,FIXABLE,CVSS SCORE,SCORE TYPE,LINK openssl,1.1.1d-0+deb10u7,CVE-2007-6755,LOW,false,5.8,V2,https://security-tracker.debian.org/tracker/CVE-2007-6755 -,-,CVE-MISSING-DATA,UNKNOWN,false,0.0,-,- diff --git a/stackrox-container-image-scanner/src/test/resources/report_with_no_issues/mis-spelled.lts/cves.csv b/stackrox-container-image-scanner/src/test/resources/report_with_no_issues/mis-spelled.lts/cves.csv index 3840847c..2f227edf 100644 --- a/stackrox-container-image-scanner/src/test/resources/report_with_no_issues/mis-spelled.lts/cves.csv +++ b/stackrox-container-image-scanner/src/test/resources/report_with_no_issues/mis-spelled.lts/cves.csv @@ -1 +1 @@ -COMPONENT,VERSION,CVE,FIXABLE,SEVERITY,CVSS SCORE,SCORE TYPE,LINK +COMPONENT,VERSION,CVE,SEVERITY,FIXABLE,CVSS SCORE,SCORE TYPE,LINK diff --git a/stackrox-container-image-scanner/yourfile.txt b/stackrox-container-image-scanner/yourfile.txt new file mode 100644 index 00000000..8f5b0607 Binary files /dev/null and b/stackrox-container-image-scanner/yourfile.txt differ