Languages and analyzers

Qualor has no rule engine of its own. It runs well-known open-source analyzers, reads their SARIF output, and turns their findings into tracked issues. For the supported languages it also computes size, complexity, duplication and coverage.

Area Analyzer Runs when
JavaScript, TypeScript ESLint, the project’s own config and plugins the repository has an ESLint config and its dependencies are installed
Java PMD 7 (source) and SpotBugs (bytecode) .java files exist. SpotBugs also needs compiled classes
C# Roslyn analyzers of the .NET SDK, plus Roslynator through qualor dotnet begin / end around your build
Security patterns (SAST) OpenGrep (or Semgrep) you name rule files in qualor.yml. No rules are bundled yet
Secrets Gitleaks always (enabled: true by default)
Vulnerable dependencies Trivy lockfiles or manifests exist
Anything else any tool that writes SARIF 2.1.0 you pass --sarif file or list it in qualor.yml

Metrics (lines of code, functions, classes, cyclomatic and cognitive complexity) and duplication detection cover TypeScript, JavaScript, Java and C#. Other files still get findings from Gitleaks, Trivy, OpenGrep and external SARIF.

Each analyzer has enabled: auto | true | false in qualor.yml:

  • auto runs it when its language is present and its tool is available. Otherwise it is skipped, with one log line saying why.
  • true makes it required: if it cannot run or it crashes, the scan exits with code 3.
  • false turns it off.

The analyzers run in parallel (at most 4 at once) and offline. No analyzer downloads rules, databases or plugins during a scan.

JavaScript and TypeScript (ESLint)

ESLint runs from the project’s own node_modules with the project’s own config, because that is the only way plugin rules resolve. So:

  1. Install the dependencies before the scan (npm ci, pnpm install --frozen-lockfile, yarn install --immutable). The scanner image has Node.js, npm and corepack.
  2. Keep an ESLint config at the repository root: eslint.config.js (or .mjs, .cjs, .ts), or a legacy .eslintrc*. ESLint 9+ reads only flat configs. Without a config, ESLint is skipped, because no default config is bundled.
analyzers:
  eslint:
    configFile: config/eslint.config.js   # optional; default: ESLint's own lookup
    args: ['--ext', '.ts,.tsx']           # optional; appended to the command line

ESLint runs the config and plugins from the checkout, so it runs repository code. Never scan untrusted merge requests (for example from forks) in a job that holds secrets.

Java (PMD and SpotBugs)

  • PMD reads the source. The default ruleset qualor-default is PMD’s own rulesets/java/quickstart.xml. Use your own with analyzers.pmd.rulesets: [config/pmd.xml], or PMD’s built-in categories (category/java/bestpractices.xml).
  • SpotBugs reads compiled classes. Build first (mvn -B -DskipTests package or ./gradlew classes). It looks in target/classes and build/classes/java/main by default. When those are empty it is skipped, with the reason no compiled classes … (build the project before qualor scan).
analyzers:
  pmd: { rulesets: [config/pmd.xml] }
  spotbugs:
    classDirs: [app/target/classes, lib/target/classes]
    auxClasspathFile: target/classpath.txt   # optional: one classpath entry per line

For a better SpotBugs result on a Maven project, write the classpath first: mvn dependency:build-classpath -Dmdep.outputFile=target/classpath.txt. That file has one line separated by :. Convert it to one entry per line, for example with tr ':' '\n'.

C#

Roslyn analyzers need the compiler’s semantic model, so Qualor hooks into your own build instead of building anything itself. SonarScanner for .NET works the same way:

qualor dotnet begin                 # installs an MSBuild hook for this checkout
dotnet build --no-incremental       # your build: its SDK, restore, feeds and arguments
qualor dotnet end                   # removes the hook, reads the Roslyn logs, runs qualor scan

Use the qualor/scanner-dotnet image. It adds the .NET 8 and .NET 10 SDKs and Roslynator to the scanner. GitLab:

qualor:
  image: { name: qualor/scanner-dotnet:1, entrypoint: [''] }
  variables: { GIT_DEPTH: 0 }
  script:
    - qualor dotnet begin
    - dotnet build MySolution.sln --no-incremental
    - qualor dotnet end
  after_script:
    - if [ -d .qualor/dotnet ]; then qualor dotnet abort; fi

