Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
80 changes: 65 additions & 15 deletions cli/cmd/command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,26 @@ func NewAutobuilderBuilder() *AutobuilderBuilder {

type AnalyzerBuilder struct {
*BaseCommandBuilder
projectPath string
outputDir string
sarifFileName string
sarifCodeFlowLimit int64
sarifToolVersion string
sarifToolSemanticVersion string
sarifUriBase string
semgrepCompatibility bool
partialFingerprints bool
ifdsAnalysisTimeout int64
severities []string
ruleSetPaths []string
ruleLoadTracePath string
jarPath string
maxMemory string
projectPath string
outputDir string
sarifFileName string
sarifCodeFlowLimit int64
sarifToolVersion string
sarifToolSemanticVersion string
sarifUriBase string
semgrepCompatibility bool
partialFingerprints bool
ifdsAnalysisTimeout int64
severities []string
ruleSetPaths []string
ruleLoadTracePath string
jarPath string
maxMemory string
ruleIDs []string
approximationsConfig []string
dataflowApproximations []string
trackExternalMethods bool
debugFactReachabilitySarif bool
}

func (a *AnalyzerBuilder) SetProject(projectPath string) *AnalyzerBuilder {
Expand Down Expand Up @@ -134,6 +139,31 @@ func (a *AnalyzerBuilder) SetMaxMemory(maxMemory string) *AnalyzerBuilder {
return a
}

func (a *AnalyzerBuilder) AddRuleID(ruleID string) *AnalyzerBuilder {
a.ruleIDs = append(a.ruleIDs, ruleID)
return a
}

func (a *AnalyzerBuilder) AddApproximationsConfig(configPath string) *AnalyzerBuilder {
a.approximationsConfig = append(a.approximationsConfig, configPath)
return a
}

func (a *AnalyzerBuilder) AddDataflowApproximations(approxPath string) *AnalyzerBuilder {
a.dataflowApproximations = append(a.dataflowApproximations, approxPath)
return a
}

func (a *AnalyzerBuilder) SetTrackExternalMethods(track bool) *AnalyzerBuilder {
a.trackExternalMethods = track
return a
}

func (a *AnalyzerBuilder) EnableDebugFactReachabilitySarif() *AnalyzerBuilder {
a.debugFactReachabilitySarif = true
return a
}

func (a *AnalyzerBuilder) BuildNativeCommand() []string {
// For native execution, create a temporary logs directory
tempLogsDir, err := os.MkdirTemp("", "opentaint-*")
Expand Down Expand Up @@ -203,6 +233,26 @@ func (a *AnalyzerBuilder) BuildNativeCommand() []string {
flags = append(flags, "--semgrep-rule-load-trace", a.ruleLoadTracePath)
}

for _, ruleID := range a.ruleIDs {
flags = append(flags, "--semgrep-rule-id", ruleID)
}

for _, configPath := range a.approximationsConfig {
flags = append(flags, "--approximations-config", configPath)
}

for _, approxPath := range a.dataflowApproximations {
flags = append(flags, "--dataflow-approximations", approxPath)
}

if a.trackExternalMethods {
flags = append(flags, "--track-external-methods")
}

if a.debugFactReachabilitySarif {
flags = append(flags, "--debug-fact-reachability-sarif")
}

return append(command, flags...)
}

Expand Down
10 changes: 9 additions & 1 deletion cli/cmd/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ func init() {
}

func ensureAutobuilderAvailable() (string, error) {
if globals.Config.Autobuilder.JarPath != "" {
return globals.Config.Autobuilder.JarPath, nil
}

autobuilderJarPath, err := utils.GetAutobuilderJarPath(globals.Config.Autobuilder.Version)
if err != nil {
return "", fmt.Errorf("failed to construct path to the autobuilder: %w", err)
Expand Down Expand Up @@ -189,11 +193,15 @@ func compileProject(absOutputProjectModelPath, absProjectRoot, autobuilderJarPat
return true
}
// Execute the command using JavaRunner
err = javaRunner.ExecuteJavaCommand(autobuilderCommand, commandSucceeded)
cmdErr, err := javaRunner.ExecuteJavaCommand(autobuilderCommand, commandSucceeded)
if err != nil {
output.LogInfof("Native compilation has failed: %s", err)
return fmt.Errorf("native compilation has failed: %w", err)
}
if cmdErr != nil {
output.LogInfof("Native compilation has failed: %s", cmdErr)
return fmt.Errorf("native compilation has failed: %w", cmdErr)
}

return nil
}
170 changes: 170 additions & 0 deletions cli/cmd/compile_approximations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package cmd

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/seqra/opentaint/internal/globals"
"github.com/seqra/opentaint/internal/output"
"github.com/seqra/opentaint/internal/utils"
"github.com/seqra/opentaint/internal/utils/java"
"github.com/seqra/opentaint/internal/utils/project"
)

// approxClassesJarPrefix is the path prefix under which the analyzer fat JAR
// bundles approximation support sources (OpentaintNdUtil, ArgumentTypeContext).
const approxClassesJarPrefix = "opentaint-dataflow-approximations/"

// compileApproximationsIfNeeded checks whether a --dataflow-approximations directory
// contains .java source files. If so, it compiles them using javac (with the
// analyzer JAR + project dependencies on the classpath) and returns the path to
// the compiled .class output directory. If the directory already contains only
// .class files (or no .java files at all), it is returned as-is.
//
// projectModelDir is the directory containing project.yaml — used to resolve
// project dependencies for the javac classpath (approximation code may reference
// library types like org.apache.pdfbox.pdmodel.PDDocument).
func compileApproximationsIfNeeded(approxPath string, analyzerJarPath string, projectModelDir string) (string, error) {
info, err := os.Stat(approxPath)
if err != nil {
return "", fmt.Errorf("approximation path does not exist: %w", err)
}
if !info.IsDir() {
return approxPath, nil
}

javaFiles, err := collectJavaSources(approxPath)
if err != nil {
return "", err
}
if len(javaFiles) == 0 {
return approxPath, nil
}

output.LogInfof("Found %d .java file(s) in approximations directory, compiling...", len(javaFiles))

javacPath, err := resolveJavacPath()
if err != nil {
return "", err
}

extractedDir, err := os.MkdirTemp("", "opentaint-approx-deps-*")
if err != nil {
return "", fmt.Errorf("failed to create temp directory for approximation deps: %w", err)
}
defer func() { _ = os.RemoveAll(extractedDir) }()

if err := utils.ExtractZipPrefix(analyzerJarPath, approxClassesJarPrefix, extractedDir); err != nil {
return "", fmt.Errorf("failed to extract approximation classes from analyzer JAR: %w", err)
}

outputDir, err := os.MkdirTemp("", "opentaint-approx-compiled-*")
if err != nil {
return "", fmt.Errorf("failed to create temp directory for compiled approximations: %w", err)
}

classpath := buildApproxClasspath(analyzerJarPath, extractedDir, projectModelDir)
if err := runJavac(javacPath, classpath, outputDir, javaFiles); err != nil {
_ = os.RemoveAll(outputDir)
return "", err
}

output.LogInfof("Approximation compilation succeeded, output: %s", outputDir)
return outputDir, nil
}

func collectJavaSources(root string) ([]string, error) {
var javaFiles []string
err := filepath.Walk(root, func(path string, fi os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
if !fi.IsDir() && strings.HasSuffix(fi.Name(), ".java") {
javaFiles = append(javaFiles, path)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to walk approximations directory: %w", err)
}
return javaFiles, nil
}

func resolveJavacPath() (string, error) {
javacRunner := java.NewJavaRunner().
WithSkipVerify(globals.Config.SkipVerify).
WithImageType(java.AdoptiumImageJDK).
TrySystem().
TrySpecificVersion(globals.DefaultJavaVersion)

javaPath, err := javacRunner.EnsureJava()
if err != nil {
return "", fmt.Errorf("failed to resolve Java for approximation compilation: %w", err)
}

javacPath := java.DeriveJavacPath(javaPath)
if _, err := os.Stat(javacPath); err != nil {
return "", fmt.Errorf("javac not found at %s (resolved from java at %s). A JDK (not JRE) is required to compile approximation sources", javacPath, javaPath)
}
return javacPath, nil
}

// buildApproxClasspath assembles the javac classpath for approximation compilation:
// 1. Analyzer JAR — contains @Approximate, @ApproximateByName annotations
// 2. Extracted approximation utilities — OpentaintNdUtil, ArgumentTypeContext
// 3. Project dependencies — library JARs that approximation code may reference
func buildApproxClasspath(analyzerJarPath, extractedDir, projectModelDir string) string {
parts := []string{analyzerJarPath, extractedDir}
parts = append(parts, resolveProjectDependencies(projectModelDir)...)
return strings.Join(parts, string(os.PathListSeparator))
}

func runJavac(javacPath, classpath, outputDir string, javaFiles []string) error {
args := []string{
"-source", "8",
"-target", "8",
"-cp", classpath,
"-d", outputDir,
}
args = append(args, javaFiles...)

output.LogDebugf("Running javac: %s %s", javacPath, strings.Join(args, " "))

cmd := exec.Command(javacPath, args...)
cmdOutput, cmdErr := cmd.CombinedOutput()
if cmdErr != nil {
return fmt.Errorf(
"approximation compilation failed:\n%s\njavac exited with: %w",
string(cmdOutput), cmdErr,
)
}
return nil
}

// resolveProjectDependencies reads project.yaml from the project model directory
// and returns absolute paths to the dependency JARs listed there.
func resolveProjectDependencies(projectModelDir string) []string {
if projectModelDir == "" {
return nil
}
config, err := project.LoadConfig(projectModelDir)
if err != nil {
output.LogDebugf("Could not read project config for approximation compilation: %v", err)
return nil
}
var absDeps []string
for _, dep := range config.Dependencies {
absPath := dep
if !filepath.IsAbs(dep) {
absPath = filepath.Join(projectModelDir, dep)
}
if _, err := os.Stat(absPath); err == nil {
absDeps = append(absDeps, absPath)
}
}
output.LogDebugf("Resolved %d project dependencies for approximation classpath", len(absDeps))
return absDeps
}
5 changes: 4 additions & 1 deletion cli/cmd/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,13 @@ func (c *JavaAutobuilderConfig) runAutobuilder() error {
return true
}

err = javaRunner.ExecuteJavaCommand(autobuilderCommand, commandSucceeded)
cmdErr, err := javaRunner.ExecuteJavaCommand(autobuilderCommand, commandSucceeded)
if err != nil {
return fmt.Errorf("native autobuilder execution failed: %w", err)
}
if cmdErr != nil {
return fmt.Errorf("native autobuilder execution failed: %w", cmdErr)
}

config, err := validation.ValidateProjectModelOutput(c.outputDir)
if err != nil {
Expand Down
Loading
Loading