Skip to content

Commit

Permalink
feat: support dockerfile base image vulnerability scan (#150)
Browse files Browse the repository at this point in the history
* feat: support dockerfile base image vulnerability scan

* feat: discontinue support for synk

* docs: support dockerfile base image vulnerability scan

* 4/18/24 - ritz303 : Added content for Docker scanning feature

* 4/19/24 - ritz303 : Added Docker scanning settings

* docs: update docs for dockerfile scan

* docs: update change notes

* chore: bump development version to 1.0.0-SNAPSHOT

---------

Co-authored-by: Aron Gunn <agunn@redhat.com>
  • Loading branch information
xieshenzh and agunn303 committed Apr 23, 2024
1 parent 0f4283c commit 3657233
Show file tree
Hide file tree
Showing 34 changed files with 1,655 additions and 220 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/IJ.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
IJ: [IC-2021.1]
IJ: [IC-2022.1]

steps:
- uses: actions/checkout@v2
Expand Down
77 changes: 62 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ vulnerability report.

**IMPORTANT:**
<br >Currently, Dependency Analytics only supports projects that use Maven (`mvn`), Node (`npm`), Golang (`go mod`) and
Python (`pip`) ecosystems.
Python (`pip`) ecosystems, and base images in `Dockerfile`.
In future releases, Red Hat plans to support other programming languages.

##### Table of Contents
Expand All @@ -45,6 +45,10 @@ In future releases, Red Hat plans to support other programming languages.
- For Golang projects, analyzing a `go.mod` file, you must have the `go` binary in your IDE's `PATH` environment.
- For Python projects, analyzing a `requirements.txt` file, you must have the `python3` and `pip3` binaries in your
IDE's `PATH` environment.
- For base images, analyzing a `Dockerfile`, you must have
the [`syft`](https://github.com/anchore/syft?tab=readme-ov-file#installation)
and [`skopeo`](https://github.com/containers/skopeo/blob/main/install.md) binaries in your IDE's `PATH`
environment.

**Procedure**

Expand Down Expand Up @@ -77,36 +81,57 @@ according to your preferences.
- **Maven** :
<br >Set the full path of the Maven executable, which allows Exhort to locate and execute the `mvn` command to resolve
dependencies for Maven projects.
Path of the `JAVA_HOME` directory is required by the `mvn` executable.
If the paths are not provided, your IDE's `PATH` and `JAVA_HONE` environments will be used to locate the executables.
<br >Path of the `JAVA_HOME` directory is required by the `mvn` executable.
<br >If the paths are not provided, your IDE's `PATH` and `JAVA_HONE` environments will be used to locate the
executables.

- **Node** :
<br >Set the full path of the Node executable, which allows Exhort to locate and execute the `npm` command to resolve
dependencies for Node projects.
Path of the directory containing the `node` executable is required by the `npm` executable.
If the paths are not provided, your IDE's `PATH` environment will be used to locate the executables.
<br >Path of the directory containing the `node` executable is required by the `npm` executable.
<br >If the paths are not provided, your IDE's `PATH` environment will be used to locate the executables.

- **Golang** :
<br >Set the full path of the Go executable, which allows Exhort to locate and execute the `go` command to resolve
dependencies for Go projects.
If the path is not provided, your IDE's `PATH` environment will be used to locate the executable.
When option `Strictly match package version` is selected, the resolved dependency versions will be compared to the
versions specified in the manifest file, and users will be alerted if any mismatch is detected.
<br >If the path is not provided, your IDE's `PATH` environment will be used to locate the executable.
<br >When option `Strictly match package version` is selected, the resolved dependency versions will be compared to
the versions specified in the manifest file, and users will be alerted if any mismatch is detected.

- **Python** :
<br >Set the full paths of the Python and the package installer for Python executables, which allows Exhort to locate
and execute the `pip3` commands to resolve dependencies for Python projects.
Python 2 executables `python` and `pip` can be used instead, if the `Use python 2.x` option is selected.
If the paths are not provided, your IDE's `PATH` environment will be used to locate the executables.
When option `Strictly match package version` is selected, the resolved dependency versions will be compared to the
versions specified in the manifest file, and users will be alerted if any mismatch is detected.
Python virtual environment can be applied, when selecting the `Use python virtual environment` option.
If selecting option `Allow alternate package version` while using virtual environment, the dependency versions
<br >Python 2 executables `python` and `pip` can be used instead, if the `Use python 2.x` option is selected.
<br >If the paths are not provided, your IDE's `PATH` environment will be used to locate the executables.
<br >When option `Strictly match package version` is selected, the resolved dependency versions will be compared to
the versions specified in the manifest file, and users will be alerted if any mismatch is detected.
<br >Python virtual environment can be applied, when selecting the `Use python virtual environment` option.
<br >If selecting option `Allow alternate package version` while using virtual environment, the dependency versions
specified in the manifest file will be ignored, and dependency versions will be resolved dynamically instead (this
feature cannot be enabled when `Strictly match package version` is selected).

- **Image** :
<br >Set the full path of the Syft executable, which allows Exhort to locate and execute the `syft` command to
generate Software Bill of Materials for the base images.
<br >Optionally, set the full path of the Docker or Podman executable. Syft will attempt to find the images in the
Docker or Podman daemon with the executable. Otherwise, Syft will try direct remote registry access.
<br >Set the full path of the Skopeo executable, which allows Exhort to locate and execute the `skopeo` command to
determine the image digests.
<br >If the paths are not provided, your IDE's `PATH` environment will be used to locate the executables.
<br >If a Syft configuration file is used and not at the
default [paths](https://github.com/anchore/syft/blob/469b4c13bbc52c43bc5216924b6ffd9d6d47bbd6/README.md#configuration),
set the full path to the configuration file in configuration.
<br >If
an [authentication file](https://github.com/containers/skopeo/blob/3eacbe5ae2fe859f872a02bf28c16371fb1de7b8/docs/skopeo-inspect.1.md#options)
is applied for `skopeo inspect`, set the full path to the file in configuration.
<br >If platform is not specified in the `Dockerfile` for multi-platform images and a default platform should be
applied, set the default platform in the configuration. Otherwise, set the full path of the Docker or Podman
executable, then Exhort will use the executable to determine the image platform based on the OS and architecture of
the container runtime.

- **Inline Vulnerability Severity Alerts** :
<br >You can set the vulnerability severity alert level to `Error` or `Warning` for inline notifications of detected vulnerabilities.
<br >You can set the vulnerability severity alert level to `Error` or `Warning` for inline notifications of detected
vulnerabilities.

## Features

Expand All @@ -121,6 +146,28 @@ according to your preferences.

![ Animated screenshot showing the inline reporting feature of Dependency Analytics ](src/main/resources/images/component-analysis.gif)

- **Docker scanning**
<br >Upon opening a Dockerfile, a vulnerability scan starts analyzing the images within the Dockerfile.
After the analysis finishes, you can view any recommendations and remediation by clicking the _More actions..._ menu
from the highlighted image name.
Any recommendations for an alternative image does not replace the current image.
By clicking _Switch to..._, you go to Red Hat's Ecosystem Catalog for the recommended image.

<br >You must have the [`syft`](https://github.com/anchore/syft#installation)
and [`skopeo`](https://www.redhat.com/en/topics/containers/what-is-skopeo) binaries installed on your workstation to
use the Docker scanning feature.
You can specify a specific path to these binaries, and others by settings the following parameters:

* `syft.executable.path` : Specify the absolute path of `syft` executable.
* `syft.config.path` : Specify the absolute path to the Syft configuration file.
* `skopeo.executable.path` : Specify the absolute path of `skopeo` executable.
* `skopeo.config.path` : Specify the absolute path to the authentication file used by the `skopeo inspect` command.
* `docker.executable.path` : Specify the absolute path of `docker` executable.
* `podman.executable.path` : Specify the absolute path of `podman` executable.
* `image.platform` : Specify the platform used for multi-arch images.

![ Animated screenshot showing the inline reporting feature of Image Analysis ](src/main/resources/images/image-analysis.gif)

- **Excluding dependencies with `exhortignore`**
<br >You can exclude a package from analysis by marking the package for exclusion.
If you want to ignore vulnerabilities for a dependency in a `pom.xml` file, you must add `exhortignore` as a comment
Expand Down
5 changes: 3 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ intellij {
version = ideaVersion //for a full list of IntelliJ IDEA releases please see https://www.jetbrains.com/intellij-repository/releases
pluginName = 'org.jboss.tools.intellij.analytics'
plugins = ['com.redhat.devtools.intellij.telemetry:1.1.0.52',
"org.jetbrains.plugins.go:211.6693.111"]
"org.jetbrains.plugins.go:221.5080.9",
"Docker:221.5080.9"]
updateSinceUntilBuild = false
}

Expand All @@ -61,7 +62,7 @@ dependencies {

implementation 'org.kohsuke:github-api:1.314'
implementation 'org.apache.commons:commons-compress:1.21'
implementation 'com.redhat.exhort:exhort-java-api:0.0.6-SNAPSHOT'
implementation 'com.redhat.exhort:exhort-java-api:0.0.7-SNAPSHOT'
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
// https://mvnrepository.com/artifact/com.github.package-url/packageurl-java
implementation group: 'com.github.package-url', name: 'packageurl-java', version: '1.4.1'
Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ideaVersion = 2021.1
projectVersion=0.9.2-SNAPSHOT
ideaVersion = 2022.1
projectVersion=1.0.0-SNAPSHOT
jetBrainsToken=invalid
jetBrainsChannel=stable
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.util.IncorrectOperationException;
import org.jboss.tools.intellij.report.AnalyticsReportUtils;
import org.jboss.tools.intellij.stackanalysis.SaUtils;
import org.jetbrains.annotations.NotNull;

Expand Down Expand Up @@ -57,7 +58,8 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws
JsonObject manifestDetails = saUtils.performSA(vf);
if (manifestDetails != null) {
try {
saUtils.openCustomEditor(FileEditorManager.getInstance(project), manifestDetails);
AnalyticsReportUtils analyticsReportUtils = new AnalyticsReportUtils();
analyticsReportUtils.openCustomEditor(FileEditorManager.getInstance(project), manifestDetails);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
Expand Down
5 changes: 0 additions & 5 deletions src/main/java/org/jboss/tools/intellij/exhort/ApiService.java
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,5 @@ private void setRequestProperties(final String manifestName) {
if (!"go.mod".equals(manifestName) && !"requirements.txt".equals(manifestName)) {
System.clearProperty("MATCH_MANIFEST_VERSIONS");
}
if (settings.snykToken != null && !settings.snykToken.isBlank()) {
System.setProperty("EXHORT_SNYK_TOKEN", settings.snykToken);
} else {
System.clearProperty("EXHORT_SNYK_TOKEN");
}
}
}
167 changes: 167 additions & 0 deletions src/main/java/org/jboss/tools/intellij/image/ApiService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*******************************************************************************
* Copyright (c) 2024 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/

package org.jboss.tools.intellij.image;

import com.intellij.ide.plugins.PluginManagerCore;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.Service;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.PluginDescriptor;
import com.intellij.openapi.extensions.PluginId;
import com.redhat.exhort.Api;
import com.redhat.exhort.api.AnalysisReport;
import com.redhat.exhort.image.ImageRef;
import com.redhat.exhort.impl.ExhortApi;
import org.jboss.tools.intellij.exhort.TelemetryService;
import org.jboss.tools.intellij.settings.ApiSettingsState;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service(Service.Level.APP)
public final class ApiService {

private static final Logger LOG = Logger.getInstance(ApiService.class);

private final Api exhortApi;

public ApiService() {
this.exhortApi = new ExhortApi();
}

static ApiService getInstance() {
return ApplicationManager.getApplication().getService(ApiService.class);
}

Map<BaseImage, ImageRef> getImageRefs(final Collection<BaseImage> images) {
setServiceEnvironment();
return images.stream().collect(Collectors.toMap(
Function.identity(),
image -> new ImageRef(image.getImageName(), image.getPlatform())
));
}

Map<ImageRef, AnalysisReport> getImageAnalysis(final Set<ImageRef> imageRefs) {
var telemetryMsg = TelemetryService.instance().action("image-analysis");
telemetryMsg.property("ecosystem", "image");
telemetryMsg.property("platform", System.getProperty("os.name"));
telemetryMsg.property("images", String.join(";", imageRefs.toString()));
telemetryMsg.property("rhda_token", ApiSettingsState.getInstance().rhdaToken);

try {
setServiceEnvironment();
var imageReports = exhortApi.imageAnalysis(imageRefs);
var reports = imageReports.get();
telemetryMsg.send();
return reports;
} catch (IOException | InterruptedException | ExecutionException ex) {
telemetryMsg.error(ex);
telemetryMsg.send();
throw new RuntimeException(ex);
} catch (IllegalArgumentException ex) {
telemetryMsg.error(ex);
telemetryMsg.send();
LOG.warn("Invalid image reference submitted.", ex);
} catch (CompletionException ex) {
telemetryMsg.error(ex);
telemetryMsg.send();
LOG.warn("Invalid vulnerability report returned.", ex);
}
return null;
}

Path getImageAnalysisReport(final Set<ImageRef> imageRefs) {
var telemetryMsg = TelemetryService.instance().action("image-analysis-report");
telemetryMsg.property("ecosystem", "image");
telemetryMsg.property("platform", System.getProperty("os.name"));
telemetryMsg.property("images", String.join(";", imageRefs.toString()));
telemetryMsg.property("rhda_token", ApiSettingsState.getInstance().rhdaToken);

try {
setServiceEnvironment();
var htmlContent = exhortApi.imageAnalysisHtml(imageRefs);
var tmpFile = Files.createTempFile("exhort_image_", ".html");
Files.write(tmpFile, htmlContent.get());
telemetryMsg.send();
return tmpFile;
} catch (IOException | InterruptedException | ExecutionException exc) {
telemetryMsg.error(exc);
telemetryMsg.send();
throw new RuntimeException(exc);
}
}

private void setServiceEnvironment() {
var ideName = ApplicationInfo.getInstance().getFullApplicationName();
PluginDescriptor pluginDescriptor = PluginManagerCore.getPlugin(PluginId.getId("org.jboss.tools.intellij.analytics"));
if (pluginDescriptor != null) {
var pluginName = pluginDescriptor.getName() + " " + pluginDescriptor.getVersion();
System.setProperty("RHDA_SOURCE", ideName + " / " + pluginName);
} else {
System.setProperty("RHDA_SOURCE", ideName);
}

var settings = ApiSettingsState.getInstance();
System.setProperty("RHDA_TOKEN", settings.rhdaToken);

if (settings.syftPath != null && !settings.syftPath.isBlank()) {
System.setProperty("EXHORT_SYFT_PATH", settings.syftPath);
} else {
System.clearProperty("EXHORT_SYFT_PATH");
}

if (settings.syftConfigPath != null && !settings.syftConfigPath.isBlank()) {
System.setProperty("EXHORT_SYFT_CONFIG_PATH", settings.syftConfigPath);
} else {
System.clearProperty("EXHORT_SYFT_CONFIG_PATH");
}

if (settings.skopeoPath != null && !settings.skopeoPath.isBlank()) {
System.setProperty("EXHORT_SKOPEO_PATH", settings.skopeoPath);
} else {
System.clearProperty("EXHORT_SKOPEO_PATH");
}

if (settings.skopeoConfigPath != null && !settings.skopeoConfigPath.isBlank()) {
System.setProperty("EXHORT_SKOPEO_CONFIG_PATH", settings.skopeoConfigPath);
} else {
System.clearProperty("EXHORT_SKOPEO_CONFIG_PATH");
}

if (settings.dockerPath != null && !settings.dockerPath.isBlank()) {
System.setProperty("EXHORT_DOCKER_PATH", settings.dockerPath);
} else {
System.clearProperty("EXHORT_DOCKER_PATH");
}

if (settings.podmanPath != null && !settings.podmanPath.isBlank()) {
System.setProperty("EXHORT_PODMAN_PATH", settings.podmanPath);
} else {
System.clearProperty("EXHORT_PODMAN_PATH");
}

if (settings.imagePlatform != null && !settings.imagePlatform.isBlank()) {
System.setProperty("EXHORT_IMAGE_PLATFORM", settings.imagePlatform);
} else {
System.clearProperty("EXHORT_IMAGE_PLATFORM");
}
}
}
Loading

0 comments on commit 3657233

Please sign in to comment.