With the GitLab component, set inputs: { dotnet: true, image: qualor/scanner-dotnet, build-command: 'dotnet build MySolution.sln --no-incremental' }. For GitHub, use integrations/github/qualor-dotnet.yml (GitHub).

  • --no-incremental matters. A project that is not recompiled writes no log, and you get the warning ROSLYN_PROJECT_NOT_ANALYZED.
  • Inside the scan job, the hook turns off warnings as errors, so the extra rules cannot break a build that passes without them. Compiler errors still fail it. If your pipeline must fail on warnings, keep a separate build job for that.
  • The repository’s .editorconfig and .globalconfig still decide severities. A rule set to none stays off. If the project references Roslynator itself, its own version is used.
  • qualor dotnet abort cleans up when the build fails, so no hook is left behind.
  • A plain qualor scan does not analyse C#.

Secrets (Gitleaks)

Gitleaks scans the working tree with its built-in rules, or with .gitleaks.toml at the repository root. It honours .gitleaksignore and gitleaks:allow comments. The secret itself never reaches the report or the server: the finding’s message is the rule’s text.

Gitleaks is enabled: true by default, so a scan outside the scanner image without Gitleaks exits 3. Set analyzers.gitleaks.enabled: auto to make it optional.

Dependencies (Trivy)

Trivy reads lockfiles and manifests (package-lock.json, yarn.lock, pnpm-lock.yaml, pom.xml, gradle.lockfile, and Go, Python, Ruby, Rust, PHP and .NET lockfiles) and reports packages with known vulnerabilities. Development dependencies are left out. A vulnerability counts as new code only when the merge request adds the package or changes its version. So a database update never fails a merge request that did not touch the dependency.

  • Ignore a vulnerability by id in .trivyignore at the repository root.
  • The vulnerability database comes from the image and is never downloaded during a scan. It is as old as the scanner release you run, so keep the scanner current (the major tag 1 does that), or fetch a fresh database in the job:
script:
  - trivy image --download-db-only --cache-dir /tmp/trivy   # add --db-repository <your mirror> if needed
  - QUALOR_TRIVY_CACHE_DIR=/tmp/trivy qualor scan

QUALOR_TRIVY_CACHE_DIR must be absolute and outside the checkout. Jobs that run repository code must not be able to write it.

SAST patterns (OpenGrep / Semgrep)

No rules are bundled yet. To use OpenGrep, keep rule files in the repository and name them:

analyzers:
  semgrep:
    configs: [.semgrep/]            # local files or directories only

Registry ids such as p/default are refused (exit 2), because they would fetch rules over the network.

External SARIF

Any tool that writes SARIF 2.1.0 can feed Qualor: osv-scanner, Checkov, tfsec, Bandit, golangci-lint, Hadolint and others. Run it before the scan, then:

qualor scan --sarif reports/osv.sarif --sarif reports/checkov.sarif

Or permanently in qualor.yml:

sarif:
  - path: reports/osv.sarif
    engine: osv-scanner            # optional; default: the SARIF tool name

Coverage

Qualor imports LCOV, Cobertura XML and JaCoCo XML. Run your tests with coverage before the scan, then list the reports:

coverage:
  reports:
    - path: coverage/lcov.info                          # Jest, Vitest, nyc, c8
    - path: '**/target/site/jacoco/jacoco.xml'          # Maven + JaCoCo
      format: jacoco
    - path: '**/coverage.cobertura.xml'                 # .NET: coverlet / dotnet-coverage
      format: cobertura
  pathPrefixes: []   # prefixes to strip or try when report paths do not match repository paths

Or pass --coverage <path> on the command line. Test files are excluded from coverage. If a scan imports no coverage report at all, the coverage conditions have no value, and they do not fail the gate. The UI shows a warning instead.

What is scanned

Every file in the working tree (sources.include, default **/*), minus what .gitignore ignores, minus the built-in excludes (node_modules, dist, build, target, vendor, *.min.js, .NET obj/ and generated *.g.cs / *.Designer.cs, and binary files), minus your own sources.exclude. Test files are recognised by tests.include (by default *.test.*, *.spec.*, __tests__/, src/test/, *Tests/). A committed coverage/ directory is not excluded automatically. Add it to sources.exclude if yours is generated output.