diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6eeeb91..894b91c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,8 @@ name: CI +permissions: + contents: read + on: push: branches: [main, release] diff --git a/core/analysis/src/main/java/com/github/serj/jcapslock/analyzer/CallGraphBuilder.java b/core/analysis/src/main/java/com/github/serj/jcapslock/analyzer/CallGraphBuilder.java index 8f78c6c..54b9323 100644 --- a/core/analysis/src/main/java/com/github/serj/jcapslock/analyzer/CallGraphBuilder.java +++ b/core/analysis/src/main/java/com/github/serj/jcapslock/analyzer/CallGraphBuilder.java @@ -188,7 +188,7 @@ public static Graph buildUnifiedGraphProto( // Create SootGraphBuilder for SootUp-aware graph construction SootGraphBuilder sootBuilder = new SootGraphBuilder(); - GraphProtoBuilder graphBuilder = sootBuilder.getBuilder(); + GraphProtoBuilder graphBuilder = sootBuilder.builder(); Map methodToFunctionId = new HashMap<>(); // Create implicit capability detector diff --git a/core/analysis/src/main/java/com/github/serj/jcapslock/analyzer/SootGraphBuilder.java b/core/analysis/src/main/java/com/github/serj/jcapslock/analyzer/SootGraphBuilder.java index fc0c95c..6f159a1 100644 --- a/core/analysis/src/main/java/com/github/serj/jcapslock/analyzer/SootGraphBuilder.java +++ b/core/analysis/src/main/java/com/github/serj/jcapslock/analyzer/SootGraphBuilder.java @@ -3,33 +3,30 @@ import capslock.proto.CapabilityOuterClass.Capability; import capslock.proto.GraphOuterClass.Graph; import sootup.core.model.SootMethod; +import sootup.core.signatures.PackageName; import sootup.core.types.ClassType; /** * Extension of GraphProtoBuilder that adds SootUp-specific methods. * Wraps GraphProtoBuilder for use in static analysis with SootUp. */ -public class SootGraphBuilder { - - private final GraphProtoBuilder builder; +public record SootGraphBuilder(GraphProtoBuilder builder) { public SootGraphBuilder() { - this.builder = new GraphProtoBuilder(); - } - - public SootGraphBuilder(GraphProtoBuilder builder) { - this.builder = builder; + this(new GraphProtoBuilder()); } /** * Get the underlying GraphProtoBuilder. */ - public GraphProtoBuilder getBuilder() { + @Override + public GraphProtoBuilder builder() { return builder; } /** * Add a function from a SootUp SootMethod. + * * @param method The SootMethod to add * @return The index of the added function */ @@ -49,7 +46,7 @@ public int addFunction(SootMethod method) { */ public int addRootFunction(SootMethod method) { ClassType classType = method.getDeclaringClassType(); - sootup.core.signatures.PackageName pkg = classType.getPackageName(); + PackageName pkg = classType.getPackageName(); String packageName = pkg != null ? pkg.getName() : ""; builder.markPackageAsRoot(packageName); return addFunction(method); @@ -88,7 +85,7 @@ public int getFunctionIndex(String fullyQualifiedName) { return builder.getFunctionIndex(fullyQualifiedName); } - public capslock.proto.GraphOuterClass.Graph build() { + public Graph build() { return builder.build(); } diff --git a/core/src/main/java/com/github/serj/jcapslock/capability/CapabilityMapper.java b/core/src/main/java/com/github/serj/jcapslock/capability/CapabilityMapper.java index 936a9d6..1809e1b 100644 --- a/core/src/main/java/com/github/serj/jcapslock/capability/CapabilityMapper.java +++ b/core/src/main/java/com/github/serj/jcapslock/capability/CapabilityMapper.java @@ -9,7 +9,6 @@ import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -242,20 +241,4 @@ public static void reset() { SAFE_CLASSES.clear(); loadDefaultMappings(); } - - /** - * Get all safe methods. - * Returns an immutable view. - */ - public static Set getSafeMethods() { - return Collections.unmodifiableSet(new HashSet<>(SAFE_METHODS)); - } - - /** - * Get all safe classes (classes where all methods are safe). - * Returns an immutable view. - */ - public static Set getSafeClasses() { - return Collections.unmodifiableSet(new HashSet<>(SAFE_CLASSES)); - } } \ No newline at end of file diff --git a/core/src/main/java/com/github/serj/jcapslock/model/DependencyInfo.java b/core/src/main/java/com/github/serj/jcapslock/model/DependencyInfo.java deleted file mode 100644 index cb0a7e3..0000000 --- a/core/src/main/java/com/github/serj/jcapslock/model/DependencyInfo.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.github.serj.jcapslock.model; - -import java.io.File; -import java.util.Objects; - -/** - * Build-system agnostic representation of a dependency. - * This class provides a common interface for dependencies regardless of - * whether they come from Maven, Gradle, Bazel, or any other build system. - */ -public record DependencyInfo(String groupId, String artifactId, String version, File jarFile) { - public DependencyInfo { - Objects.requireNonNull(groupId, "groupId cannot be null"); - Objects.requireNonNull(artifactId, "artifactId cannot be null"); - Objects.requireNonNull(version, "version cannot be null"); - // jarFile can be null if JAR is not available - } - - /** - * Returns the coordinate string in the format groupId:artifactId:version - */ - public String getCoordinate() { - return groupId + ":" + artifactId + ":" + version; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DependencyInfo that = (DependencyInfo) o; - return groupId.equals(that.groupId) && - artifactId.equals(that.artifactId) && - version.equals(that.version); - } - - @Override - public int hashCode() { - return Objects.hash(groupId, artifactId, version); - } - - @Override - public String toString() { - return getCoordinate(); - } -} \ No newline at end of file diff --git a/examples/compress-test/src/main/java/com/example/CompressExample.java b/examples/compress-test/src/main/java/com/example/CompressExample.java index d0f413e..fe7526c 100644 --- a/examples/compress-test/src/main/java/com/example/CompressExample.java +++ b/examples/compress-test/src/main/java/com/example/CompressExample.java @@ -7,13 +7,10 @@ import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; -import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; -import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.compress.compressors.zstandard.ZstdUtils; import org.apache.commons.compress.compressors.zstandard.ZstdCompressorOutputStream; import org.apache.commons.compress.compressors.brotli.BrotliUtils; -import org.apache.commons.compress.archivers.zip.ExtraFieldUtils; import java.io.*; import java.nio.file.Files; @@ -32,22 +29,16 @@ public class CompressExample { */ public void createZipArchive(Path sourceDir, Path outputFile) throws IOException { try (ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream( - Files.newOutputStream(outputFile))) { - - Files.walk(sourceDir) - .filter(Files::isRegularFile) - .forEach(path -> { - try { - ZipArchiveEntry entry = new ZipArchiveEntry( - sourceDir.relativize(path).toString()); - entry.setSize(Files.size(path)); - zipOut.putArchiveEntry(entry); - Files.copy(path, zipOut); - zipOut.closeArchiveEntry(); - } catch (IOException e) { - System.err.println("Error adding file to zip: " + e.getMessage()); - } - }); + Files.newOutputStream(outputFile)); + var paths = Files.walk(sourceDir)) { + + for (Path path : paths.filter(Files::isRegularFile).toList()) { + ZipArchiveEntry entry = new ZipArchiveEntry(sourceDir.relativize(path).toString()); + entry.setSize(Files.size(path)); + zipOut.putArchiveEntry(entry); + Files.copy(path, zipOut); + zipOut.closeArchiveEntry(); + } } } @@ -68,13 +59,17 @@ public void extractArchive(Path archiveFile, Path destDir) throws Exception { continue; } - Path outputPath = destDir.resolve(entry.getName()); + // in case of refactoring we want to avid path traversals + Path outputPath = destDir.resolve(entry.getName()).normalize(); + if (!outputPath.startsWith(destDir)) { + throw new IOException("Archive entry is outside of the target directory: " + entry.getName()); + } if (entry.isDirectory()) { Files.createDirectories(outputPath); } else { Files.createDirectories(outputPath.getParent()); try (OutputStream out = Files.newOutputStream(outputPath)) { - IOUtils.copy(archiveIn, out); + archiveIn.transferTo(out); } } } @@ -90,7 +85,7 @@ public void compressFileGzip(Path inputFile, Path outputFile) throws IOException OutputStream out = Files.newOutputStream(outputFile); GzipCompressorOutputStream gzipOut = new GzipCompressorOutputStream(out)) { - IOUtils.copy(in, gzipOut); + in.transferTo(gzipOut); } } @@ -100,23 +95,18 @@ public void compressFileGzip(Path inputFile, Path outputFile) throws IOException */ public void createTarArchive(Path sourceDir, Path outputFile) throws IOException { try (TarArchiveOutputStream tarOut = new TarArchiveOutputStream( - Files.newOutputStream(outputFile))) { + Files.newOutputStream(outputFile)); + var paths = Files.walk(sourceDir)) { tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); - Files.walk(sourceDir) - .filter(Files::isRegularFile) - .forEach(path -> { - try { - TarArchiveEntry entry = new TarArchiveEntry( - path.toFile(), - sourceDir.relativize(path).toString()); - tarOut.putArchiveEntry(entry); - Files.copy(path, tarOut); - tarOut.closeArchiveEntry(); - } catch (IOException e) { - System.err.println("Error adding file to tar: " + e.getMessage()); - } - }); + for (Path path : paths.filter(Files::isRegularFile).toList()) { + TarArchiveEntry entry = new TarArchiveEntry( + path.toFile(), + sourceDir.relativize(path).toString()); + tarOut.putArchiveEntry(entry); + Files.copy(path, tarOut); + tarOut.closeArchiveEntry(); + } } } @@ -134,15 +124,6 @@ public void checkAvailableFormats() { System.out.println(" Zstandard (zstd) support: " + zstdAvailable); System.out.println(" Brotli support: " + brotliAvailable); - - // ExtraFieldUtils also uses reflection for registering handlers - // This also triggers CAPABILITY_REFLECT - try { - // Register a custom extra field (uses reflection internally) - ExtraFieldUtils.register(ZipArchiveEntry.class); - } catch (Exception e) { - // Expected - just demonstrating the reflection usage - } } /** diff --git a/maven-plugin/src/main/java/com/github/serj/jcapslock/maven/OptionalDependencyResolver.java b/maven-plugin/src/main/java/com/github/serj/jcapslock/maven/OptionalDependencyResolver.java index b43789c..8baef23 100644 --- a/maven-plugin/src/main/java/com/github/serj/jcapslock/maven/OptionalDependencyResolver.java +++ b/maven-plugin/src/main/java/com/github/serj/jcapslock/maven/OptionalDependencyResolver.java @@ -18,10 +18,13 @@ import org.w3c.dom.NodeList; import com.github.serj.jcapslock.util.JavaUtils; +import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import java.io.File; +import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.http.HttpClient; @@ -305,21 +308,30 @@ private List fetchFromRemotePom(Artifact artifact) { return Collections.emptyList(); } + /** + * Creates a secure DocumentBuilder with XXE protections enabled. + * Screw XML and all those design patterns below + */ + @SuppressWarnings("HttpUrlsUsage") // These are XML feature URIs, not network URLs + private static Document createSecureDocument(InputStream is) throws ParserConfigurationException, IOException, SAXException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(is); + } + /** * Parse POM to extract optional dependencies. */ private List parsePom(InputStream is) { try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - factory.setFeature("http://xml.org/sax/features/external-general-entities", false); - factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); - factory.setXIncludeAware(false); - factory.setExpandEntityReferences(false); - factory.setNamespaceAware(true); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document doc = builder.parse(is); + Document doc = createSecureDocument(is); List result = new ArrayList<>(); NodeList dependencies = doc.getElementsByTagName("dependency");