Skip to content

Multi-Module and Monorepo Projects

This page describes how to integrate MethodAtlas into projects built as multiple Maven or Gradle submodules within a single repository. It covers per-module scanning, the -emit-source-root flag for record disambiguation, cache management, CSV aggregation, and cross-module coverage analysis.

How MethodAtlas handles multi-module projects

MethodAtlas scans one or more directory paths in a single invocation. It does not read Maven pom.xml or Gradle build scripts; it traverses the given paths looking for files whose names end with the configured suffix (default: Test.java). This means multi-module support is achieved by supplying multiple root paths to a single invocation, or by running separate per-module invocations and aggregating the results.

Both approaches are valid; the choice depends on whether you need per-module caching and per-module SARIF artefacts, or a single consolidated output.

Disambiguating records in multi-root scans

When multiple modules contain test classes with the same fully qualified class name — which can happen in monorepos that follow an identical package convention — the default CSV output does not distinguish which scan root a record came from. Use -emit-source-root to append the scan root path as an additional column in every output row:

java -jar methodatlas.jar \
  -ai -ai-provider openai -ai-api-key-env OPENAI_API_KEY \
  -content-hash \
  -emit-source-root \
  module-auth/src/test/java \
  module-payment/src/test/java \
  module-reporting/src/test/java \
  > security-tests-all.csv

The source_root column in the output identifies which scan root produced each record, making the CSV unambiguous even when class names collide across modules. This column is preserved when the CSV is used as an -ai-cache on subsequent runs.

Approach 1: single invocation across all modules

Supply all test source roots as positional arguments to a single MethodAtlas invocation. The tool scans all paths and emits one unified CSV or SARIF:

java -jar methodatlas.jar \
  -ai -ai-provider openai -ai-api-key-env OPENAI_API_KEY \
  -content-hash -sarif -security-only \
  -emit-source-root \
  module-auth/src/test/java \
  module-payment/src/test/java \
  module-reporting/src/test/java \
  > security-tests.sarif

This approach is the simplest to configure and produces a single output artefact. It is well-suited to projects with a small number of modules.

Trade-off: with a single cache file, a change to one module's test source invalidates the cached classifications for classes in that module, but leaves all other modules' cached results intact. The cache still provides savings; it does not need to be structured per-module.

Approach 2: per-module invocations with aggregation

Run a separate MethodAtlas invocation for each module and combine the results into a project-level CSV. This approach provides finer-grained control over caching and allows per-module SARIF artefacts.

Per-module scan

for module in module-auth module-payment module-reporting; do
  java -jar methodatlas.jar \
    -ai -ai-provider openai -ai-api-key-env OPENAI_API_KEY \
    -content-hash \
    -emit-source-root \
    -ai-cache .methodatlas-cache-${module}.csv \
    ${module}/src/test/java \
    > scan-${module}.csv
done

Each module has its own cache file (.methodatlas-cache-${module}.csv), so a change in one module does not force re-classification of classes in other modules.

Aggregating CSV outputs

To combine per-module CSVs into a single project-level CSV, keep the header from the first file and concatenate the data rows from all others:

# Write header from the first module
head -n 1 scan-module-auth.csv > security-tests-all.csv

# Append data rows from all modules (skip the header of each)
for f in scan-module-*.csv; do
  tail -n +2 "$f"
done >> security-tests-all.csv

The resulting security-tests-all.csv can be used with -diff for project- level delta gating, or retained as a consolidated evidence artefact.

Producing a project-level SARIF

Run a second pass with -sarif using the aggregated cache:

java -jar methodatlas.jar \
  -ai -ai-provider openai -ai-api-key-env OPENAI_API_KEY \
  -sarif -security-only \
  -emit-source-root \
  -ai-cache .methodatlas-cache-combined.csv \
  module-auth/src/test/java \
  module-payment/src/test/java \
  module-reporting/src/test/java \
  > security-tests-all.sarif

GitHub Actions: per-module matrix

For projects where each module's scan should appear as a separate pipeline job, use a matrix strategy:

name: Security test scan — monorepo

on:
  push:
    branches: [main]
  pull_request:

