1616 */
1717package io .github .guacsec .trustifyda .providers ;
1818
19- import static io .github .guacsec .trustifyda .impl .ExhortApi .debugLoggingIsNeeded ;
20-
21- import com .fasterxml .jackson .core .JsonProcessingException ;
22- import com .github .packageurl .MalformedPackageURLException ;
2319import com .github .packageurl .PackageURL ;
24- import io .github .guacsec .trustifyda .Api ;
25- import io .github .guacsec .trustifyda .Provider ;
26- import io .github .guacsec .trustifyda .logging .LoggersFactory ;
27- import io .github .guacsec .trustifyda .sbom .Sbom ;
28- import io .github .guacsec .trustifyda .sbom .SbomFactory ;
29- import io .github .guacsec .trustifyda .tools .Ecosystem ;
30- import io .github .guacsec .trustifyda .tools .Operations ;
31- import io .github .guacsec .trustifyda .utils .Environment ;
32- import io .github .guacsec .trustifyda .utils .IgnorePatternDetector ;
3320import io .github .guacsec .trustifyda .utils .PythonControllerBase ;
34- import io .github .guacsec .trustifyda .utils .PythonControllerRealEnv ;
35- import io .github .guacsec .trustifyda .utils .PythonControllerVirtualEnv ;
36- import java .io .IOException ;
37- import java .nio .charset .StandardCharsets ;
38- import java .nio .file .Files ;
3921import java .nio .file .Path ;
4022import java .util .Arrays ;
41- import java .util .List ;
42- import java .util .Map ;
4323import java .util .Set ;
44- import java .util .logging .Logger ;
4524import java .util .stream .Collectors ;
4625
47- public final class PythonPipProvider extends Provider {
48-
49- private static final Logger log = LoggersFactory .getLogger (PythonPipProvider .class .getName ());
50- private static final String DEFAULT_PIP_ROOT_COMPONENT_NAME = "default-pip-root" ;
51- private static final String DEFAULT_PIP_ROOT_COMPONENT_VERSION = "0.0.0" ;
52-
53- public void setPythonController (PythonControllerBase pythonController ) {
54- this .pythonController = pythonController ;
55- }
56-
57- private PythonControllerBase pythonController ;
26+ public final class PythonPipProvider extends PythonProvider {
5827
5928 public PythonPipProvider (Path manifest ) {
60- super (Ecosystem . Type . PYTHON , manifest );
29+ super (manifest );
6130 }
6231
6332 @ Override
64- public Content provideStack () throws IOException {
65- PythonControllerBase pythonController = getPythonController ();
66- List <Map <String , Object >> dependencies =
67- pythonController .getDependencies (manifest .toString (), true );
68- printDependenciesTree (dependencies );
69- Sbom sbom = SbomFactory .newInstance (Sbom .BelongingCondition .PURL , "sensitive" );
70- sbom .addRoot (toPurl (DEFAULT_PIP_ROOT_COMPONENT_NAME , DEFAULT_PIP_ROOT_COMPONENT_VERSION ));
71- for (Map <String , Object > component : dependencies ) {
72- addAllDependencies (sbom .getRoot (), component , sbom );
73- }
74- byte [] requirementsFile = Files .readAllBytes (manifest );
75- handleIgnoredDependencies (new String (requirementsFile ), sbom );
76- return new Content (
77- sbom .getAsJsonString ().getBytes (StandardCharsets .UTF_8 ), Api .CYCLONEDX_MEDIA_TYPE );
78- }
79-
80- private void addAllDependencies (PackageURL source , Map <String , Object > component , Sbom sbom ) {
81-
82- PackageURL packageURL =
83- toPurl ((String ) component .get ("name" ), (String ) component .get ("version" ));
84- sbom .addDependency (source , packageURL , null );
85-
86- List <Map <String , Object >> directDeps =
87- (List <Map <String , Object >>) component .get ("dependencies" );
88- if (directDeps != null ) {
89- for (Map <String , Object > dep : directDeps ) {
90- addAllDependencies (packageURL , dep , sbom );
91- }
92- }
33+ protected Path getRequirementsPath () {
34+ return manifest ;
9335 }
9436
9537 @ Override
96- public Content provideComponent () throws IOException {
97- PythonControllerBase pythonController = getPythonController ();
98- List <Map <String , Object >> dependencies =
99- pythonController .getDependencies (manifest .toString (), false );
100- printDependenciesTree (dependencies );
101- Sbom sbom = SbomFactory .newInstance ();
102- sbom .addRoot (toPurl (DEFAULT_PIP_ROOT_COMPONENT_NAME , DEFAULT_PIP_ROOT_COMPONENT_VERSION ));
103- dependencies .forEach (
104- (component ) ->
105- sbom .addDependency (
106- sbom .getRoot (),
107- toPurl ((String ) component .get ("name" ), (String ) component .get ("version" )),
108- null ));
109-
110- var manifestContent = Files .readString (manifest );
111- handleIgnoredDependencies (manifestContent , sbom );
112- return new Content (
113- sbom .getAsJsonString ().getBytes (StandardCharsets .UTF_8 ), Api .CYCLONEDX_MEDIA_TYPE );
114- }
115-
116- private void printDependenciesTree (List <Map <String , Object >> dependencies )
117- throws JsonProcessingException {
118- if (debugLoggingIsNeeded ()) {
119- String pythonControllerTree =
120- objectMapper .writerWithDefaultPrettyPrinter ().writeValueAsString (dependencies );
121- log .info (
122- String .format (
123- "Python Generated Dependency Tree in Json Format: %s %s %s" ,
124- System .lineSeparator (), pythonControllerTree , System .lineSeparator ()));
125- }
38+ protected void cleanupRequirementsPath (Path requirementsPath ) {
39+ // No cleanup needed — the manifest is the requirements file itself.
12640 }
12741
128- private void handleIgnoredDependencies (String manifestContent , Sbom sbom ) {
129- Set <PackageURL > ignoredDeps = getIgnoredDependencies (manifestContent );
130- Set <String > ignoredDepsVersions =
131- ignoredDeps .stream ()
132- .filter (dep -> !dep .getVersion ().trim ().equals ("*" ))
133- .map (PackageURL ::getCoordinates )
134- .collect (Collectors .toSet ());
135- Set <String > ignoredDepsNoVersions =
136- ignoredDeps .stream ()
137- .filter (dep -> dep .getVersion ().trim ().equals ("*" ))
138- .map (PackageURL ::getCoordinates )
139- .collect (Collectors .toSet ());
140-
141- // filter out by name only from sbom all exhortignore dependencies that their version will be
142- // resolved by pip.
143- sbom .setBelongingCriteriaBinaryAlgorithm (Sbom .BelongingCondition .NAME );
144- sbom .filterIgnoredDeps (ignoredDepsNoVersions );
145- boolean matchManifestVersions = Environment .getBoolean (PROP_MATCH_MANIFEST_VERSIONS , true );
146- // filter out by purl from sbom all exhortignore dependencies that their version hardcoded in
147- // requirements.txt -
148- // in case all versions in manifest matching installed versions of packages in environment.
149- if (matchManifestVersions ) {
150- sbom .setBelongingCriteriaBinaryAlgorithm (Sbom .BelongingCondition .PURL );
151- sbom .filterIgnoredDeps (ignoredDepsVersions );
152- } else {
153- // in case version mismatch is possible (MATCH_MANIFEST_VERSIONS=false) , need to parse the
154- // name of package
155- // from the purl, and remove the package name from sbom according to name only
156- Set <String > deps =
157- ignoredDepsVersions .stream ()
158- .map (
159- purlString -> {
160- try {
161- return new PackageURL (purlString ).getName ();
162- } catch (MalformedPackageURLException e ) {
163- throw new RuntimeException (e );
164- }
165- })
166- .collect (Collectors .toSet ());
167- sbom .setBelongingCriteriaBinaryAlgorithm (Sbom .BelongingCondition .NAME );
168- sbom .filterIgnoredDeps (deps );
169- }
170- }
171-
172- /**
173- * Checks if a text line contains a Python pip ignore pattern. Handles both '#exhortignore' and
174- * '#trustify-da-ignore' with optional spacing.
175- *
176- * @param line the line to check
177- * @return true if the line contains a Python pip ignore pattern
178- */
179- private boolean containsPythonIgnorePattern (String line ) {
180- return line .contains ("#" + IgnorePatternDetector .IGNORE_PATTERN )
181- || line .contains ("# " + IgnorePatternDetector .IGNORE_PATTERN )
182- || line .contains ("#" + IgnorePatternDetector .LEGACY_IGNORE_PATTERN )
183- || line .contains ("# " + IgnorePatternDetector .LEGACY_IGNORE_PATTERN );
42+ @ Override
43+ protected Set <PackageURL > getIgnoredDependencies (String manifestContent ) {
44+ String [] lines = manifestContent .split (System .lineSeparator ());
45+ return Arrays .stream (lines )
46+ .filter (this ::containsIgnorePattern )
47+ .map (PythonPipProvider ::extractDepFull )
48+ .map (this ::splitToNameVersion )
49+ .map (dep -> toPurl (dep [0 ], dep [1 ]))
50+ .collect (Collectors .toSet ());
18451 }
18552
186- private Set <PackageURL > getIgnoredDependencies (String requirementsDeps ) {
187-
188- String [] requirementsLines = requirementsDeps .split (System .lineSeparator ());
189- Set <PackageURL > collected =
190- Arrays .stream (requirementsLines )
191- .filter (this ::containsPythonIgnorePattern )
192- .map (PythonPipProvider ::extractDepFull )
193- .map (this ::splitToNameVersion )
194- .map (dep -> toPurl (dep [0 ], dep [1 ]))
195- // .map(packageURL -> packageURL.getCoordinates())
196- .collect (Collectors .toSet ());
197-
198- return collected ;
53+ private static String extractDepFull (String requirementLine ) {
54+ return requirementLine .substring (0 , requirementLine .indexOf ("#" )).trim ();
19955 }
20056
20157 private String [] splitToNameVersion (String nameVersion ) {
@@ -209,91 +65,4 @@ private String[] splitToNameVersion(String nameVersion) {
20965 }
21066 return result ;
21167 }
212-
213- private static String extractDepFull (String requirementLine ) {
214- return requirementLine .substring (0 , requirementLine .indexOf ("#" )).trim ();
215- }
216-
217- private PackageURL toPurl (String name , String version ) {
218-
219- try {
220- return new PackageURL (Ecosystem .Type .PYTHON .getType (), null , name , version , null , null );
221- } catch (MalformedPackageURLException e ) {
222- throw new RuntimeException (e );
223- }
224- }
225-
226- private PythonControllerBase getPythonController () {
227- String pythonPipBinaries ;
228- boolean useVirtualPythonEnv ;
229- if (!Environment .get (PythonControllerBase .PROP_TRUSTIFY_DA_PIP_SHOW , "" ).trim ().isEmpty ()
230- && !Environment .get (PythonControllerBase .PROP_TRUSTIFY_DA_PIP_FREEZE , "" )
231- .trim ()
232- .isEmpty ()) {
233- pythonPipBinaries = "python;;pip" ;
234- useVirtualPythonEnv = false ;
235- } else {
236- pythonPipBinaries = getExecutable ("python" , "--version" );
237- useVirtualPythonEnv =
238- Environment .getBoolean (PythonControllerBase .PROP_TRUSTIFY_DA_PYTHON_VIRTUAL_ENV , false );
239- }
240-
241- String [] parts = pythonPipBinaries .split (";;" );
242- var python = parts [0 ];
243- var pip = parts [1 ];
244- PythonControllerBase pythonController ;
245- if (this .pythonController == null ) {
246- if (useVirtualPythonEnv ) {
247- pythonController = new PythonControllerVirtualEnv (python );
248- } else {
249- pythonController = new PythonControllerRealEnv (python , pip );
250- }
251- } else {
252- pythonController = this .pythonController ;
253- }
254- return pythonController ;
255- }
256-
257- private String getExecutable (String command , String args ) {
258- String python = Operations .getCustomPathOrElse ("python3" );
259- String pip = Operations .getCustomPathOrElse ("pip3" );
260- try {
261- Operations .runProcess (python , args );
262- Operations .runProcess (pip , args );
263- } catch (Exception e ) {
264- python = Operations .getCustomPathOrElse ("python" );
265- pip = Operations .getCustomPathOrElse ("pip" );
266- try {
267- Process process = new ProcessBuilder (command , args ).redirectErrorStream (true ).start ();
268- int exitCode = process .waitFor ();
269- if (exitCode != 0 ) {
270- throw new IOException (
271- "Python executable found, but it exited with error code " + exitCode );
272- }
273- } catch (IOException | InterruptedException ex ) {
274- throw new RuntimeException (
275- String .format (
276- "Unable to find or run Python executable '%s'. Please ensure Python is installed"
277- + " and available in your PATH." ,
278- command ),
279- ex );
280- }
281-
282- try {
283- Process process = new ProcessBuilder ("pip" , args ).redirectErrorStream (true ).start ();
284- int exitCode = process .waitFor ();
285- if (exitCode != 0 ) {
286- throw new IOException ("Pip executable found, but it exited with error code " + exitCode );
287- }
288- } catch (IOException | InterruptedException ex ) {
289- throw new RuntimeException (
290- String .format (
291- "Unable to find or run Pip executable '%s'. Please ensure Pip is installed and"
292- + " available in your PATH." ,
293- command ),
294- ex );
295- }
296- }
297- return String .format ("%s;;%s" , python , pip );
298- }
29968}
0 commit comments