|
30 | 30 | import io.github.guacsec.trustifyda.image.ImageUtils; |
31 | 31 | import io.github.guacsec.trustifyda.license.LicenseCheck; |
32 | 32 | import io.github.guacsec.trustifyda.logging.LoggersFactory; |
| 33 | +import io.github.guacsec.trustifyda.providers.JavaMavenProvider; |
33 | 34 | import io.github.guacsec.trustifyda.providers.javascript.workspace.JsWorkspaceDiscovery; |
34 | 35 | import io.github.guacsec.trustifyda.providers.rust.model.CargoMetadata; |
35 | 36 | import io.github.guacsec.trustifyda.tools.Ecosystem; |
@@ -842,7 +843,7 @@ int resolveBatchConcurrency() { |
842 | 843 | } |
843 | 844 |
|
844 | 845 | private static final Set<String> DEFAULT_WORKSPACE_DISCOVERY_IGNORE = |
845 | | - Set.of("**/node_modules/**", "**/.git/**"); |
| 846 | + Set.of("**/node_modules/**", "**/.git/**", "**/target/**", "**/build/**", "**/.gradle/**"); |
846 | 847 |
|
847 | 848 | /** Merges default ignore patterns, env var overrides, and caller-provided patterns. */ |
848 | 849 | Set<String> resolveIgnorePatterns(Set<String> callerPatterns) { |
@@ -875,6 +876,14 @@ List<Path> discoverWorkspaceManifests(Path workspaceDir, Set<String> ignorePatte |
875 | 876 | return discoverCargoManifests(workspaceDir, ignorePatterns); |
876 | 877 | } |
877 | 878 |
|
| 879 | + // Gradle multi-project: settings.gradle or settings.gradle.kts |
| 880 | + boolean hasGradleSettings = |
| 881 | + Files.isRegularFile(workspaceDir.resolve("settings.gradle")) |
| 882 | + || Files.isRegularFile(workspaceDir.resolve("settings.gradle.kts")); |
| 883 | + if (hasGradleSettings) { |
| 884 | + return discoverGradleSubprojects(workspaceDir, ignorePatterns); |
| 885 | + } |
| 886 | + |
878 | 887 | // JS workspace: require package.json + a lock file |
879 | 888 | Path packageJson = workspaceDir.resolve("package.json"); |
880 | 889 | boolean hasJsLock = |
@@ -930,6 +939,126 @@ private List<Path> discoverCargoManifests(Path workspaceDir, Set<String> ignoreP |
930 | 939 | } |
931 | 940 | } |
932 | 941 |
|
| 942 | + private static final String GRADLE_INIT_SCRIPT = |
| 943 | + "allprojects {\n" |
| 944 | + + " task daListProjects {\n" |
| 945 | + + " doLast {\n" |
| 946 | + + " println \"::DA_PROJECT::${project.path}::${project.projectDir}\"\n" |
| 947 | + + " }\n" |
| 948 | + + " }\n" |
| 949 | + + "}\n"; |
| 950 | + |
| 951 | + /** |
| 952 | + * Resolve the Gradle binary, preferring gradlew wrapper when available and configured. |
| 953 | + * |
| 954 | + * @param startDir directory from which to start the wrapper search |
| 955 | + * @return path to the Gradle binary |
| 956 | + */ |
| 957 | + private static String resolveGradleBinary(Path startDir) { |
| 958 | + if (Operations.getWrapperPreference("gradle")) { |
| 959 | + String wrapperName = Operations.isWindows() ? "gradlew.bat" : "gradlew"; |
| 960 | + String wrapper = |
| 961 | + JavaMavenProvider.traverseForMvnw( |
| 962 | + wrapperName, startDir.resolve("build.gradle").toString()); |
| 963 | + if (wrapper != null) { |
| 964 | + return wrapper; |
| 965 | + } |
| 966 | + } |
| 967 | + return Operations.getCustomPathOrElse("gradle"); |
| 968 | + } |
| 969 | + |
| 970 | + /** |
| 971 | + * Discover all build.gradle[.kts] manifest paths in a Gradle multi-project build. Uses a custom |
| 972 | + * init script to get a structured project listing. |
| 973 | + */ |
| 974 | + private List<Path> discoverGradleSubprojects(Path workspaceDir, Set<String> ignorePatterns) { |
| 975 | + Path rootBuildKts = workspaceDir.resolve("build.gradle.kts"); |
| 976 | + Path rootBuild = workspaceDir.resolve("build.gradle"); |
| 977 | + |
| 978 | + List<Path> manifestPaths = new ArrayList<>(); |
| 979 | + if (Files.isRegularFile(rootBuildKts)) { |
| 980 | + manifestPaths.add(rootBuildKts); |
| 981 | + } else if (Files.isRegularFile(rootBuild)) { |
| 982 | + manifestPaths.add(rootBuild); |
| 983 | + } |
| 984 | + |
| 985 | + String gradleBin = resolveGradleBinary(workspaceDir); |
| 986 | + Path initScriptPath = null; |
| 987 | + try { |
| 988 | + initScriptPath = Files.createTempFile("da-list-projects-", ".gradle"); |
| 989 | + Files.writeString(initScriptPath, GRADLE_INIT_SCRIPT); |
| 990 | + |
| 991 | + Operations.ProcessExecOutput output = |
| 992 | + Operations.runProcessGetFullOutput( |
| 993 | + workspaceDir, |
| 994 | + new String[] { |
| 995 | + gradleBin, |
| 996 | + "-q", |
| 997 | + "--no-daemon", |
| 998 | + "--init-script", |
| 999 | + initScriptPath.toString(), |
| 1000 | + "daListProjects" |
| 1001 | + }, |
| 1002 | + null); |
| 1003 | + |
| 1004 | + if (output.getExitCode() != 0) { |
| 1005 | + LOG.warning("gradle daListProjects failed with exit code " + output.getExitCode()); |
| 1006 | + return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns); |
| 1007 | + } |
| 1008 | + |
| 1009 | + for (var proj : parseGradleInitScriptOutput(output.getOutput())) { |
| 1010 | + if (":".equals(proj.path())) { |
| 1011 | + continue; |
| 1012 | + } |
| 1013 | + Path projDir = Path.of(proj.dir()).toAbsolutePath().normalize(); |
| 1014 | + Path buildKts = projDir.resolve("build.gradle.kts"); |
| 1015 | + Path buildGroovy = projDir.resolve("build.gradle"); |
| 1016 | + if (Files.isRegularFile(buildKts)) { |
| 1017 | + manifestPaths.add(buildKts); |
| 1018 | + } else if (Files.isRegularFile(buildGroovy)) { |
| 1019 | + manifestPaths.add(buildGroovy); |
| 1020 | + } |
| 1021 | + } |
| 1022 | + } catch (Exception e) { |
| 1023 | + LOG.warning("Failed to discover Gradle subprojects: " + e.getMessage()); |
| 1024 | + return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns); |
| 1025 | + } finally { |
| 1026 | + if (initScriptPath != null) { |
| 1027 | + try { |
| 1028 | + Files.deleteIfExists(initScriptPath); |
| 1029 | + } catch (IOException ignored) { |
| 1030 | + } |
| 1031 | + } |
| 1032 | + } |
| 1033 | + |
| 1034 | + return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns); |
| 1035 | + } |
| 1036 | + |
| 1037 | + record GradleProject(String path, String dir) {} |
| 1038 | + |
| 1039 | + static List<GradleProject> parseGradleInitScriptOutput(String raw) { |
| 1040 | + if (raw == null || raw.isBlank()) { |
| 1041 | + return List.of(); |
| 1042 | + } |
| 1043 | + List<GradleProject> projects = new ArrayList<>(); |
| 1044 | + for (String line : raw.split("\n")) { |
| 1045 | + if (!line.startsWith("::DA_PROJECT::")) { |
| 1046 | + continue; |
| 1047 | + } |
| 1048 | + String[] parts = line.split("::"); |
| 1049 | + List<String> nonEmpty = new ArrayList<>(); |
| 1050 | + for (String part : parts) { |
| 1051 | + if (!part.isEmpty()) { |
| 1052 | + nonEmpty.add(part); |
| 1053 | + } |
| 1054 | + } |
| 1055 | + if (nonEmpty.size() >= 3) { |
| 1056 | + projects.add(new GradleProject(nonEmpty.get(1), nonEmpty.get(2))); |
| 1057 | + } |
| 1058 | + } |
| 1059 | + return projects; |
| 1060 | + } |
| 1061 | + |
933 | 1062 | /** |
934 | 1063 | * Checks whether a package.json has "private": true, meaning it should not be analyzed as a |
935 | 1064 | * publishable package. |
|
0 commit comments