From 12378b0505f113b5f120a5f7ff4423df251e1efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0est=C3=A1k=20V=C3=ADt?= Date: Fri, 31 Jan 2020 15:12:34 +0100 Subject: [PATCH] Initial commit --- .gitignore | 19 ++ README.md | 17 ++ pom.xml | 49 +++++ .../odc/yocto/AbstractYoctoAnalyzer.java | 182 ++++++++++++++++++ .../security/odc/yocto/ControlFileParser.java | 110 +++++++++++ .../com/ysoft/security/odc/yocto/IpkFile.java | 67 +++++++ .../ysoft/security/odc/yocto/IpkManifest.java | 50 +++++ .../com/ysoft/security/odc/yocto/Key.java | 40 ++++ .../security/odc/yocto/YoctoAnalyzer.java | 63 ++++++ ...ctoFilenameVersionSuppressionAnalyzer.java | 45 +++++ ...tchedVulnerabilitySuppressionAnalyzer.java | 54 ++++++ ...rg.owasp.dependencycheck.analyzer.Analyzer | 3 + .../odc/yocto/ControlFileParserTest.java | 40 ++++ .../security/odc/yocto/IpkManifestTest.java | 56 ++++++ src/test/resources/example.control | 12 ++ 15 files changed, 807 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/com/ysoft/security/odc/yocto/AbstractYoctoAnalyzer.java create mode 100644 src/main/java/com/ysoft/security/odc/yocto/ControlFileParser.java create mode 100644 src/main/java/com/ysoft/security/odc/yocto/IpkFile.java create mode 100644 src/main/java/com/ysoft/security/odc/yocto/IpkManifest.java create mode 100644 src/main/java/com/ysoft/security/odc/yocto/Key.java create mode 100644 src/main/java/com/ysoft/security/odc/yocto/YoctoAnalyzer.java create mode 100644 src/main/java/com/ysoft/security/odc/yocto/YoctoFilenameVersionSuppressionAnalyzer.java create mode 100644 src/main/java/com/ysoft/security/odc/yocto/YoctoPatchedVulnerabilitySuppressionAnalyzer.java create mode 100644 src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer create mode 100644 src/test/java/com/ysoft/security/odc/yocto/ControlFileParserTest.java create mode 100644 src/test/java/com/ysoft/security/odc/yocto/IpkManifestTest.java create mode 100644 src/test/resources/example.control diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51ab68e --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Created by .ignore support plugin (hsz.mobi) +### Maven template +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) +!/.mvn/wrapper/maven-wrapper.jar + + +.idea +sample +*.iml diff --git a/README.md b/README.md new file mode 100644 index 0000000..1735462 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +A plugin for OWASP Dependency Check that analyzes IPK files from YOCTO. + +Useful for finding known vulnerabilities and licenses. + +The plugin automatically suppresses CVEs mentioned in source section, as it expects any mention of a CVE in this section is a patch fixing the CVE. + +## Requirements + +This plugin calls `tar` and `ar` utilities. You need them on $PATH. + +Tested with Debian, but it will likely work with other distributions or even with Windows if these two utilities are on $PATH (or %PATH% :) ). + +## Howto + +1. Build JAR file using `mvn package`. +2. Add the JAR file to `plugins` directory of OWASP Dependency Check (CLI version). +3. Run the ODC on IPK files with com.ysoft.yocto.enabled=true. \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2f2e63a --- /dev/null +++ b/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + com.ysoft.security + odc-yocto-analyzer + 1.2.1 + + + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + + + + org.owasp + dependency-check-core + 5.2.4 + provided + + + junit + junit + 4.12 + test + + + com.google.guava + guava + 21.0 + test + + + + diff --git a/src/main/java/com/ysoft/security/odc/yocto/AbstractYoctoAnalyzer.java b/src/main/java/com/ysoft/security/odc/yocto/AbstractYoctoAnalyzer.java new file mode 100644 index 0000000..53ae6e7 --- /dev/null +++ b/src/main/java/com/ysoft/security/odc/yocto/AbstractYoctoAnalyzer.java @@ -0,0 +1,182 @@ +package com.ysoft.security.odc.yocto; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.owasp.dependencycheck.analyzer.AbstractFileTypeAnalyzer; +import org.owasp.dependencycheck.utils.InvalidSettingException; +import org.owasp.dependencycheck.utils.Settings; + +import static com.ysoft.security.odc.yocto.ControlFileParser.parseControlFile; + +abstract class AbstractYoctoAnalyzer extends AbstractFileTypeAnalyzer { + + public static final String YOCTO_ANALYZER_KEY = "com.ysoft.yocto.enabled"; + public static final String YOCTO_ANALYZER_EXPERIMENTAL_DEBIAN_KEY = "com.ysoft.yocto.experimental.debian.enabled"; + public static final String IPK_SOURCE = "ipk"; + + protected FileFilter getFileFilter() { + return f -> { + final String name = f.getName().toLowerCase(); + return name.endsWith(".ipk") || (isExperimentalDebEnabled() && name.endsWith(".deb")); + }; + } + + @Override + protected String getAnalyzerEnabledSettingKey() { + return YOCTO_ANALYZER_KEY; + } + + private void throwChecked(Throwable e) { + this.throwChecked0(e); + } + + protected boolean isExperimentalDebEnabled() { + return getSettings().getBoolean(YOCTO_ANALYZER_EXPERIMENTAL_DEBIAN_KEY, false); + } + + @SuppressWarnings("unchecked") + private void throwChecked0(Throwable e) { + throw (T) e; + } + + + // In future, we might want to be more universal: https://blog.philippklaus.de/2011/04/have-a-look-into-an-ipk-file-used-by-the-ipkg-or-opkg-manager + + protected IpkFile parseIpkFile(File ipkFile) throws IOException { + return new IpkFile(parseIpkManifest(ipkFile));//listIpkFiles(ipkFile)); + } + + private Set listIpkFiles(File ipkFile) throws IOException { + // TODO: add support for xz, bz etc. + final Process arProcess = createArExtractionProcess(ipkFile.getAbsolutePath(), "data.tar.gz"); + try{ + final Future arStderrFuture = processStream(arProcess.getErrorStream()); + final Process tarProcess = new ProcessBuilder("tar", "-tz").start(); + try { + final Future tarStderrFuture = processStream(tarProcess.getErrorStream()); + final Future copyResult = pipe(arProcess.getInputStream(), tarProcess.getOutputStream()); + final Set fileNames = new HashSet<>(); + try(final BufferedReader reader = new BufferedReader(new InputStreamReader(tarProcess.getInputStream()))){ + String line; + while((line = reader.readLine()) != null){ + fileNames.add(line); + } + } + copyResult.get(); // this throws exception if pipe has failed + final int tarReturnCode = tarProcess.waitFor(); + final int arReturnCode = arProcess.waitFor(); + checkStderr(tarStderrFuture, "tar"); + checkStderr(arStderrFuture, "ar"); + if (tarReturnCode != 0 || arReturnCode != 0) { + throw new IOException("Bad return code (tar: " + tarReturnCode + ", ar: " + arReturnCode + ")"); + } + return fileNames; + } catch (InterruptedException | ExecutionException e) { + throw new IOException(e); + } finally { + tarProcess.destroyForcibly(); + } + }finally{ + arProcess.destroyForcibly(); + } + } + + private IpkManifest parseIpkManifest(File ipkFile) throws IOException { + final Process arProcess = createArExtractionProcess(ipkFile.getAbsolutePath(), "control.tar.gz"); + try{ + final Future arStderrFuture = processStream(arProcess.getErrorStream()); + final Process tarProcess = new ProcessBuilder("tar", "-xzO", "./control").start(); + try{ + final Future tarStderrFuture = processStream(tarProcess.getErrorStream()); + final Future copyResult = pipe(arProcess.getInputStream(), tarProcess.getOutputStream()); + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int len; + final byte[] buff = new byte[1024]; + while((len = tarProcess.getInputStream().read(buff)) != -1){ + baos.write(buff, 0, len); + } + copyResult.get(); // this throws exception if pipe has failed + final int tarReturnCode = tarProcess.waitFor(); + final int arReturnCode = arProcess.waitFor(); + checkStderr(tarStderrFuture, "tar"); + checkStderr(arStderrFuture, "ar"); + if(tarReturnCode != 0 || arReturnCode != 0){ + throw new IOException("Bad return code (tar: "+tarReturnCode+", ar: "+arReturnCode+")"); + } + return new IpkManifest(parseControlFile(baos.toString("utf-8"))); + } catch (InterruptedException | ExecutionException e) { + throw new IOException(e); + } finally { + tarProcess.destroyForcibly(); + } + }finally{ + arProcess.destroyForcibly(); + } + } + + private Process createArExtractionProcess(String arPath, String file) throws IOException { + return new ProcessBuilder("ar", "p", "--", arPath, file).start(); + } + + private void checkStderr(Future stderrFuture, String name) throws ExecutionException, InterruptedException, IOException { + final String result = stderrFuture.get(); + if(!result.equals("")){ + throw new IOException("Process "+name+" has written something to stderr: "+result); + } + } + + private Future processStream(InputStream inputStream) { + final ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + return executor.submit(() -> { + final InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); + final char[] buff = new char[1024]; + int len; + final StringBuilder out = new StringBuilder(); + while ((len = reader.read(buff)) != -1){ + out.append(buff, 0, len); + } + return out.toString(); + }); + }finally { + executor.shutdown(); + } + } + + + protected Future pipe(InputStream in, OutputStream out){ + final ExecutorService executor = Executors.newSingleThreadExecutor(); + try{ + return executor.submit(() -> { + int len; + final byte[] buff = new byte[1024]; + try { + while ((len = in.read(buff)) != -1) { + out.write(buff, 0, len); + } + out.close(); + } catch (IOException e){ + throwChecked(e); + } + return null; + }); + }finally { + executor.shutdown(); + } + } + +} diff --git a/src/main/java/com/ysoft/security/odc/yocto/ControlFileParser.java b/src/main/java/com/ysoft/security/odc/yocto/ControlFileParser.java new file mode 100644 index 0000000..c78cd5e --- /dev/null +++ b/src/main/java/com/ysoft/security/odc/yocto/ControlFileParser.java @@ -0,0 +1,110 @@ +package com.ysoft.security.odc.yocto; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +class ControlFileLineBuilder{ + + private final List lines = new ArrayList<>(); + private final Key key; + + public ControlFileLineBuilder(String line) throws IOException { + final int pos = line.indexOf(':'); + if(pos == -1){ + throw new IOException("Bad line: expected colon: "+line); + } + key = new Key(line.substring(0, pos)); + addLine(line.substring(pos+1)); + } + + private void addLine(String s) { + lines.add(s.trim()); + } + + public Line build() { + return new Line(key, lines.stream().collect(Collectors.joining("\n"))); + } + + public void add(String line) { + addLine(line); + } +} + +class Line{ + private final Key key; + private final String value; + public Line(Key key, String value) { + this.key = key; + this.value = value; + } + public Key getKey() { + return key; + } + public String getValue() { + return value; + } +} + +class ControlFileParagraphBuilder{ + + private ControlFileLineBuilder lineBuilder; + + private Map map = new HashMap<>(); + + public void addLine(String line) throws IOException { + if(line.startsWith(" ") || line.startsWith("\t")){ + if(lineBuilder == null) { + throw new IOException("Bad control file: paragraph cannot start with whitespace."); + } else { + lineBuilder.add(line); + } + }else{ + finishCurrentLine(); + lineBuilder = new ControlFileLineBuilder(line); + } + } + + private void finishCurrentLine() throws IOException { + if(lineBuilder != null){ + add(lineBuilder.build()); + lineBuilder = null; + } + } + + private void add(Line line) throws IOException { + if(map.containsKey(line.getKey())){ + throw new IOException("Duplicate key: "+line.getKey()); + } + map.put(line.getKey(), line.getValue()); + } + + public Map build() throws IOException { + finishCurrentLine(); + final Map res = Collections.unmodifiableMap(map); + map = null; // don't allow reuse + return res; + } +} + +public class ControlFileParser { + + /** + * Works according specification https://www.debian.org/doc/debian-policy/ch-controlfields.html with some limitations: + * * parses only one paragraph + * @param s + * @return + */ + public static Map parseControlFile(String s) throws IOException { + final ControlFileParagraphBuilder builder = new ControlFileParagraphBuilder(); + for(final String line : s.split("\n")){ + builder.addLine(line); + } + return builder.build(); + } + +} diff --git a/src/main/java/com/ysoft/security/odc/yocto/IpkFile.java b/src/main/java/com/ysoft/security/odc/yocto/IpkFile.java new file mode 100644 index 0000000..c549b49 --- /dev/null +++ b/src/main/java/com/ysoft/security/odc/yocto/IpkFile.java @@ -0,0 +1,67 @@ +package com.ysoft.security.odc.yocto; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +public class IpkFile { + + private final IpkManifest manifest; + //private final Set fileNames; + + public IpkFile(IpkManifest manifest/*, Set fileNames*/) { + this.manifest = manifest; + //this.fileNames = fileNames; + } + + public IpkManifest getManifest() { + return manifest; + } + + /*public Set getFileNames() { + return fileNames; + }*/ + + public Set getFixedCves() { + return manifest.getFixedCves(); + } + + public String getPackageName(){ + return manifest.get(IpkManifest.KEY_PACKAGE); + } + + public String getVersion(){ + return manifest.get(IpkManifest.KEY_VERSION); + } + + public String getDescription(){ + return manifest.get(IpkManifest.KEY_DESCRIPTION); + } + + public String getLicense(){ + return manifest.get(IpkManifest.KEY_LICENSE); + } + + public String getHomepage(){ + return manifest.get(IpkManifest.KEY_HOMEPAGE); + } + + public String getOE(){ + return manifest.get(IpkManifest.KEY_OE); + } + + public List getSources(boolean experimentalDebEnabled){ + final String source = experimentalDebEnabled + ? manifest.get(IpkManifest.KEY_SOURCE, "") + : manifest.get(IpkManifest.KEY_SOURCE); + return Arrays.asList(source.split("\\s+")); + } + + @Override + public String toString() { + return "IpkFile{" + + "manifest=" + manifest + + //", fileNames=" + fileNames + + '}'; + } +} diff --git a/src/main/java/com/ysoft/security/odc/yocto/IpkManifest.java b/src/main/java/com/ysoft/security/odc/yocto/IpkManifest.java new file mode 100644 index 0000000..f4a4ad8 --- /dev/null +++ b/src/main/java/com/ysoft/security/odc/yocto/IpkManifest.java @@ -0,0 +1,50 @@ +package com.ysoft.security.odc.yocto; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class IpkManifest { + + public static final Key KEY_PACKAGE = new Key("Package"); + public static final Key KEY_VERSION = new Key("Version"); + public static final Key KEY_DESCRIPTION = new Key("Description"); + public static final Key KEY_LICENSE = new Key("License"); + public static final Key KEY_HOMEPAGE = new Key("Homepage"); + public static final Key KEY_SOURCE = new Key("Source"); + public static final Key KEY_OE = new Key("OE"); + + private static final Pattern CVE_REGEX = Pattern.compile("CVE-[0-9]{4}-[0-9]{4,}"); // slightly more permissible than https://cve.mitre.org/cve/identifiers/syntaxchange.html (leading zeros, number length) + private final Map manifest; + + public IpkManifest(Map manifest) { + this.manifest = manifest; + } + + public String get(Key key){ + return manifest.get(key); + } + + public String get(Key key, String defaultValue){ + return manifest.getOrDefault(key, defaultValue); + } + + public Set getFixedCves() { + final Matcher matcher = CVE_REGEX.matcher(manifest.getOrDefault(KEY_SOURCE, "")); + final Set set = new HashSet<>(); + while(matcher.find()){ + set.add(matcher.group()); + } + return Collections.unmodifiableSet(set); + } + + @Override + public String toString() { + return "IpkManifest{" + + "manifest=" + manifest + + '}'; + } +} diff --git a/src/main/java/com/ysoft/security/odc/yocto/Key.java b/src/main/java/com/ysoft/security/odc/yocto/Key.java new file mode 100644 index 0000000..3f80fdc --- /dev/null +++ b/src/main/java/com/ysoft/security/odc/yocto/Key.java @@ -0,0 +1,40 @@ +package com.ysoft.security.odc.yocto; + +public class Key{ + private final String key; + private final String canonicalKey; + + public Key(String key) { + this.key = key; + this.canonicalKey = key.toLowerCase(); + } + + public String getKey() { + return key; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Key key = (Key) o; + + return canonicalKey.equals(key.canonicalKey); + } + + @Override + public int hashCode() { + return canonicalKey.hashCode(); + } + + @Override + public String toString() { + return "Key(" + key + ")"; + } + +} diff --git a/src/main/java/com/ysoft/security/odc/yocto/YoctoAnalyzer.java b/src/main/java/com/ysoft/security/odc/yocto/YoctoAnalyzer.java new file mode 100644 index 0000000..03de4e3 --- /dev/null +++ b/src/main/java/com/ysoft/security/odc/yocto/YoctoAnalyzer.java @@ -0,0 +1,63 @@ +package com.ysoft.security.odc.yocto; + +import com.github.packageurl.MalformedPackageURLException; +import org.owasp.dependencycheck.Engine; +import org.owasp.dependencycheck.analyzer.AnalysisPhase; +import org.owasp.dependencycheck.analyzer.exception.AnalysisException; +import org.owasp.dependencycheck.dependency.Confidence; +import org.owasp.dependencycheck.dependency.Dependency; +import org.owasp.dependencycheck.dependency.naming.PurlIdentifier; +import org.owasp.dependencycheck.exception.InitializationException; + +import java.io.IOException; +import java.util.List; + +import static org.owasp.dependencycheck.dependency.EvidenceType.*; + +public class YoctoAnalyzer extends AbstractYoctoAnalyzer { + + public AnalysisPhase getAnalysisPhase() { + return AnalysisPhase.INFORMATION_COLLECTION; + } + + @Override + protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {} + + public String getName() { + return "YOCTO analyzer"; + } + + @Override + protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException { + try { + final IpkFile ipkFile = parseIpkFile(dependency.getActualFile()); + + dependency.addSoftwareIdentifier(new PurlIdentifier("yocto", ipkFile.getPackageName(), ipkFile.getVersion(), Confidence.HIGHEST)); + dependency.addEvidence(PRODUCT, IPK_SOURCE, "package name", ipkFile.getPackageName(), Confidence.HIGHEST); + dependency.addEvidence(VENDOR, IPK_SOURCE, "package name", ipkFile.getPackageName()+"_project", Confidence.LOW); + final String oe = ipkFile.getOE(); + final List sources = ipkFile.getSources(isExperimentalDebEnabled()); + if(!sources.isEmpty()){ + dependency.addEvidence(VENDOR, IPK_SOURCE, "source url", sources.get(0), Confidence.MEDIUM); + //productEvidence.addEvidence(IPK_SOURCE, "source url", sources.get(0), Confidence.MEDIUM); + } + if(oe != null){ + dependency.addEvidence(PRODUCT, IPK_SOURCE, "name", oe, Confidence.HIGHEST); + dependency.addEvidence(VENDOR, IPK_SOURCE, "name", oe+"_project", Confidence.LOW); + } + dependency.addEvidence(VERSION, IPK_SOURCE, "package version", ipkFile.getVersion(), Confidence.HIGH); + dependency.addEvidence(VERSION, IPK_SOURCE, "version", ipkFile.getVersion().replaceAll("-r[0-9]+$", ""), Confidence.HIGHEST); + final String homepage = ipkFile.getHomepage(); + dependency.setDescription(ipkFile.getDescription()); + dependency.setLicense(ipkFile.getLicense()); + if(homepage != null && !homepage.equals("")) { + dependency.addEvidence(VENDOR, IPK_SOURCE, "organization url", homepage, Confidence.HIGHEST); + dependency.addEvidence(PRODUCT, IPK_SOURCE, "organization url", homepage, Confidence.HIGHEST); + } + } catch (IOException | MalformedPackageURLException e) { + throw new AnalysisException(e); + } + } + + +} diff --git a/src/main/java/com/ysoft/security/odc/yocto/YoctoFilenameVersionSuppressionAnalyzer.java b/src/main/java/com/ysoft/security/odc/yocto/YoctoFilenameVersionSuppressionAnalyzer.java new file mode 100644 index 0000000..d8e2d05 --- /dev/null +++ b/src/main/java/com/ysoft/security/odc/yocto/YoctoFilenameVersionSuppressionAnalyzer.java @@ -0,0 +1,45 @@ +package com.ysoft.security.odc.yocto; + +import org.owasp.dependencycheck.Engine; +import org.owasp.dependencycheck.analyzer.AbstractAnalyzer; +import org.owasp.dependencycheck.analyzer.AnalysisPhase; +import org.owasp.dependencycheck.analyzer.exception.AnalysisException; +import org.owasp.dependencycheck.dependency.Dependency; +import org.owasp.dependencycheck.dependency.EvidenceType; + +import static org.owasp.dependencycheck.dependency.EvidenceType.*; + +/** + * FileNameAnalyzer tries to get the version from file name. The problem is that it includes “-r0” or something similar from distribution, which is not + * much relevant for ODC. This analyzer removes the version added by FileNameAnalyzer if YOCTO analyzer has added some version evidence. + */ +public class YoctoFilenameVersionSuppressionAnalyzer extends AbstractAnalyzer { + + @Override + protected String getAnalyzerEnabledSettingKey() { + return AbstractYoctoAnalyzer.YOCTO_ANALYZER_KEY; + } + + @Override + public String getName() { + return "YOCTO filename version suppression"; + } + + @Override + public AnalysisPhase getAnalysisPhase() { + return AnalysisPhase.POST_INFORMATION_COLLECTION; + } + + @Override + protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException { + cleanEvidence(dependency, VERSION); + cleanEvidence(dependency, PRODUCT); + cleanEvidence(dependency, VENDOR); + } + + private static void cleanEvidence(Dependency dependency, EvidenceType evidenceType) { + if(dependency.getEvidence(evidenceType).stream().anyMatch(x -> x.getSource().equals(AbstractYoctoAnalyzer.IPK_SOURCE))){ + dependency.getEvidence(evidenceType).stream().filter(item -> item.getSource().equals("file")).forEach(evidence -> dependency.removeEvidence(evidenceType, evidence)); + } + } +} diff --git a/src/main/java/com/ysoft/security/odc/yocto/YoctoPatchedVulnerabilitySuppressionAnalyzer.java b/src/main/java/com/ysoft/security/odc/yocto/YoctoPatchedVulnerabilitySuppressionAnalyzer.java new file mode 100644 index 0000000..6b49e21 --- /dev/null +++ b/src/main/java/com/ysoft/security/odc/yocto/YoctoPatchedVulnerabilitySuppressionAnalyzer.java @@ -0,0 +1,54 @@ +package com.ysoft.security.odc.yocto; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.owasp.dependencycheck.Engine; +import org.owasp.dependencycheck.analyzer.AnalysisPhase; +import org.owasp.dependencycheck.analyzer.exception.AnalysisException; +import org.owasp.dependencycheck.dependency.Dependency; +import org.owasp.dependencycheck.dependency.Vulnerability; +import org.owasp.dependencycheck.exception.InitializationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class YoctoPatchedVulnerabilitySuppressionAnalyzer extends AbstractYoctoAnalyzer { + + private static final Logger LOGGER = LoggerFactory.getLogger(YoctoPatchedVulnerabilitySuppressionAnalyzer.class); + + @Override + public String getName() { + return "YOCTO suppression analyzer"; + } + + @Override + protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {} + + @Override + public AnalysisPhase getAnalysisPhase() { + return AnalysisPhase.POST_FINDING_ANALYSIS; + } + + @Override + protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException { + try{ + final IpkFile ipkFile = parseIpkFile(dependency.getActualFile()); + final Set remainingCvesForSuppression = new HashSet<>(ipkFile.getFixedCves()); + for (final Vulnerability vulnerability : new HashSet<>(dependency.getVulnerabilities())) { // For some reason, this API does not return a copy, which causes ConcurrentModificationException. + final String vulnerabilityName = vulnerability.getName(); + if (remainingCvesForSuppression.contains(vulnerabilityName)) { + dependency.removeVulnerability(vulnerability); + dependency.addSuppressedVulnerability(vulnerability); + remainingCvesForSuppression.remove(vulnerabilityName); + } + } + if(!remainingCvesForSuppression.isEmpty()){ + LOGGER.warn("Dependency {} has some undetected vulnerabilities to suppress that were not matched: {}", dependency.getActualFilePath(), remainingCvesForSuppression); + } + } catch (IOException e) { + throw new AnalysisException(e); + } + } +} diff --git a/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer b/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer new file mode 100644 index 0000000..bc2d962 --- /dev/null +++ b/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer @@ -0,0 +1,3 @@ +com.ysoft.security.odc.yocto.YoctoAnalyzer +com.ysoft.security.odc.yocto.YoctoPatchedVulnerabilitySuppressionAnalyzer +com.ysoft.security.odc.yocto.YoctoFilenameVersionSuppressionAnalyzer diff --git a/src/test/java/com/ysoft/security/odc/yocto/ControlFileParserTest.java b/src/test/java/com/ysoft/security/odc/yocto/ControlFileParserTest.java new file mode 100644 index 0000000..58ef0f3 --- /dev/null +++ b/src/test/java/com/ysoft/security/odc/yocto/ControlFileParserTest.java @@ -0,0 +1,40 @@ +package com.ysoft.security.odc.yocto; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ControlFileParserTest { + + @Test + public void parseControlFile() throws IOException { + try(final InputStream in = getClass().getResourceAsStream("/example.control")) { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final byte[] buff = new byte[1024]; + int len; + while((len = in.read(buff)) != -1){ + baos.write(buff, 0, len); + } + final String s = baos.toString("utf-8"); + final Map res = ControlFileParser.parseControlFile(s); + // single-line + assertEquals(res.get(new Key("Package")), "augeas-lenses"); + // case sensitivity + assertEquals(res.get(new Key("packAge")), "augeas-lenses"); + // missint + assertEquals(res.get(new Key("hackage")), null); + // multiline + assertEquals(res.get(new Key("Description")), "Augeas configuration API\n" + "Augeas configuration API."); + // last line + assertEquals(res.get(new Key("Source")), "http://download.augeas.net/augeas-1.4.0.tar.gz file://add-missing-argz-conditional" + + ".patch file://sepbuildfix.patch file://0001-Unset-need_charset_alias-when-building-for-musl.patch"); + } + } + + +} \ No newline at end of file diff --git a/src/test/java/com/ysoft/security/odc/yocto/IpkManifestTest.java b/src/test/java/com/ysoft/security/odc/yocto/IpkManifestTest.java new file mode 100644 index 0000000..b389bb7 --- /dev/null +++ b/src/test/java/com/ysoft/security/odc/yocto/IpkManifestTest.java @@ -0,0 +1,56 @@ +package com.ysoft.security.odc.yocto; + +import org.junit.Test; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import static org.junit.Assert.*; + +public class IpkManifestTest { + + @Test + public void testCveParseXinetd(){ + final IpkManifest ipkManifest = new IpkManifest(ImmutableMap.of(new Key("Source"), "git://github.com/xinetd-org/xinetd.git;protocol=https " + + "file://xinetd.init file://xinetd.conf file://xinetd.default file://Various-fixes-from-the-previous-maintainer.patch " + + "file://Disable-services-from-inetd.conf-if-a-service-with-t.patch file://xinetd-should-be-able-to-listen-on-IPv6-even-in-ine.patch " + + "file://xinetd-CVE-2013-4342.patch file://xinetd.service")); + assertEquals(ImmutableSet.of("CVE-2013-4342"), ipkManifest.getFixedCves()); + } + + @Test + public void testCveParseOpenSsh(){ + final IpkManifest ipkManifest = new IpkManifest(ImmutableMap.of(new Key("Source"), "Source: ftp://ftp.openbsd" + + ".org/pub/OpenBSD/OpenSSH/portable/openssh-7.1p2.tar.gz file://sshd_config file://ssh_config file://init file://sshd file://sshd" + + ".socket file://sshd@.service file://sshdgenkeys.service file://volatiles.99_sshd file://add-test-support-for-busybox.patch " + + "file://run-ptest file://CVE-2016-1907_upstream_commit.patch file://CVE-2016-1907_2.patch file://CVE-2016-1907_3.patch " + + "file://CVE-2016-3115.patch file://sshdgenkeys.service\n" + + "/home/user/projects/odc-yocto-analyzer/sample/cortexa9t2hf-vfp-neon/openssh-ssh_7.1p2-r0_cortexa9t2hf-vfp-neon.ipk")); + assertEquals(ImmutableSet.of("CVE-2016-1907", "CVE-2016-1907", "CVE-2016-1907", "CVE-2016-3115"), ipkManifest.getFixedCves()); + } + + @Test + public void testCveParseLibXml2(){ + final IpkManifest ipkManifest = new IpkManifest(ImmutableMap.of(new Key("Source"), "Source: ftp://xmlsoft.org/libxml2/libxml2-2.9.2.tar.gz;" + + "name=libtar file://libxml-64bit.patch file://ansidecl.patch file://runtest.patch file://run-ptest file://libxml2-CVE-2014-0191-fix" + + ".patch file://python-sitepackages-dir.patch file://libxml-m4-use-pkgconfig.patch file://configure.ac-fix-cross-compiling-warning" + + ".patch file://0001-CVE-2015-1819-Enforce-the-reader-to-run-in-constant-.patch " + + "file://CVE-2015-7941-1-Stop-parsing-on-entities-boundaries-errors.patch " + + "file://CVE-2015-7941-2-Cleanup-conditional-section-error-handling.patch " + + "file://CVE-2015-8317-Fail-parsing-early-on-if-encoding-conversion-failed.patch " + + "file://CVE-2015-7942-Another-variation-of-overflow-in-Conditional-section.patch " + + "file://CVE-2015-7942-2-Fix-an-error-in-previous-Conditional-section-patch.patch " + + "file://0001-CVE-2015-8035-Fix-XZ-compression-support-loop.patch " + + "file://CVE-2015-7498-Avoid-processing-entities-after-encoding-conversion-.patch " + + "file://0001-CVE-2015-7497-Avoid-an-heap-buffer-overflow-in-xmlDi.patch file://CVE-2015-7499-1-Add-xmlHaltParser-to-stop-the-parser" + + ".patch file://CVE-2015-7499-2-Detect-incoherency-on-GROW.patch file://0001-Fix-a-bug-on-name-parsing-at-the-end-of-current-inpu" + + ".patch file://0001-CVE-2015-7500-Fix-memory-access-error-due-to-incorre.patch " + + "file://0001-CVE-2015-8242-Buffer-overead-with-HTML-parser-in-pus.patch file://0001-CVE-2015-5312-Another-entity-expansion-issue" + + ".patch file://CVE-2015-8241.patch file://CVE-2015-8710.patch http://www.w3.org/XML/Test/xmlts20080827.tar.gz;name=testtar " + + "file://72a46a519ce7326d9a00f0b6a7f2a8e958cd1675.patch file://0001-threads-Define-pthread-definitions-for-glibc-complia.patch")); + assertEquals(ImmutableSet.of("CVE-2014-0191", "CVE-2015-1819", "CVE-2015-7941", "CVE-2015-8317", "CVE-2015-7942", "CVE-2015-8035", + "CVE-2015-7498", "CVE-2015-7497", "CVE-2015-7499", "CVE-2015-7499", "CVE-2015-7500", "CVE-2015-8242", "CVE-2015-5312", + "CVE-2015-8241", "CVE-2015-8710"), ipkManifest.getFixedCves()); + } + +} \ No newline at end of file diff --git a/src/test/resources/example.control b/src/test/resources/example.control new file mode 100644 index 0000000..6131793 --- /dev/null +++ b/src/test/resources/example.control @@ -0,0 +1,12 @@ +Package: augeas-lenses +Version: 1.4.0-r0 +Description: Augeas configuration API + Augeas configuration API. +Section: base +Priority: optional +Maintainer: OE-Core Developers +License: LGPLv2.1+ +Architecture: cortexa9t2hf-vfp-neon +OE: augeas +Homepage: http://augeas.net/ +Source: http://download.augeas.net/augeas-1.4.0.tar.gz file://add-missing-argz-conditional.patch file://sepbuildfix.patch file://0001-Unset-need_charset_alias-when-building-for-musl.patch