1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.owasp.dependencycheck.analyzer;
17
18 import com.github.packageurl.MalformedPackageURLException;
19 import com.github.packageurl.PackageURL;
20 import com.github.packageurl.PackageURLBuilder;
21 import java.io.FileFilter;
22 import java.util.List;
23
24 import javax.annotation.concurrent.ThreadSafe;
25
26 import org.owasp.dependencycheck.Engine;
27 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
28 import org.owasp.dependencycheck.dependency.Confidence;
29 import org.owasp.dependencycheck.dependency.Dependency;
30 import org.owasp.dependencycheck.dependency.EvidenceType;
31 import org.owasp.dependencycheck.exception.InitializationException;
32 import org.owasp.dependencycheck.utils.FileFilterBuilder;
33 import org.owasp.dependencycheck.utils.Settings;
34
35 import com.moandjiezana.toml.Toml;
36 import java.io.File;
37 import java.util.Optional;
38
39 import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
40 import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
41 import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
42 import org.owasp.dependencycheck.utils.Checksum;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46
47
48
49
50
51
52 @ThreadSafe
53 @Experimental
54 public class PoetryAnalyzer extends AbstractFileTypeAnalyzer {
55
56
57
58
59 private static final Logger LOGGER = LoggerFactory.getLogger(PoetryAnalyzer.class);
60
61
62
63
64
65 public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.PYTHON;
66
67
68
69
70 private static final String POETRY_LOCK = "poetry.lock";
71
72
73
74 private static final String PYPROJECT_TOML = "pyproject.toml";
75
76
77
78 private static final FileFilter POETRY_LOCK_FILTER = FileFilterBuilder.newInstance()
79 .addFilenames(POETRY_LOCK, PYPROJECT_TOML)
80 .build();
81
82
83
84
85
86
87 @Override
88 public String getName() {
89 return "Poetry Analyzer";
90 }
91
92
93
94
95
96
97 @Override
98 public AnalysisPhase getAnalysisPhase() {
99 return AnalysisPhase.INFORMATION_COLLECTION;
100 }
101
102
103
104
105
106
107 @Override
108 protected String getAnalyzerEnabledSettingKey() {
109 return Settings.KEYS.ANALYZER_POETRY_ENABLED;
110 }
111
112
113
114
115
116
117 @Override
118 protected FileFilter getFileFilter() {
119 return POETRY_LOCK_FILTER;
120 }
121
122
123
124
125
126
127
128
129 @Override
130 protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
131
132 }
133
134
135
136
137
138
139
140
141
142
143 @Override
144 protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
145 LOGGER.debug("Checking file {}", dependency.getActualFilePath());
146
147
148 engine.removeDependency(dependency);
149
150 final Optional<Toml> potentiallyParsedToml = parseDependencyFile(dependency);
151 if (potentiallyParsedToml.isEmpty()) {
152 LOGGER.warn("toml file skipped: {} could not be parsed", dependency.getActualFilePath());
153 return;
154 }
155 final Toml result = potentiallyParsedToml.get();
156 if (PYPROJECT_TOML.equals(dependency.getActualFile().getName())) {
157 if (result.getTable("tool.poetry") == null) {
158 LOGGER.debug("skipping {} as it does not contain `tool.poetry`", dependency.getDisplayFileName());
159 return;
160 }
161
162 final File parentPath = dependency.getActualFile().getParentFile();
163 ensureLock(parentPath);
164
165 return;
166 }
167
168 final List<Toml> projectsLocks = result.getTables("package");
169 if (projectsLocks == null) {
170 return;
171 }
172 projectsLocks.forEach((project) -> {
173 final String name = project.getString("name");
174 final String version = project.getString("version");
175
176 LOGGER.debug(String.format("package, version: %s %s", name, version));
177
178 final Dependency d = new Dependency(dependency.getActualFile(), true);
179 d.setName(name);
180 d.setVersion(version);
181
182 try {
183 final PackageURL purl = PackageURLBuilder.aPackageURL()
184 .withType("pypi")
185 .withName(name)
186 .withVersion(version)
187 .build();
188 d.addSoftwareIdentifier(new PurlIdentifier(purl, Confidence.HIGHEST));
189 } catch (MalformedPackageURLException ex) {
190 LOGGER.debug("Unable to build package url for pypi", ex);
191 d.addSoftwareIdentifier(new GenericIdentifier("pypi:" + name + "@" + version, Confidence.HIGH));
192 }
193
194 d.setPackagePath(String.format("%s:%s", name, version));
195 d.setEcosystem(PythonDistributionAnalyzer.DEPENDENCY_ECOSYSTEM);
196 final String filePath = String.format("%s:%s/%s", dependency.getFilePath(), name, version);
197 d.setFilePath(filePath);
198 d.setSha1sum(Checksum.getSHA1Checksum(filePath));
199 d.setSha256sum(Checksum.getSHA256Checksum(filePath));
200 d.setMd5sum(Checksum.getMD5Checksum(filePath));
201 d.addEvidence(EvidenceType.PRODUCT, POETRY_LOCK, "product", name, Confidence.HIGHEST);
202 d.addEvidence(EvidenceType.VERSION, POETRY_LOCK, "version", version, Confidence.HIGHEST);
203 d.addEvidence(EvidenceType.VENDOR, POETRY_LOCK, "vendor", name, Confidence.HIGHEST);
204 engine.addDependency(d);
205 });
206 }
207
208 private Optional<Toml> parseDependencyFile(Dependency dependency) {
209 try {
210 final Toml toml = new Toml().read(dependency.getActualFile());
211 return Optional.of(toml);
212 } catch (RuntimeException e) {
213 final Optional<String> unparsableFileErrorMessage = Optional.ofNullable(e.getCause())
214 .filter(c -> c instanceof IllegalStateException)
215 .map(Throwable::getMessage)
216 .filter(PoetryAnalyzer::isInvalidKeyErrorMessage);
217
218 if (unparsableFileErrorMessage.isPresent()) {
219 final String message = String.format("Invalid toml file, cannot parse '%s'", dependency.getActualFile());
220 LOGGER.debug(message, e);
221 return Optional.empty();
222 }
223
224 throw e;
225 }
226 }
227
228 private static boolean isInvalidKeyErrorMessage(String m) {
229 return m.startsWith("Invalid key");
230 }
231
232 private void ensureLock(File parent) throws AnalysisException {
233 final File lock = new File(parent, POETRY_LOCK);
234 final File requirements = new File(parent, "requirements.txt");
235 final boolean found = lock.isFile() || requirements.isFile();
236
237 if (!found && !parent.toString().contains("node_modules")) {
238 throw new AnalysisException("Python `pyproject.toml` found and there "
239 + "is not a `poetry.lock` or `requirements.txt` - analysis will be incomplete");
240 }
241 }
242 }