| Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
| DependencyMergingAnalyzer |
|
| 4.2727272727272725;4.273 |
| 1 | /* | |
| 2 | * This file is part of dependency-check-core. | |
| 3 | * | |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); | |
| 5 | * you may not use this file except in compliance with the License. | |
| 6 | * You may obtain a copy of the License at | |
| 7 | * | |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 | |
| 9 | * | |
| 10 | * Unless required by applicable law or agreed to in writing, software | |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, | |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 13 | * See the License for the specific language governing permissions and | |
| 14 | * limitations under the License. | |
| 15 | * | |
| 16 | * Copyright (c) 2012 Jeremy Long. All Rights Reserved. | |
| 17 | */ | |
| 18 | package org.owasp.dependencycheck.analyzer; | |
| 19 | ||
| 20 | import java.io.File; | |
| 21 | import java.util.HashSet; | |
| 22 | import java.util.Iterator; | |
| 23 | import java.util.ListIterator; | |
| 24 | import java.util.Set; | |
| 25 | import org.owasp.dependencycheck.Engine; | |
| 26 | import org.owasp.dependencycheck.analyzer.exception.AnalysisException; | |
| 27 | import org.owasp.dependencycheck.dependency.Dependency; | |
| 28 | import org.owasp.dependencycheck.utils.Settings; | |
| 29 | import org.slf4j.Logger; | |
| 30 | import org.slf4j.LoggerFactory; | |
| 31 | ||
| 32 | /** | |
| 33 | * <p> | |
| 34 | * This analyzer will merge dependencies, created from different source, into a | |
| 35 | * single dependency.</p> | |
| 36 | * | |
| 37 | * @author Jeremy Long | |
| 38 | */ | |
| 39 | 8 | public class DependencyMergingAnalyzer extends AbstractAnalyzer { |
| 40 | ||
| 41 | //<editor-fold defaultstate="collapsed" desc="Constants and Member Variables"> | |
| 42 | /** | |
| 43 | * The Logger. | |
| 44 | */ | |
| 45 | 1 | private static final Logger LOGGER = LoggerFactory.getLogger(DependencyMergingAnalyzer.class); |
| 46 | /** | |
| 47 | * a flag indicating if this analyzer has run. This analyzer only runs once. | |
| 48 | */ | |
| 49 | 8 | private boolean analyzed = false; |
| 50 | ||
| 51 | /** | |
| 52 | * Returns a flag indicating if this analyzer has run. This analyzer only | |
| 53 | * runs once. Note this is currently only used in the unit tests. | |
| 54 | * | |
| 55 | * @return a flag indicating if this analyzer has run. This analyzer only | |
| 56 | * runs once | |
| 57 | */ | |
| 58 | protected boolean getAnalyzed() { | |
| 59 | 0 | return analyzed; |
| 60 | } | |
| 61 | ||
| 62 | //</editor-fold> | |
| 63 | //<editor-fold defaultstate="collapsed" desc="All standard implementation details of Analyzer"> | |
| 64 | /** | |
| 65 | * The name of the analyzer. | |
| 66 | */ | |
| 67 | private static final String ANALYZER_NAME = "Dependency Merging Analyzer"; | |
| 68 | /** | |
| 69 | * The phase that this analyzer is intended to run in. | |
| 70 | */ | |
| 71 | 1 | private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.POST_INFORMATION_COLLECTION; |
| 72 | ||
| 73 | /** | |
| 74 | * Returns the name of the analyzer. | |
| 75 | * | |
| 76 | * @return the name of the analyzer. | |
| 77 | */ | |
| 78 | @Override | |
| 79 | public String getName() { | |
| 80 | 26 | return ANALYZER_NAME; |
| 81 | } | |
| 82 | ||
| 83 | /** | |
| 84 | * Returns the phase that the analyzer is intended to run in. | |
| 85 | * | |
| 86 | * @return the phase that the analyzer is intended to run in. | |
| 87 | */ | |
| 88 | @Override | |
| 89 | public AnalysisPhase getAnalysisPhase() { | |
| 90 | 6 | return ANALYSIS_PHASE; |
| 91 | } | |
| 92 | ||
| 93 | /** | |
| 94 | * Does not support parallel processing as it only runs once and then | |
| 95 | * operates on <em>all</em> dependencies. | |
| 96 | * | |
| 97 | * @return whether or not parallel processing is enabled | |
| 98 | * @see #analyze(Dependency, Engine) | |
| 99 | */ | |
| 100 | @Override | |
| 101 | public boolean supportsParallelProcessing() { | |
| 102 | 2 | return false; |
| 103 | } | |
| 104 | ||
| 105 | /** | |
| 106 | * <p> | |
| 107 | * Returns the setting key to determine if the analyzer is enabled.</p> | |
| 108 | * | |
| 109 | * @return the key for the analyzer's enabled property | |
| 110 | */ | |
| 111 | @Override | |
| 112 | protected String getAnalyzerEnabledSettingKey() { | |
| 113 | 2 | return Settings.KEYS.ANALYZER_DEPENDENCY_MERGING_ENABLED; |
| 114 | } | |
| 115 | //</editor-fold> | |
| 116 | ||
| 117 | /** | |
| 118 | * Analyzes a set of dependencies. If they have been found to be the same | |
| 119 | * dependency created by more multiple FileTypeAnalyzers (i.e. a gemspec | |
| 120 | * dependency and a dependency from the Bundle Audit Analyzer. The | |
| 121 | * dependencies are then merged into a single reportable item. | |
| 122 | * | |
| 123 | * @param ignore this analyzer ignores the dependency being analyzed | |
| 124 | * @param engine the engine that is scanning the dependencies | |
| 125 | * @throws AnalysisException is thrown if there is an error reading the JAR | |
| 126 | * file. | |
| 127 | */ | |
| 128 | @Override | |
| 129 | protected synchronized void analyzeDependency(Dependency ignore, Engine engine) throws AnalysisException { | |
| 130 | 4 | if (!analyzed) { |
| 131 | 2 | analyzed = true; |
| 132 | 2 | final Set<Dependency> dependenciesToRemove = new HashSet<Dependency>(); |
| 133 | 2 | final ListIterator<Dependency> mainIterator = engine.getDependencies().listIterator(); |
| 134 | //for (Dependency nextDependency : engine.getDependencies()) { | |
| 135 | 6 | while (mainIterator.hasNext()) { |
| 136 | 4 | final Dependency dependency = mainIterator.next(); |
| 137 | 4 | if (mainIterator.hasNext() && !dependenciesToRemove.contains(dependency)) { |
| 138 | 2 | final ListIterator<Dependency> subIterator = engine.getDependencies().listIterator(mainIterator.nextIndex()); |
| 139 | 4 | while (subIterator.hasNext()) { |
| 140 | 2 | final Dependency nextDependency = subIterator.next(); |
| 141 | 2 | Dependency main = null; |
| 142 | 2 | if ((main = getMainGemspecDependency(dependency, nextDependency)) != null) { |
| 143 | 0 | if (main == dependency) { |
| 144 | 0 | mergeDependencies(dependency, nextDependency, dependenciesToRemove); |
| 145 | } else { | |
| 146 | 0 | mergeDependencies(nextDependency, dependency, dependenciesToRemove); |
| 147 | 0 | break; //since we merged into the next dependency - skip forward to the next in mainIterator |
| 148 | } | |
| 149 | 2 | } else if ((main = getMainSwiftDependency(dependency, nextDependency)) != null) { |
| 150 | 0 | if (main == dependency) { |
| 151 | 0 | mergeDependencies(dependency, nextDependency, dependenciesToRemove); |
| 152 | } else { | |
| 153 | 0 | mergeDependencies(nextDependency, dependency, dependenciesToRemove); |
| 154 | 0 | break; //since we merged into the next dependency - skip forward to the next in mainIterator |
| 155 | } | |
| 156 | } | |
| 157 | 2 | } |
| 158 | } | |
| 159 | 4 | } |
| 160 | //removing dependencies here as ensuring correctness and avoiding ConcurrentUpdateExceptions | |
| 161 | // was difficult because of the inner iterator. | |
| 162 | 2 | engine.getDependencies().removeAll(dependenciesToRemove); |
| 163 | } | |
| 164 | 4 | } |
| 165 | ||
| 166 | /** | |
| 167 | * Adds the relatedDependency to the dependency's related dependencies. | |
| 168 | * | |
| 169 | * @param dependency the main dependency | |
| 170 | * @param relatedDependency a collection of dependencies to be removed from | |
| 171 | * the main analysis loop, this is the source of dependencies to remove | |
| 172 | * @param dependenciesToRemove a collection of dependencies that will be | |
| 173 | * removed from the main analysis loop, this function adds to this | |
| 174 | * collection | |
| 175 | */ | |
| 176 | private void mergeDependencies(final Dependency dependency, final Dependency relatedDependency, final Set<Dependency> dependenciesToRemove) { | |
| 177 | 0 | LOGGER.debug("Merging '{}' into '{}'", relatedDependency.getFilePath(), dependency.getFilePath()); |
| 178 | 0 | dependency.addRelatedDependency(relatedDependency); |
| 179 | 0 | dependency.getVendorEvidence().getEvidence().addAll(relatedDependency.getVendorEvidence().getEvidence()); |
| 180 | 0 | dependency.getProductEvidence().getEvidence().addAll(relatedDependency.getProductEvidence().getEvidence()); |
| 181 | 0 | dependency.getVersionEvidence().getEvidence().addAll(relatedDependency.getVersionEvidence().getEvidence()); |
| 182 | ||
| 183 | 0 | final Iterator<Dependency> i = relatedDependency.getRelatedDependencies().iterator(); |
| 184 | 0 | while (i.hasNext()) { |
| 185 | 0 | dependency.addRelatedDependency(i.next()); |
| 186 | 0 | i.remove(); |
| 187 | } | |
| 188 | 0 | if (dependency.getSha1sum().equals(relatedDependency.getSha1sum())) { |
| 189 | 0 | dependency.addAllProjectReferences(relatedDependency.getProjectReferences()); |
| 190 | } | |
| 191 | 0 | dependenciesToRemove.add(relatedDependency); |
| 192 | 0 | } |
| 193 | ||
| 194 | /** | |
| 195 | * Bundling Ruby gems that are identified from different .gemspec files but | |
| 196 | * denote the same package path. This happens when Ruby bundler installs an | |
| 197 | * application's dependencies by running "bundle install". | |
| 198 | * | |
| 199 | * @param dependency1 dependency to compare | |
| 200 | * @param dependency2 dependency to compare | |
| 201 | * @return true if the the dependencies being analyzed appear to be the | |
| 202 | * same; otherwise false | |
| 203 | */ | |
| 204 | private boolean isSameRubyGem(Dependency dependency1, Dependency dependency2) { | |
| 205 | 2 | if (dependency1 == null || dependency2 == null |
| 206 | 2 | || !dependency1.getFileName().endsWith(".gemspec") |
| 207 | 0 | || !dependency2.getFileName().endsWith(".gemspec") |
| 208 | 0 | || dependency1.getPackagePath() == null |
| 209 | 0 | || dependency2.getPackagePath() == null) { |
| 210 | 2 | return false; |
| 211 | } | |
| 212 | 0 | return dependency1.getPackagePath().equalsIgnoreCase(dependency2.getPackagePath()); |
| 213 | } | |
| 214 | ||
| 215 | /** | |
| 216 | * Ruby gems installed by "bundle install" can have zero or more *.gemspec | |
| 217 | * files, all of which have the same packagePath and should be grouped. If | |
| 218 | * one of these gemspec is from <parent>/specifications/*.gemspec, because | |
| 219 | * it is a stub with fully resolved gem meta-data created by Ruby bundler, | |
| 220 | * this dependency should be the main one. Otherwise, use dependency2 as | |
| 221 | * main. | |
| 222 | * | |
| 223 | * This method returns null if any dependency is not from *.gemspec, or the | |
| 224 | * two do not have the same packagePath. In this case, they should not be | |
| 225 | * grouped. | |
| 226 | * | |
| 227 | * @param dependency1 dependency to compare | |
| 228 | * @param dependency2 dependency to compare | |
| 229 | * @return the main dependency; or null if a gemspec is not included in the | |
| 230 | * analysis | |
| 231 | */ | |
| 232 | private Dependency getMainGemspecDependency(Dependency dependency1, Dependency dependency2) { | |
| 233 | 2 | if (isSameRubyGem(dependency1, dependency2)) { |
| 234 | 0 | final File lFile = dependency1.getActualFile(); |
| 235 | 0 | final File left = lFile.getParentFile(); |
| 236 | 0 | if (left != null && left.getName().equalsIgnoreCase("specifications")) { |
| 237 | 0 | return dependency1; |
| 238 | } | |
| 239 | 0 | return dependency2; |
| 240 | } | |
| 241 | 2 | return null; |
| 242 | } | |
| 243 | ||
| 244 | /** | |
| 245 | * Bundling same swift dependencies with the same packagePath but identified | |
| 246 | * by different file type analyzers. | |
| 247 | * | |
| 248 | * @param dependency1 dependency to test | |
| 249 | * @param dependency2 dependency to test | |
| 250 | * @return <code>true</code> if the dependencies appear to be the same; | |
| 251 | * otherwise <code>false</code> | |
| 252 | */ | |
| 253 | private boolean isSameSwiftPackage(Dependency dependency1, Dependency dependency2) { | |
| 254 | 2 | if (dependency1 == null || dependency2 == null |
| 255 | 2 | || (!dependency1.getFileName().endsWith(".podspec") |
| 256 | 2 | && !dependency1.getFileName().equals("Package.swift")) |
| 257 | 0 | || (!dependency2.getFileName().endsWith(".podspec") |
| 258 | 0 | && !dependency2.getFileName().equals("Package.swift")) |
| 259 | 0 | || dependency1.getPackagePath() == null |
| 260 | 0 | || dependency2.getPackagePath() == null) { |
| 261 | 2 | return false; |
| 262 | } | |
| 263 | 0 | return dependency1.getPackagePath().equalsIgnoreCase(dependency2.getPackagePath()); |
| 264 | } | |
| 265 | ||
| 266 | /** | |
| 267 | * Determines which of the swift dependencies should be considered the | |
| 268 | * primary. | |
| 269 | * | |
| 270 | * @param dependency1 the first swift dependency to compare | |
| 271 | * @param dependency2 the second swift dependency to compare | |
| 272 | * @return the primary swift dependency | |
| 273 | */ | |
| 274 | private Dependency getMainSwiftDependency(Dependency dependency1, Dependency dependency2) { | |
| 275 | 2 | if (isSameSwiftPackage(dependency1, dependency2)) { |
| 276 | 0 | if (dependency1.getFileName().endsWith(".podspec")) { |
| 277 | 0 | return dependency1; |
| 278 | } | |
| 279 | 0 | return dependency2; |
| 280 | } | |
| 281 | 2 | return null; |
| 282 | } | |
| 283 | } |