Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ jobs:
go-version: '1.20.1'
- name: Install pnpm
run: npm install -g pnpm
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: get Python location
id: python-location
run: |
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ public class TrustifyExample {
<li><a href="https://www.java.com/">Java</a> - <a href="https://maven.apache.org/">Maven</a></li>
<li><a href="https://www.javascript.com//">JavaScript</a> - <a href="https://www.npmjs.com//">Npm</a></li>
<li><a href="https://go.dev//">Golang</a> - <a href="https://go.dev/blog/using-go-modules//">Go Modules</a></li>
<li><a href="https://www.python.org/">Python</a> - <a href="https://pypi.org/project/pip/">pip Installer</a> (<code>requirements.txt</code>, <code>pyproject.toml</code> with PEP 621 format). <strong>Note:</strong> Poetry-style dependencies (<code>[tool.poetry.dependencies]</code>) are not supported.</li>
<li><a href="https://www.python.org/">Python</a> - <a href="https://pypi.org/project/pip/">pip Installer</a> / <a href="https://docs.astral.sh/uv/">uv</a> (<code>requirements.txt</code>, <code>pyproject.toml</code> with PEP 621 format). <strong>Note:</strong> Poetry-style dependencies (<code>[tool.poetry.dependencies]</code>) are not supported.</li>
<li><a href="https://gradle.org//">Gradle</a> - <a href="https://gradle.org/install//">Gradle Installation</a></li>
<li><a href="https://www.rust-lang.org/">Rust</a> - <a href="https://doc.rust-lang.org/cargo/">Cargo</a></li>

Expand Down Expand Up @@ -393,6 +393,7 @@ System.setProperty("TRUSTIFY_DA_PYTHON3_PATH", "/path/to/python3");
System.setProperty("TRUSTIFY_DA_PIP3_PATH", "/path/to/pip3");
System.setProperty("TRUSTIFY_DA_PYTHON_PATH", "/path/to/python");
System.setProperty("TRUSTIFY_DA_PIP_PATH", "/path/to/pip");
System.setProperty("TRUSTIFY_DA_UV_PATH", "/path/to/custom/uv");
// Configure proxy for all requests
System.setProperty("TRUSTIFY_DA_PROXY_URL", "http://proxy.example.com:8080");
// Configure Maven settings and repository
Expand Down Expand Up @@ -504,6 +505,11 @@ following keys for setting custom paths for the said executables.
<td>TRUSTIFY_DA_PIP_PATH</td>
</tr>
<tr>
<td><a href="https://docs.astral.sh/uv/">uv Package Manager</a></td>
<td><em>uv</em></td>
<td>TRUSTIFY_DA_UV_PATH</td>
</tr>
<tr>
<td><a href="https://doc.rust-lang.org/cargo/">Cargo Package Manager</a></td>
<td><em>cargo</em></td>
<td>TRUSTIFY_DA_CARGO_PATH</td>
Expand Down Expand Up @@ -600,6 +606,14 @@ TRUSTIFY_DA_GO_MVS_LOGIC_ENABLED=false

Python support works with both `requirements.txt` and `pyproject.toml` manifest files. The `pyproject.toml` provider scans production dependencies from PEP 621 `[project.dependencies]` and Poetry `[tool.poetry.dependencies]` sections. Optional dependencies (`[project.optional-dependencies]`) and Poetry group dependencies (`[tool.poetry.group.*.dependencies]`) are intentionally excluded to focus on runtime dependencies.

##### uv Support

When a `uv.lock` file is present alongside `pyproject.toml`, the client automatically uses the [uv](https://docs.astral.sh/uv/) package manager for dependency resolution instead of pip. Dependency data is collected via `uv pip list --format=json` and `uv pip show`. No additional configuration is required — the provider is selected automatically based on lock file detection.
Comment thread
a-oren marked this conversation as resolved.
Outdated

To use a custom uv binary, set `TRUSTIFY_DA_UV_PATH` to the path of your uv executable.

##### pip Support

By default, Python support assumes that the package is installed using the pip/pip3 binary on the system PATH, or of the customized
Binaries passed to environment variables. If the package is not installed , then an error will be thrown.

Expand Down Expand Up @@ -683,6 +697,12 @@ Options:
- `--output <path>` - Write SBOM JSON to the specified file
- (default) - Print SBOM JSON to stdout

**SBOM for uv-managed Python project**
```shell
# When uv.lock is present alongside pyproject.toml, the uv provider is used automatically
java -jar trustify-da-java-client-cli.jar sbom /path/to/uv-project/pyproject.toml
```

**Image Analysis**
```shell
java -jar trustify-da-java-client-cli.jar image <image_ref> [<image_ref>...] [--summary|--html]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2023-2025 Trustify Dependency Analytics Authors
*
* 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.
*/
package io.github.guacsec.trustifyda.providers;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.function.Function;

/**
* Factory for creating the appropriate {@link PythonProvider} based on lock file presence. Follows
* the same pattern as {@link JavaScriptProviderFactory}.
*/
public final class PythonProviderFactory {

private static final Map<String, Function<Path, PythonProvider>> PYTHON_PROVIDERS =
Map.of(PythonUvProvider.LOCK_FILE, PythonUvProvider::new);

/**
* Creates a Python provider for {@code pyproject.toml} manifests by checking for known lock files
* in the manifest directory. When {@code uv.lock} is present, returns a {@link PythonUvProvider};
* otherwise falls back to {@link PythonPyprojectProvider} (pip-based).
*
* @param manifestPath the path to the pyproject.toml manifest
* @return the matching Python provider
*/
public static PythonProvider create(final Path manifestPath) {
Comment thread
a-oren marked this conversation as resolved.
var manifestDir = manifestPath.getParent();

for (var entry : PYTHON_PROVIDERS.entrySet()) {
if (Files.isRegularFile(manifestDir.resolve(entry.getKey()))) {
return entry.getValue().apply(manifestPath);
}
}

// Unlike JavaScript, pip fallback is valid — no lock file required
return new PythonPyprojectProvider(manifestPath);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.github.guacsec.trustifyda.sbom.SbomFactory;
import io.github.guacsec.trustifyda.tools.Operations;
import io.github.guacsec.trustifyda.utils.Environment;
import io.github.guacsec.trustifyda.utils.PyprojectTomlUtils;
import io.github.guacsec.trustifyda.utils.PythonControllerBase;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
Expand All @@ -42,10 +43,8 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.tomlj.Toml;
import org.tomlj.TomlArray;
import org.tomlj.TomlParseResult;
import org.tomlj.TomlTable;

/**
* Provider for Python projects using {@code pyproject.toml} with <a
Expand Down Expand Up @@ -342,22 +341,16 @@ static String canonicalize(String name) {

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

@Override
protected String getRootComponentName() {
try {
TomlParseResult toml = getToml();
String name = toml.getString("project.name");
if (name != null && !name.isBlank()) {
String name = PyprojectTomlUtils.getProjectName(getToml());
if (name != null) {
return name;
}
} catch (IOException e) {
Expand All @@ -369,9 +362,8 @@ protected String getRootComponentName() {
@Override
protected String getRootComponentVersion() {
try {
TomlParseResult toml = getToml();
String version = toml.getString("project.version");
if (version != null && !version.isBlank()) {
String version = PyprojectTomlUtils.getProjectVersion(getToml());
if (version != null) {
return version;
}
} catch (IOException e) {
Expand All @@ -383,16 +375,10 @@ protected String getRootComponentVersion() {
@Override
public String readLicenseFromManifest() {
try {
TomlParseResult toml = getToml();
String license = toml.getString("project.license");
if (license != null && !license.isBlank()) {
String license = PyprojectTomlUtils.getLicense(getToml());
if (license != null) {
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;
}
} catch (IOException e) {
log.fine("Failed to parse pyproject.toml for license: " + e.getMessage());
}
Expand All @@ -414,28 +400,15 @@ protected Set<PackageURL> getIgnoredDependencies(String manifestContent) {
}

private void rejectPoetryDependencies() throws IOException {
TomlParseResult toml = getToml();
TomlTable poetryDeps = toml.getTable("tool.poetry.dependencies");
if (poetryDeps != null) {
if (PyprojectTomlUtils.hasPoetryDependencies(getToml())) {
throw new IllegalStateException(
"Poetry dependencies in pyproject.toml are not supported."
+ " Please use PEP 621 [project.dependencies] format instead.");
}
}

private void collectIgnoredDeps() throws IOException {
TomlParseResult toml = getToml();
List<String> rawLines = Files.readAllLines(manifestPath);
collectedIgnoredDeps = new HashSet<>();

// [project.dependencies] - PEP 621
TomlArray projectDeps = toml.getArray("project.dependencies");
if (projectDeps != null) {
for (int i = 0; i < projectDeps.size(); i++) {
String dep = projectDeps.getString(i);
checkIgnored(rawLines, dep, dep);
}
}
collectedIgnoredDeps = PyprojectTomlUtils.collectIgnoredDeps(manifestPath, getToml());
}

List<String> parseDependencyStrings() throws IOException {
Expand All @@ -453,15 +426,6 @@ List<String> parseDependencyStrings() throws IOException {
return deps;
}

private void checkIgnored(List<String> rawLines, String searchToken, String depIdentifier) {
for (String line : rawLines) {
if (line.contains(searchToken) && containsIgnorePattern(line)) {
collectedIgnoredDeps.add(depIdentifier);
break;
}
}
}

static final class PipPackage {
final String name;
final String version;
Expand Down
Loading
Loading