33import ch .qos .logback .core .spi .ScanException ;
44import io .mixeway .mixewayflowapi .db .entity .CodeRepo ;
55import io .mixeway .mixewayflowapi .db .entity .CodeRepoBranch ;
6- import lombok .RequiredArgsConstructor ;
76import lombok .extern .log4j .Log4j2 ;
7+ import org .springframework .beans .factory .annotation .Value ;
88import org .springframework .stereotype .Service ;
9+ import org .springframework .util .StringUtils ;
910
1011import java .io .BufferedReader ;
1112import java .io .File ;
1213import java .io .IOException ;
1314import java .io .InputStreamReader ;
15+ import java .util .Map ;
1416import java .util .concurrent .ExecutorService ;
1517import java .util .concurrent .Executors ;
1618import java .util .concurrent .TimeUnit ;
2527 */
2628@ Service
2729@ Log4j2
28- @ RequiredArgsConstructor
2930public class CdxGenService {
3031
32+ /**
33+ * Prepended to {@code PATH} for cdxgen and related subprocesses so the same JVM
34+ * can resolve {@code mvn}, {@code npm}, {@code gradle}, etc. as in an interactive shell
35+ * (e.g. {@code /opt/tools/bin:/usr/local/bin} in Docker).
36+ */
37+ @ Value ("${scan.subprocess.path-extra:}" )
38+ private String pathExtra ;
39+
40+ /**
41+ * If set, exported as {@code JAVA_HOME} for subprocesses when the environment
42+ * does not already define it. Otherwise {@code java.home} of the running JVM is used
43+ * when {@code JAVA_HOME} is missing (helps Maven/Gradle invoked by cdxgen).
44+ */
45+ @ Value ("${scan.subprocess.java-home:}" )
46+ private String javaHomeOverride ;
47+
48+ /**
49+ * When true (default), runs {@code pipreqs .} before cdxgen if {@code pipreqs} is on {@code PATH}.
50+ * Set to false to match a manual {@code cdxgen} run and avoid overwriting {@code requirements.txt}.
51+ */
52+ @ Value ("${scan.cdxgen.run-pipreqs:true}" )
53+ private boolean runPipreqs ;
54+
55+ @ Value ("${proxy.host:#{null}}" )
56+ private String proxyHost ;
57+
58+ @ Value ("${proxy.port:#{null}}" )
59+ private Integer proxyPort ;
60+
3161 /**
3262 * Generates the SBOM (Software Bill of Materials) file using the cdxgen tool.
3363 *
34- * <p>This method executes the cdxgen command in the specified repository directory,
35- * redirecting both standard output and error streams to prevent blocking.
36- * It conditionally sets environment variables for proxy configuration if the
37- * system properties <code>proxy.host</code> and <code>proxy.port</code> are provided.
38- * The method waits for the process to complete, with a timeout of 2 minutes.
39- * If the process exceeds the timeout, it is forcibly terminated.
40- * After the process completes, the method validates the generated <code>bom.json</code>
41- * file by checking for its existence and ensuring it has content.</p>
64+ * <p>This method executes cdxgen in the specified repository directory with
65+ * {@link ProcessBuilder#environment()} configured for CDXGEN/CDX_* variables and optional proxy.
66+ * Optional {@code scan.subprocess.path-extra} widens {@code PATH} so language toolchains
67+ * match non-interactive vs login-shell setups.</p>
4268 *
4369 * @param repoDir the directory of the repository where cdxgen will run
4470 * @param codeRepo the code repository entity
@@ -51,70 +77,35 @@ public void generateBom(String repoDir, CodeRepo codeRepo, CodeRepoBranch codeRe
5177 throws IOException , InterruptedException {
5278 log .info ("[CdxGen] Starting SBOM generation for: {} branch: {}" , codeRepo .getName (), codeRepoBranch .getName ());
5379
54- // Step 1: Verify if 'pipreqs' command is available
55- boolean isPipreqsAvailable = false ;
56- try {
57- ProcessBuilder pbCheckPipreqs = new ProcessBuilder ("sh" , "-c" , "command -v pipreqs" );
58- pbCheckPipreqs .redirectErrorStream (true );
59- Process pCheckPipreqs = pbCheckPipreqs .start ();
60- int exitCode = pCheckPipreqs .waitFor ();
61- if (exitCode == 0 ) {
62- isPipreqsAvailable = true ;
63- log .debug ("[CdxGen] 'pipreqs' is available." );
64- } else {
65- log .debug ("[CdxGen] 'pipreqs' is not available." );
66- }
67- } catch (IOException e ) {
68- // Command not found
69- log .debug ("[CdxGen] Exception while checking for 'pipreqs': {}" , e .getMessage ());
70- }
71-
72- // Step 2: If available, execute 'pipreqs .' in repoDir
73- if (isPipreqsAvailable ) {
74- log .debug ("[CdxGen] Executing 'pipreqs .' in {}" , repoDir );
75- ProcessBuilder pbPipreqs = new ProcessBuilder ("pipreqs" , "." );
76- pbPipreqs .directory (new File (repoDir ));
77- pbPipreqs .redirectOutput (ProcessBuilder .Redirect .INHERIT );
78- pbPipreqs .redirectError (ProcessBuilder .Redirect .INHERIT );
79- Process pPipreqs = pbPipreqs .start ();
80-
81- // Wait for 'pipreqs' to finish
82- boolean finished = pPipreqs .waitFor (10 , TimeUnit .MINUTES );
83- if (!finished ) {
84- log .debug ("[CdxGen] 'pipreqs' did not finish within 10 minutes. Terminating process." );
85- pPipreqs .destroyForcibly ();
86- } else {
87- int exitCode = pPipreqs .exitValue ();
88- if (exitCode != 0 ) {
89- log .debug ("[CdxGen] 'pipreqs' exited with non-zero exit code: {}" , exitCode );
90- } else {
91- log .debug ("[CdxGen] 'pipreqs' executed successfully." );
92- }
93- }
80+ if (runPipreqs ) {
81+ runPipreqsIfAvailable (repoDir );
9482 }
9583
96- // Step 3: Proceed with executing 'cdxgen'
97- String proxyHost = System .getProperty ("proxy.host" );
98- String proxyPort = System .getProperty ("proxy.port" );
99- String command ;
100-
101- if (proxyHost != null && proxyPort != null ) {
102- command = "CDXGEN_DEBUG_MODE=debug "
103- + "CDX_MAVEN_INCLUDE_TEST_SCOPE=false "
104- + "HTTP_PROXY=http://" + proxyHost + ":" + proxyPort + " "
105- + "HTTPS_PROXY=http://" + proxyHost + ":" + proxyPort + " "
106- + "cdxgen --recurse --required-only --output sbom.json ." ;
107- log .info ("[CdxGen] Proxy settings applied: {}:{}" , proxyHost , proxyPort );
108- } else {
109- command = "CDXGEN_DEBUG_MODE=debug CDX_MAVEN_INCLUDE_TEST_SCOPE=false cdxgen --recurse --required-only --output sbom.json ." ;
110- }
111-
112- // Use 'sh -c' to execute the command in a shell
113- ProcessBuilder pb = new ProcessBuilder ("sh" , "-c" , command );
84+ ProcessBuilder pb = new ProcessBuilder (
85+ "cdxgen" ,
86+ "--recurse" ,
87+ "--required-only" ,
88+ "--output" ,
89+ "sbom.json" ,
90+ "."
91+ );
11492 pb .directory (new File (repoDir ));
11593 pb .redirectOutput (ProcessBuilder .Redirect .PIPE );
11694 pb .redirectError (ProcessBuilder .Redirect .PIPE );
11795
96+ applyScannerEnvironment (pb );
97+
98+ Map <String , String > env = pb .environment ();
99+ env .put ("CDXGEN_DEBUG_MODE" , "debug" );
100+ env .put ("CDX_MAVEN_INCLUDE_TEST_SCOPE" , "false" );
101+
102+ if (StringUtils .hasText (proxyHost ) && proxyPort != null ) {
103+ String proxyUrl = "http://" + proxyHost + ":" + proxyPort ;
104+ env .put ("HTTP_PROXY" , proxyUrl );
105+ env .put ("HTTPS_PROXY" , proxyUrl );
106+ log .info ("[CdxGen] Proxy settings applied: {}:{}" , proxyHost , proxyPort );
107+ }
108+
118109 Process process = pb .start ();
119110
120111 ExecutorService executorService = Executors .newFixedThreadPool (2 );
@@ -142,7 +133,6 @@ public void generateBom(String repoDir, CodeRepo codeRepo, CodeRepoBranch codeRe
142133 }
143134 });
144135
145- // Wait for 'cdxgen' to finish with a timeout of 30 minutes
146136 boolean finished = process .waitFor (30 , TimeUnit .MINUTES );
147137 if (!finished ) {
148138 log .warn ("[CdxGen] SBOM generation did not finish within 30 minutes. Terminating process." );
@@ -166,7 +156,6 @@ public void generateBom(String repoDir, CodeRepo codeRepo, CodeRepoBranch codeRe
166156 }
167157 }
168158
169- // Validate the 'sbom.json' file
170159 File bomFile = new File (repoDir , "sbom.json" );
171160 if (bomFile .exists ()) {
172161 if (bomFile .length () > 0 ) {
@@ -179,6 +168,79 @@ public void generateBom(String repoDir, CodeRepo codeRepo, CodeRepoBranch codeRe
179168 }
180169 }
181170
171+ private void runPipreqsIfAvailable (String repoDir ) throws IOException , InterruptedException {
172+ boolean isPipreqsAvailable = false ;
173+ try {
174+ ProcessBuilder pbCheckPipreqs = new ProcessBuilder ("sh" , "-c" , "command -v pipreqs" );
175+ applyScannerEnvironment (pbCheckPipreqs );
176+ pbCheckPipreqs .redirectErrorStream (true );
177+ Process pCheckPipreqs = pbCheckPipreqs .start ();
178+ int exitCode = pCheckPipreqs .waitFor ();
179+ if (exitCode == 0 ) {
180+ isPipreqsAvailable = true ;
181+ log .debug ("[CdxGen] 'pipreqs' is available." );
182+ } else {
183+ log .debug ("[CdxGen] 'pipreqs' is not available." );
184+ }
185+ } catch (IOException e ) {
186+ log .debug ("[CdxGen] Exception while checking for 'pipreqs': {}" , e .getMessage ());
187+ }
188+
189+ if (!isPipreqsAvailable ) {
190+ return ;
191+ }
192+
193+ log .debug ("[CdxGen] Executing 'pipreqs .' in {}" , repoDir );
194+ ProcessBuilder pbPipreqs = new ProcessBuilder ("pipreqs" , "." );
195+ pbPipreqs .directory (new File (repoDir ));
196+ applyScannerEnvironment (pbPipreqs );
197+ pbPipreqs .redirectOutput (ProcessBuilder .Redirect .INHERIT );
198+ pbPipreqs .redirectError (ProcessBuilder .Redirect .INHERIT );
199+ Process pPipreqs = pbPipreqs .start ();
200+
201+ boolean finished = pPipreqs .waitFor (10 , TimeUnit .MINUTES );
202+ if (!finished ) {
203+ log .debug ("[CdxGen] 'pipreqs' did not finish within 10 minutes. Terminating process." );
204+ pPipreqs .destroyForcibly ();
205+ } else {
206+ int exitCode = pPipreqs .exitValue ();
207+ if (exitCode != 0 ) {
208+ log .debug ("[CdxGen] 'pipreqs' exited with non-zero exit code: {}" , exitCode );
209+ } else {
210+ log .debug ("[CdxGen] 'pipreqs' executed successfully." );
211+ }
212+ }
213+ }
214+
215+ /**
216+ * Ensures subprocesses see the same toolchains as typical CLI usage: optional {@code PATH}
217+ * prefix, {@code JAVA_HOME}, and a defined {@code HOME} when missing.
218+ */
219+ private void applyScannerEnvironment (ProcessBuilder pb ) {
220+ Map <String , String > env = pb .environment ();
182221
222+ if (StringUtils .hasText (pathExtra )) {
223+ String path = env .getOrDefault ("PATH" , "" );
224+ env .put ("PATH" , pathExtra + File .pathSeparator + path );
225+ log .debug ("[CdxGen] PATH prefix applied (scan.subprocess.path-extra)" );
226+ }
183227
228+ if (!StringUtils .hasText (env .get ("JAVA_HOME" ))) {
229+ if (StringUtils .hasText (javaHomeOverride )) {
230+ env .put ("JAVA_HOME" , javaHomeOverride .trim ());
231+ } else {
232+ String jvmHome = System .getProperty ("java.home" );
233+ if (StringUtils .hasText (jvmHome )) {
234+ env .put ("JAVA_HOME" , jvmHome );
235+ }
236+ }
237+ }
238+
239+ if (!StringUtils .hasText (env .get ("HOME" ))) {
240+ String userHome = System .getProperty ("user.home" );
241+ if (StringUtils .hasText (userHome )) {
242+ env .put ("HOME" , userHome );
243+ }
244+ }
245+ }
184246}
0 commit comments