Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ public String readLicenseFromManifest() {
return LicenseUtils.readLicenseFile(manifest);
}

protected String getRootComponentName() {
return DEFAULT_PIP_ROOT_COMPONENT_NAME;
}

protected String getRootComponentVersion() {
return DEFAULT_PIP_ROOT_COMPONENT_VERSION;
}

/**
* Returns the path to a requirements-format file that the {@link PythonControllerBase} can
* consume. For requirements.txt this is the manifest itself; for pyproject.toml a temporary file
Expand All @@ -92,8 +100,7 @@ public Content provideStack() throws IOException {
printDependenciesTree(dependencies);
Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive");
sbom.addRoot(
toPurl(DEFAULT_PIP_ROOT_COMPONENT_NAME, DEFAULT_PIP_ROOT_COMPONENT_VERSION),
readLicenseFromManifest());
toPurl(getRootComponentName(), getRootComponentVersion()), readLicenseFromManifest());
for (Map<String, Object> component : dependencies) {
addAllDependencies(sbom.getRoot(), component, sbom);
}
Expand All @@ -120,8 +127,7 @@ public Content provideComponent() throws IOException {
printDependenciesTree(dependencies);
Sbom sbom = SbomFactory.newInstance();
sbom.addRoot(
toPurl(DEFAULT_PIP_ROOT_COMPONENT_NAME, DEFAULT_PIP_ROOT_COMPONENT_VERSION),
readLicenseFromManifest());
toPurl(getRootComponentName(), getRootComponentVersion()), readLicenseFromManifest());
dependencies.forEach(
(component) ->
sbom.addDependency(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package io.github.guacsec.trustifyda.providers;

import com.github.packageurl.PackageURL;
import io.github.guacsec.trustifyda.license.LicenseUtils;
import io.github.guacsec.trustifyda.logging.LoggersFactory;
import io.github.guacsec.trustifyda.utils.PythonControllerBase;
import java.io.IOException;
import java.nio.file.Files;
Expand All @@ -25,6 +27,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.tomlj.Toml;
import org.tomlj.TomlArray;
Expand All @@ -33,7 +36,11 @@

public final class PythonPyprojectProvider extends PythonProvider {

private static final Logger log =
LoggersFactory.getLogger(PythonPyprojectProvider.class.getName());

private Set<String> collectedIgnoredDeps;
private TomlParseResult cachedToml;

public PythonPyprojectProvider(Path manifest) {
super(manifest);
Expand All @@ -54,6 +61,77 @@ protected void cleanupRequirementsPath(Path requirementsPath) throws IOException
Files.deleteIfExists(requirementsPath.getParent());
}

private TomlParseResult getToml() throws IOException {
if (cachedToml == null) {
TomlParseResult parsed = Toml.parse(manifest);
if (parsed.hasErrors()) {
throw new IOException(
"Invalid pyproject.toml format: " + parsed.errors().get(0).getMessage());
}
cachedToml = parsed;
}
return cachedToml;
}

@Override
protected String getRootComponentName() {
try {
TomlParseResult toml = getToml();
String name = toml.getString("project.name");
if (name != null && !name.isBlank()) {
return name;
}
String poetryName = toml.getString("tool.poetry.name");
if (poetryName != null && !poetryName.isBlank()) {
return poetryName;
}
} catch (IOException e) {
log.fine("Failed to parse pyproject.toml for root component name: " + e.getMessage());
}
return super.getRootComponentName();
}

@Override
protected String getRootComponentVersion() {
try {
TomlParseResult toml = getToml();
String version = toml.getString("project.version");
if (version != null && !version.isBlank()) {
return version;
}
String poetryVersion = toml.getString("tool.poetry.version");
if (poetryVersion != null && !poetryVersion.isBlank()) {
return poetryVersion;
}
} catch (IOException e) {
log.fine("Failed to parse pyproject.toml for root component version: " + e.getMessage());
}
return super.getRootComponentVersion();
}

@Override
public String readLicenseFromManifest() {
try {
TomlParseResult toml = getToml();
String license = toml.getString("project.license");
if (license != null && !license.isBlank()) {
return license;
}
// PEP 639: license may be in project.license.text
String licenseText = toml.getString("project.license.text");
if (licenseText != null && !licenseText.isBlank()) {
return licenseText;
}
String poetryLicense = toml.getString("tool.poetry.license");
if (poetryLicense != null && !poetryLicense.isBlank()) {
return poetryLicense;
}
} catch (IOException e) {
log.fine("Failed to parse pyproject.toml for license: " + e.getMessage());
}
return LicenseUtils.readLicenseFile(manifest);
}

@Override
protected Set<PackageURL> getIgnoredDependencies(String manifestContent) {
if (collectedIgnoredDeps == null) {
Expand All @@ -69,10 +147,7 @@ protected Set<PackageURL> getIgnoredDependencies(String manifestContent) {
}

List<String> parseDependencyStrings() throws IOException {
TomlParseResult toml = Toml.parse(manifest);
if (toml.hasErrors()) {
throw new IOException("Invalid pyproject.toml format: " + toml.errors().get(0).getMessage());
}
TomlParseResult toml = getToml();

List<String> rawLines = Files.readAllLines(manifest);
collectedIgnoredDeps = new HashSet<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,75 @@ void test_ignored_deps_collected_during_parsing() throws IOException {
assertThat(ignoredNames).doesNotContain("anyio", "requests");
}

@Test
void test_getRootComponentName_reads_pep621_name() {
Path pyprojectPath =
Path.of("src/test/resources/tst_manifests/pip/pip_pyproject_toml_no_ignore/pyproject.toml");
var provider = new PythonPyprojectProvider(pyprojectPath);
assertThat(provider.getRootComponentName()).isEqualTo("test-project");
}

@Test
void test_getRootComponentName_reads_poetry_name() {
Path pyprojectPath =
Path.of("src/test/resources/tst_manifests/pip/pip_pyproject_toml_poetry/pyproject.toml");
var provider = new PythonPyprojectProvider(pyprojectPath);
assertThat(provider.getRootComponentName()).isEqualTo("test-project");
}

@Test
void test_getRootComponentName_falls_back_to_default() {
Path pyprojectPath =
Path.of(
"src/test/resources/tst_manifests/pip/pip_pyproject_toml_no_metadata/pyproject.toml");
var provider = new PythonPyprojectProvider(pyprojectPath);
assertThat(provider.getRootComponentName()).isEqualTo("default-pip-root");
}

@Test
void test_getRootComponentVersion_reads_pep621_version() {
Path pyprojectPath =
Path.of(
"src/test/resources/tst_manifests/pip/pip_pyproject_toml_pep621_license/pyproject.toml");
var provider = new PythonPyprojectProvider(pyprojectPath);
assertThat(provider.getRootComponentVersion()).isEqualTo("2.0.0");
}

@Test
void test_getRootComponentVersion_reads_poetry_version() {
Path pyprojectPath =
Path.of("src/test/resources/tst_manifests/pip/pip_pyproject_toml_poetry/pyproject.toml");
var provider = new PythonPyprojectProvider(pyprojectPath);
assertThat(provider.getRootComponentVersion()).isEqualTo("0.1.0");
}

@Test
void test_getRootComponentVersion_falls_back_to_default() {
Path pyprojectPath =
Path.of(
"src/test/resources/tst_manifests/pip/pip_pyproject_toml_no_metadata/pyproject.toml");
var provider = new PythonPyprojectProvider(pyprojectPath);
assertThat(provider.getRootComponentVersion()).isEqualTo("0.0.0");
}

@Test
void test_readLicenseFromManifest_reads_pep621_license() {
Path pyprojectPath =
Path.of(
"src/test/resources/tst_manifests/pip/pip_pyproject_toml_pep621_license/pyproject.toml");
var provider = new PythonPyprojectProvider(pyprojectPath);
assertThat(provider.readLicenseFromManifest()).isEqualTo("MIT");
}

@Test
void test_readLicenseFromManifest_reads_poetry_license() {
Path pyprojectPath =
Path.of(
"src/test/resources/tst_manifests/pip/pip_pyproject_toml_poetry_license/pyproject.toml");
var provider = new PythonPyprojectProvider(pyprojectPath);
assertThat(provider.readLicenseFromManifest()).isEqualTo("Apache-2.0");
}

@Test
void test_provideComponent_generates_correct_media_type() throws IOException {
Path pyprojectPath =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[project]
dependencies = [
"anyio==3.6.2",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
name = "licensed-project"
version = "2.0.0"
license = "MIT"
dependencies = [
"anyio==3.6.2",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[tool.poetry]
name = "poetry-licensed"
version = "1.5.0"
license = "Apache-2.0"

[tool.poetry.dependencies]
python = "^3.9"
anyio = "^3.6.2"
Loading