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:
autoruns it when its language is present and its tool is available. Otherwise it is skipped, with one log line saying why.truemakes it required: if it cannot run or it crashes, the scan exits with code 3.falseturns 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:
- Install the dependencies before the scan (
npm ci,pnpm install --frozen-lockfile,yarn install --immutable). The scanner image has Node.js, npm and corepack. - 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-defaultis PMD’s ownrulesets/java/quickstart.xml. Use your own withanalyzers.pmd.rulesets: [config/pmd.xml], or PMD’s built-in categories (category/java/bestpractices.xml). - SpotBugs reads compiled classes. Build first (
mvn -B -DskipTests packageor./gradlew classes). It looks intarget/classesandbuild/classes/java/mainby default. When those are empty it is skipped, with the reasonno 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-incrementalmatters. A project that is not recompiled writes no log, and you get the warningROSLYN_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
.editorconfigand.globalconfigstill decide severities. A rule set tononestays off. If the project references Roslynator itself, its own version is used. qualor dotnet abortcleans up when the build fails, so no hook is left behind.- A plain
qualor scandoes 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
.trivyignoreat 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
1does 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.