jobs:
  scan:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        module: [module-auth, module-payment, module-reporting]
      fail-fast: false
    permissions:
      contents: read

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Download MethodAtlas
        run: |
          curl -fsSL -o methodatlas.jar \
            https://github.com/Accenture/MethodAtlas/releases/latest/download/methodatlas.jar

      - name: Restore AI cache
        uses: actions/cache@v4
        with:
          path: .methodatlas-cache-${{ matrix.module }}.csv
          key: methodatlas-${{ matrix.module }}-${{ hashFiles(format('{0}/src/test/java/**/*.java', matrix.module)) }}
          restore-keys: methodatlas-${{ matrix.module }}-

      - name: Scan module
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          CACHE_ARG=""
          if [ -f .methodatlas-cache-${{ matrix.module }}.csv ]; then
            CACHE_ARG="-ai-cache .methodatlas-cache-${{ matrix.module }}.csv"
          fi

          java -jar methodatlas.jar \
            -ai -ai-provider openai -ai-api-key-env OPENAI_API_KEY \
            -content-hash -sarif -security-only \
            -emit-source-root \
            $CACHE_ARG \
            ${{ matrix.module }}/src/test/java \
            > scan-${{ matrix.module }}.sarif

          # Also emit CSV for aggregation and caching
          java -jar methodatlas.jar \
            -ai -ai-provider openai -ai-api-key-env OPENAI_API_KEY \
            -content-hash -security-only \
            -emit-source-root \
            -ai-cache ${{ matrix.module }}/src/test/java \
            $CACHE_ARG \
            ${{ matrix.module }}/src/test/java \
            > .methodatlas-cache-${{ matrix.module }}.csv

      - uses: actions/upload-artifact@v4
        with:
          name: scan-${{ matrix.module }}
          path: |
            scan-${{ matrix.module }}.sarif
            .methodatlas-cache-${{ matrix.module }}.csv
          retention-days: 30

  aggregate:
    runs-on: ubuntu-latest
    needs: scan
    permissions:
      contents: read

    steps:
      - uses: actions/download-artifact@v4
        with:
          pattern: scan-*
          merge-multiple: true
          path: scans

      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Download MethodAtlas
        run: |
          curl -fsSL -o methodatlas.jar \
            https://github.com/Accenture/MethodAtlas/releases/latest/download/methodatlas.jar

      - name: Aggregate CSVs
        run: |
          FIRST_CSV=$(ls scans/*.csv | head -1)
          head -n 1 "$FIRST_CSV" > security-tests-all.csv
          for f in scans/*.csv; do tail -n +2 "$f"; done >> security-tests-all.csv

      - uses: actions/upload-artifact@v4
        with:
          name: methodatlas-project
          path: security-tests-all.csv
          retention-days: 90

Cross-module coverage analysis

A common gap in multi-module projects is that a service module contains the production implementation of a security control, while the integration tests for that control live in a separate test module. MethodAtlas running on the service module may find no security tests, even though the project as a whole tests the control adequately.

Identifying the gap

Run the aggregated scan and filter for modules with no security-relevant methods:

# Count security-relevant tests per module (prefix = first two components of FQCN)
awk -F',' 'NR > 1 && $5 == "true" {
  split($1, parts, ".")
  print parts[1] "." parts[2]
}' security-tests-all.csv | sort | uniq -c | sort -rn

A module that appears in the full inventory but not in this filtered count has test methods but none classified as security-relevant. This warrants review: either the module has no security responsibilities, the tests are misclassified, or security tests have not yet been written.

Documenting intent

If a module intentionally delegates its security testing to an integration or end-to-end test module, document this in the override file to prevent audit confusion:

# methodatlas-overrides.yaml
overrides:
  # module-reporting has no security tests because its auth
  # boundary is enforced and tested at the API gateway layer
  - fqcn: com.acme.reporting.ReportExportTest
    securityRelevant: false
    note: "Auth boundary tested in module-gateway/src/test  2026-04-25 alice"

Gradle multi-project builds

For Gradle multi-project builds, you can enumerate test source roots programmatically in the pipeline rather than listing them statically:

# Discover all test source directories in the project
TEST_ROOTS=$(find . -path '*/src/test/java' -not -path '*/build/*' | tr '\n' ' ')

java -jar methodatlas.jar \
  -ai -ai-provider openai -ai-api-key-env OPENAI_API_KEY \
  -content-hash -sarif -security-only \
  -emit-source-root \
  $TEST_ROOTS \
  > security-tests.sarif

This approach automatically includes new submodules without pipeline changes, but produces a single cache file for the entire project.

Further reading