diff --git a/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/CocoaPodsAnalyzer.java b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/CocoaPodsAnalyzer.java new file mode 100644 index 000000000..9f5c4700a --- /dev/null +++ b/dependency-check-core/src/main/java/org/owasp/dependencycheck/analyzer/CocoaPodsAnalyzer.java @@ -0,0 +1,257 @@ +/* + * This file is part of dependency-check-core. + * + * 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) 2015 Bianca Jiang. All Rights Reserved. + */ +package org.owasp.dependencycheck.analyzer; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.json.JsonObject; +import javax.json.JsonString; +import javax.json.JsonValue; + +import org.apache.commons.io.FileUtils; +import org.owasp.dependencycheck.Engine; +import org.owasp.dependencycheck.analyzer.exception.AnalysisException; +import org.owasp.dependencycheck.dependency.Confidence; +import org.owasp.dependencycheck.dependency.Dependency; +import org.owasp.dependencycheck.dependency.EvidenceCollection; +import org.owasp.dependencycheck.utils.FileFilterBuilder; +import org.owasp.dependencycheck.utils.Settings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Bianca Xue Jiang + * + */ +public class CocoaPodsAnalyzer extends AbstractFileTypeAnalyzer { + + /** + * The logger. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(CocoaPodsAnalyzer.class); + + /** + * The name of the analyzer. + */ + private static final String ANALYZER_NAME = "CocoaPods Package Analyzer"; + + /** + * The phase that this analyzer is intended to run in. + */ + private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION; + + /** + * The file name to scan. + */ + public static final String PODSPEC = "podspec"; + /** + * Filter that detects files named "package.json". + */ + private static final FileFilter PODSPEC_FILTER = FileFilterBuilder.newInstance().addExtensions(PODSPEC).build(); + + + /** + * The capture group #1 is the block variable. + * e.g. "Pod::Spec.new do |spec|" + */ + private static final Pattern PODSPEC_BLOCK_PATTERN + = Pattern.compile("Pod::Spec\\.new\\s+?do\\s+?\\|(.+?)\\|"); + + + /** + * Returns the FileFilter + * + * @return the FileFilter + */ + @Override + protected FileFilter getFileFilter() { + return PODSPEC_FILTER; + } + + @Override + protected void initializeFileTypeAnalyzer() throws Exception { + // NO-OP + } + + /** + * Returns the name of the analyzer. + * + * @return the name of the analyzer. + */ + @Override + public String getName() { + return ANALYZER_NAME; + } + + /** + * Returns the phase that the analyzer is intended to run in. + * + * @return the phase that the analyzer is intended to run in. + */ + @Override + public AnalysisPhase getAnalysisPhase() { + return ANALYSIS_PHASE; + } + + /** + * Returns the key used in the properties file to reference the analyzer's enabled property. + * + * @return the analyzer's enabled property setting key + */ + @Override + protected String getAnalyzerEnabledSettingKey() { + return Settings.KEYS.ANALYZER_NODE_PACKAGE_ENABLED; + } + + @Override + protected void analyzeFileType(Dependency dependency, Engine engine) + throws AnalysisException { + + String contents; + try { + contents = FileUtils.readFileToString(dependency.getActualFile()); + } catch (IOException e) { + throw new AnalysisException( + "Problem occurred while reading dependency file.", e); + } + final Matcher matcher = PODSPEC_BLOCK_PATTERN.matcher(contents); + if (matcher.find()) { + contents = contents.substring(matcher.end()); + final String blockVariable = matcher.group(1); + + final EvidenceCollection vendor = dependency.getVendorEvidence(); + final EvidenceCollection product = dependency.getProductEvidence(); + final EvidenceCollection version = dependency.getVersionEvidence(); + + final String name = addStringEvidence(product, contents, blockVariable, "name", "name", Confidence.HIGHEST); + if (!name.isEmpty()) { + vendor.addEvidence(PODSPEC, "name_project", name, Confidence.LOW); + } + addStringEvidence(product, contents, blockVariable, "summary", "summary", Confidence.LOW); + + addStringEvidence(vendor, contents, blockVariable, "author", "authors?", Confidence.HIGHEST); + addStringEvidence(vendor, contents, blockVariable, "homepage", "homepage", Confidence.HIGHEST); + addStringEvidence(vendor, contents, blockVariable, "license", "licen[cs]es?", Confidence.HIGHEST); + + addStringEvidence(version, contents, blockVariable, "version", "version", Confidence.HIGHEST); + } + + setPackagePath(dependency); + +// final File file = dependency.getActualFile(); +// JsonReader jsonReader; +// try { +// jsonReader = Json.createReader(FileUtils.openInputStream(file)); +// } catch (IOException e) { +// throw new AnalysisException( +// "Problem occurred while reading dependency file.", e); +// } +// try { +// final JsonObject json = jsonReader.readObject(); +// final EvidenceCollection productEvidence = dependency.getProductEvidence(); +// final EvidenceCollection vendorEvidence = dependency.getVendorEvidence(); +// if (json.containsKey("name")) { +// final Object value = json.get("name"); +// if (value instanceof JsonString) { +// final String valueString = ((JsonString) value).getString(); +// productEvidence.addEvidence(PODSPEC, "name", valueString, Confidence.HIGHEST); +// vendorEvidence.addEvidence(PODSPEC, "name_project", String.format("%s_project", valueString), Confidence.LOW); +// } else { +// LOGGER.warn("JSON value not string as expected: {}", value); +// } +// } +// addToEvidence(json, productEvidence, "description"); +// addToEvidence(json, vendorEvidence, "author"); +// addToEvidence(json, dependency.getVersionEvidence(), "version"); +// dependency.setDisplayFileName(String.format("%s/%s", file.getParentFile().getName(), file.getName())); +// } catch (JsonException e) { +// LOGGER.warn("Failed to parse package.json file.", e); +// } finally { +// jsonReader.close(); +// } + } + + private String addStringEvidence(EvidenceCollection evidences, String contents, + String blockVariable, String field, String fieldPattern, Confidence confidence) { + String value = ""; + + //capture array value between [ ] + final Matcher arrayMatcher = Pattern.compile( + String.format("\\s*?%s\\.%s\\s*?=\\s*?\\[(.*?)\\]", blockVariable, fieldPattern), Pattern.CASE_INSENSITIVE).matcher(contents); + if(arrayMatcher.find()) { + String arrayValue = arrayMatcher.group(1); + value = arrayValue.replaceAll("['\"]", "").trim(); //strip quotes + } + //capture single value between quotes + else { + final Matcher matcher = Pattern.compile( + String.format("\\s*?%s\\.%s\\s*?=\\s*?(['\"])(.*?)\\1", blockVariable, fieldPattern), Pattern.CASE_INSENSITIVE).matcher(contents); + if (matcher.find()) { + value = matcher.group(2); + } + } + if(value.length() > 0) + evidences.addEvidence(PODSPEC, field, value, confidence); + + return value; + } + + private void setPackagePath(Dependency dep) { + File file = new File(dep.getFilePath()); + String parent = file.getParent(); + if(parent != null) + dep.setPackagePath(parent); + } + + /** + * Adds information to an evidence collection from the node json configuration. + * + * @param json information from node.js + * @param collection a set of evidence about a dependency + * @param key the key to obtain the data from the json information + */ + private void addToEvidence(JsonObject json, EvidenceCollection collection, String key) { + if (json.containsKey(key)) { + final JsonValue value = json.get(key); + if (value instanceof JsonString) { + collection.addEvidence(PODSPEC, key, ((JsonString) value).getString(), Confidence.HIGHEST); + } else if (value instanceof JsonObject) { + final JsonObject jsonObject = (JsonObject) value; + for (final Map.Entry entry : jsonObject.entrySet()) { + final String property = entry.getKey(); + final JsonValue subValue = entry.getValue(); + if (subValue instanceof JsonString) { + collection.addEvidence(PODSPEC, + String.format("%s.%s", key, property), + ((JsonString) subValue).getString(), + Confidence.HIGHEST); + } else { + LOGGER.warn("JSON sub-value not string as expected: {}", subValue); + } + } + } else { + LOGGER.warn("JSON value not string or JSON object as expected: {}", value); + } + } + } +} diff --git a/dependency-check-core/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer b/dependency-check-core/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer index b0d1c95c8..3377ab6bc 100644 --- a/dependency-check-core/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer +++ b/dependency-check-core/src/main/resources/META-INF/services/org.owasp.dependencycheck.analyzer.Analyzer @@ -22,3 +22,4 @@ org.owasp.dependencycheck.analyzer.RubyGemspecAnalyzer org.owasp.dependencycheck.analyzer.RubyBundleInstallDeploymentAnalyzer org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer org.owasp.dependencycheck.analyzer.ComposerLockAnalyzer +org.owasp.dependencycheck.analyzer.CocoaPodsAnalyzer diff --git a/dependency-check-core/src/test/java/org/owasp/dependencycheck/analyzer/CocoaPodsAnalyzerTest.java b/dependency-check-core/src/test/java/org/owasp/dependencycheck/analyzer/CocoaPodsAnalyzerTest.java new file mode 100644 index 000000000..e41e9155b --- /dev/null +++ b/dependency-check-core/src/test/java/org/owasp/dependencycheck/analyzer/CocoaPodsAnalyzerTest.java @@ -0,0 +1,89 @@ +package org.owasp.dependencycheck.analyzer; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.owasp.dependencycheck.BaseTest; +import org.owasp.dependencycheck.analyzer.exception.AnalysisException; +import org.owasp.dependencycheck.dependency.Dependency; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.io.File; + +/** + * Unit tests for CocoaPodsAnalyzer. + * + * @author Bianca Jiang + */ +public class CocoaPodsAnalyzerTest extends BaseTest { + + /** + * The analyzer to test. + */ + CocoaPodsAnalyzer analyzer; + + /** + * Correctly setup the analyzer for testing. + * + * @throws Exception thrown if there is a problem + */ + @Before + public void setUp() throws Exception { + analyzer = new CocoaPodsAnalyzer(); + analyzer.setFilesMatched(true); + analyzer.initialize(); + } + + /** + * Cleanup the analyzer's temp files, etc. + * + * @throws Exception thrown if there is a problem + */ + @After + public void tearDown() throws Exception { + analyzer.close(); + analyzer = null; + } + + /** + * Test of getName method, of class PythonDistributionAnalyzer. + */ + @Test + public void testGetName() { + assertThat(analyzer.getName(), is("CocoaPods Package Analyzer")); + } + + /** + * Test of supportsExtension method, of class PythonDistributionAnalyzer. + */ + @Test + public void testSupportsFiles() { + assertThat(analyzer.accept(new File("test.podspec")), is(true)); + } + + /** + * Test of inspect method, of class PythonDistributionAnalyzer. + * + * @throws AnalysisException is thrown when an exception occurs. + */ + @Test + public void testAnalyzePackageJson() throws AnalysisException { + final Dependency result = new Dependency(BaseTest.getResourceAsFile(this, + "swift/cocoapods/EasyPeasy.podspec")); + analyzer.analyze(result, null); + final String vendorString = result.getVendorEvidence().toString(); + + assertThat(vendorString, containsString("Carlos Vidal")); + assertThat(vendorString, containsString("https://github.com/nakiostudio/EasyPeasy")); + assertThat(vendorString, containsString("MIT")); + assertThat(result.getProductEvidence().toString(), containsString("EasyPeasy")); + assertThat(result.getVersionEvidence().toString(), containsString("0.2.3")); + + System.out.println("vendor: " + vendorString); + System.out.println("product: " + result.getProductEvidence().toString()); + System.out.println("version: " + result.getVersionEvidence().toString()); + } +} diff --git a/dependency-check-core/src/test/resources/swift/cocoapods/EasyPeasy.podspec b/dependency-check-core/src/test/resources/swift/cocoapods/EasyPeasy.podspec new file mode 100644 index 000000000..52d0ef7a8 --- /dev/null +++ b/dependency-check-core/src/test/resources/swift/cocoapods/EasyPeasy.podspec @@ -0,0 +1,25 @@ +Pod::Spec.new do |s| + s.name = "EasyPeasy" + s.version = "0.2.3" + s.summary = "EasyPeasy is a Swift framework that eases the creation of + Autolayout constraints programmatically" + s.description = <<-DESC + EasyPeasy is a Swift framework that lets you create Autolayout constraints + programmatically without headaches and never ending boilerplate code. Besides the + basics, **EasyPeasy** resolves most of the constraint conflicts for you and lets + you attach to a constraint conditional closures that are evaluated before applying + a constraint, this lets you apply (or not) a constraint depending on platform, size + classes, orientation... or the state of your controller, easy peasy! + DESC + s.homepage = "https://github.com/nakiostudio/EasyPeasy" + s.license = 'MIT' + s.author = { "Carlos Vidal" => "nakioparkour@gmail.com" } + s.source = { :git => "https://github.com/nakiostudio/EasyPeasy.git", :tag => s.version.to_s } + s.social_media_url = 'https://twitter.com/carlostify' + + s.platform = :ios, '8.0' + s.requires_arc = true + + s.source_files = 'EasyPeasy/**/*' + s.frameworks = 'UIKit' +end