-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
398 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
284 changes: 284 additions & 0 deletions
284
core/src/main/java/org/owasp/dependencycheck/analyzer/YarnBerryAuditAnalyzer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,284 @@ | ||
/* | ||
* This file is part of dependency-check-ant. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* Copyright (c) 2021 The OWASP Foundation. All Rights Reserved. | ||
*/ | ||
package org.owasp.dependencycheck.analyzer; | ||
|
||
import org.apache.commons.collections4.MultiValuedMap; | ||
import org.apache.commons.collections4.multimap.HashSetValuedHashMap; | ||
import org.apache.commons.io.IOUtils; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.json.JSONException; | ||
import org.json.JSONObject; | ||
import org.owasp.dependencycheck.Engine; | ||
import org.owasp.dependencycheck.analyzer.exception.AnalysisException; | ||
import org.owasp.dependencycheck.analyzer.exception.SearchException; | ||
import org.owasp.dependencycheck.analyzer.exception.UnexpectedAnalysisException; | ||
import org.owasp.dependencycheck.data.nodeaudit.Advisory; | ||
import org.owasp.dependencycheck.data.nodeaudit.NpmPayloadBuilder; | ||
import org.owasp.dependencycheck.dependency.Dependency; | ||
import org.owasp.dependencycheck.exception.InitializationException; | ||
import org.owasp.dependencycheck.utils.FileFilterBuilder; | ||
import org.owasp.dependencycheck.utils.Settings; | ||
import org.owasp.dependencycheck.utils.URLConnectionFailureException; | ||
import org.owasp.dependencycheck.utils.processing.ProcessReader; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import us.springett.parsers.cpe.exceptions.CpeValidationException; | ||
|
||
import javax.annotation.concurrent.ThreadSafe; | ||
import javax.json.Json; | ||
import javax.json.JsonException; | ||
import javax.json.JsonObject; | ||
import javax.json.JsonReader; | ||
import java.io.File; | ||
import java.io.FileFilter; | ||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
import java.nio.file.Files; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
|
||
@ThreadSafe | ||
public class YarnBerryAuditAnalyzer extends AbstractNpmAnalyzer { | ||
|
||
/** | ||
* The logger. | ||
*/ | ||
private static final Logger LOGGER = LoggerFactory.getLogger(YarnBerryAuditAnalyzer.class); | ||
|
||
/** | ||
* The file name to scan. | ||
*/ | ||
public static final String YARN_PACKAGE_LOCK = "yarn.lock"; | ||
|
||
/** | ||
* Filter that detects files named "yarn.lock" | ||
*/ | ||
private static final FileFilter LOCK_FILE_FILTER = FileFilterBuilder.newInstance() | ||
.addFilenames(YARN_PACKAGE_LOCK).build(); | ||
|
||
/** | ||
* The path to the `yarn` executable. | ||
*/ | ||
private String yarnPath; | ||
|
||
/** | ||
* Analyzes the yarn lock file to determine vulnerable dependencies. Uses | ||
* yarn npm audit to generate the advisories. | ||
* | ||
* @param dependency the yarn lock file | ||
* @param engine the analysis engine | ||
* @throws AnalysisException thrown if there is an error analyzing the file | ||
*/ | ||
@Override | ||
protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException { | ||
if (dependency.getDisplayFileName().equals(dependency.getFileName())) { | ||
engine.removeDependency(dependency); | ||
} | ||
final File packageLock = dependency.getActualFile(); | ||
if (!packageLock.isFile() || packageLock.length() == 0 || !shouldProcess(packageLock)) { | ||
return; | ||
} | ||
final List<Advisory> advisories; | ||
final MultiValuedMap<String, String> dependencyMap = new HashSetValuedHashMap<>(); | ||
advisories = analyzePackage(dependency); | ||
try { | ||
processResults(advisories, engine, dependency, dependencyMap); | ||
} catch (CpeValidationException ex) { | ||
throw new UnexpectedAnalysisException(ex); | ||
} | ||
} | ||
|
||
@Override | ||
protected String getAnalyzerEnabledSettingKey() { | ||
return Settings.KEYS.ANALYZER_YARN_BERRY_AUDIT_ENABLED; | ||
} | ||
|
||
@Override | ||
protected FileFilter getFileFilter() { | ||
return LOCK_FILE_FILTER; | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return "Yarn Audit Analyzer"; | ||
} | ||
|
||
@Override | ||
public AnalysisPhase getAnalysisPhase() { | ||
return AnalysisPhase.FINDING_ANALYSIS; | ||
} | ||
|
||
/** | ||
* Initializes the analyzer once before any analysis is performed. | ||
* | ||
* @param engine a reference to the dependency-check engine | ||
* @throws InitializationException if there's an error during initialization | ||
*/ | ||
@Override | ||
protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException { | ||
super.prepareFileTypeAnalyzer(engine); | ||
if (!isEnabled()) { | ||
LOGGER.debug("{} Analyzer is disabled skipping yarn executable check", getName()); | ||
return; | ||
} | ||
final List<String> args = new ArrayList<>(); | ||
args.add(getYarn()); | ||
args.add("--help"); | ||
final ProcessBuilder builder = new ProcessBuilder(args); | ||
LOGGER.debug("Launching: {}", args); | ||
try { | ||
final Process process = builder.start(); | ||
try (ProcessReader processReader = new ProcessReader(process)) { | ||
processReader.readAll(); | ||
final int exitValue = process.waitFor(); | ||
final int expectedExitValue = 0; | ||
final int yarnExecutableNotFoundExitValue = 127; | ||
switch (exitValue) { | ||
case expectedExitValue: | ||
LOGGER.debug("{} is enabled.", getName()); | ||
break; | ||
case yarnExecutableNotFoundExitValue: | ||
default: | ||
this.setEnabled(false); | ||
LOGGER.warn("The {} has been disabled. Yarn executable was not found.", getName()); | ||
} | ||
} | ||
} catch (Exception ex) { | ||
this.setEnabled(false); | ||
LOGGER.warn("The {} has been disabled. Yarn executable was not found.", getName()); | ||
throw new InitializationException("Unable to read yarn audit output.", ex); | ||
} | ||
} | ||
|
||
/** | ||
* Attempts to determine the path to `yarn`. | ||
* | ||
* @return the path to `yarn` | ||
*/ | ||
private String getYarn() { | ||
final String value; | ||
synchronized (this) { | ||
if (yarnPath == null) { | ||
final String path = getSettings().getString(Settings.KEYS.ANALYZER_YARN_PATH); | ||
if (path == null) { | ||
yarnPath = "yarn"; | ||
} else { | ||
final File yarnFile = new File(path); | ||
if (yarnFile.isFile()) { | ||
yarnPath = yarnFile.getAbsolutePath(); | ||
} else { | ||
LOGGER.warn("Provided path to `yarn` executable is invalid."); | ||
yarnPath = "yarn"; | ||
} | ||
} | ||
} | ||
value = yarnPath; | ||
} | ||
return value; | ||
} | ||
|
||
private List<JSONObject> fetchYarnAdvisories(Dependency dependency, boolean skipDevDependencies) throws AnalysisException { | ||
final File folder = dependency.getActualFile().getParentFile(); | ||
if (!folder.isDirectory()) { | ||
throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath())); | ||
} | ||
try { | ||
final List<String> args = new ArrayList<>(); | ||
|
||
args.add(getYarn()); | ||
args.add("npm"); | ||
args.add("audit"); | ||
if (skipDevDependencies) { | ||
args.add("--environment"); | ||
args.add("production"); | ||
} | ||
args.add("--all"); | ||
args.add("--recursive"); | ||
args.add("--json"); | ||
final ProcessBuilder builder = new ProcessBuilder(args); | ||
builder.directory(folder); | ||
LOGGER.debug("Launching: {}", args); | ||
// Workaround 64k limitation of InputStream, redirect stdout to a file that we will read later | ||
// instead of reading directly stdout from Process's InputStream which is topped at 64k | ||
|
||
final File tmpFile = getSettings().getTempFile("yarn_audit", "json"); | ||
builder.redirectOutput(tmpFile); | ||
final Process process = builder.start(); | ||
try (ProcessReader processReader = new ProcessReader(process)) { | ||
processReader.readAll(); | ||
final String errOutput = processReader.getError(); | ||
|
||
if (!StringUtils.isBlank(errOutput)) { | ||
LOGGER.debug("Process Error Out: {}", errOutput); | ||
LOGGER.debug("Process Out: {}", processReader.getOutput()); | ||
} | ||
final String advisoriesJsons = new String(Files.readAllBytes(tmpFile.toPath()), StandardCharsets.UTF_8); | ||
LOGGER.debug("Advisories JSON: {}", advisoriesJsons); | ||
String[] advisoriesJsonArray = advisoriesJsons.split("\n"); | ||
List<JSONObject> advisories = new ArrayList<>(); | ||
for (String advisoriesJson : advisoriesJsonArray) { | ||
advisories.add(new JSONObject(advisoriesJson)); | ||
} | ||
|
||
return advisories; | ||
} catch (JSONException e) { | ||
throw new AnalysisException("Failed to parse the response from NPM Audit API " | ||
+ "(YarnBerryAuditAnalyzer).", e); | ||
} catch (InterruptedException ex) { | ||
Thread.currentThread().interrupt(); | ||
throw new AnalysisException("Yarn audit process was interrupted.", ex); | ||
} | ||
} catch (IOException ioe) { | ||
throw new AnalysisException("yarn audit failure; this error can be ignored if you are not analyzing projects with a yarn lockfile.", ioe); | ||
} | ||
} | ||
|
||
/** | ||
* Analyzes the package and yarn lock files by calling yarn npm audit and returning the identified advisories. | ||
* | ||
* @param dependency a reference to the dependency-object for the yarn.lock | ||
* @return a list of advisories | ||
*/ | ||
private List<Advisory> analyzePackage(Dependency dependency) throws AnalysisException { | ||
try { | ||
final boolean skipDevDependencies = getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_SKIPDEV, false); | ||
final List<JSONObject> advisoriesJson = fetchYarnAdvisories(dependency, skipDevDependencies); | ||
final List<Advisory> advisories = new ArrayList<>(); | ||
for (JSONObject advisoryJson : advisoriesJson) { | ||
final Advisory advisory = new Advisory(); | ||
JSONObject object = advisoryJson.getJSONObject("children"); | ||
advisory.setGhsaId(object.getString("ID")); | ||
advisory.setTitle(object.optString("Issue", null)); | ||
advisory.setOverview(object.optString("URL", null)); | ||
advisory.setSeverity(object.optString("Severity", null)); | ||
advisory.setVulnerableVersions(object.optString("Vulnerable Versions", null)); | ||
advisory.setModuleName(advisoryJson.optString("value", null)); | ||
advisory.setCwes(new ArrayList<>()); | ||
advisories.add(advisory); | ||
} | ||
return advisories; | ||
} catch (JSONException e) { | ||
throw new AnalysisException("Failed to parse the response from NPM Audit API " | ||
+ "(YarnBerryAuditAnalyzer).", e); | ||
} catch (SearchException ex) { | ||
LOGGER.error("YarnBerryAuditAnalyzer failed on {}", dependency.getActualFilePath()); | ||
throw ex; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.