Guidance for Claude Code (or any contributor) working in this repo.
A Gradle plugin (net.continuous-security-tools.zap-gradle) intended to run OWASP ZAP
security scans as part of a Gradle build. It is the Gradle counterpart to
zap-maven-plugin.
ZapPlugin registers a
zap {} extension and three tasks (startZap, zapAnalyze, zapSeleniumAnalyze) that mirror
the Maven plugin's startZap/analyze/seleniumAnalyze goals — see README.md for
user-facing usage. Build, publishing, and release pipeline are fully wired up and working.
./gradlew build # compile, test, assemble, validatePlugins
./gradlew publishToMavenLocal # install a SNAPSHOT locally for testing consumptionSource is Groovy, under src/main/groovy (the groovy plugin is applied, not java).
The plugin does not reimplement any ZAP logic — it's a thin Gradle wrapper around the
net.continuous-security-tools:zap-utils / zap-client-api / zap-report-parser libraries
(the net.cst.zap.* packages; see Dependencies below), the same
libraries zap-maven-plugin's Mojos call directly. When extending plugin behavior, check what
ZapMojo/AnalyzeMojo/StartZapMojo/SeleniumAnalyzeMojo
do first — the Gradle side should stay behaviorally equivalent.
ZapPluginExtensionholds allzap {}config (plain mutable Groovy properties, not the lazyProperty<T>API — intentionally kept simple since this isn't aiming for configuration-cache support yet) and thebuildZapInfo()/buildAuthenticationInfo()/buildAnalysisInfo()builder methods, mirroringZapMojo's protected builder methods.targetUrlandzapPortare validated as required at build-time (throwsGradleException) since Gradle extensions don't have Maven's@Parameter(required = true)binding-time validation.StartZapTask,ZapAnalyzeTask,ZapSeleniumAnalyzeTaskeach hold azapExtensionfield (typeZapPluginExtension,@Internal) set once byZapPlugin.apply()at configuration time — do not fetch the extension viaproject.extensions.getByType(...)from inside@TaskActionmethods oronlyIfclosures.Task.getProject()at execution time is deprecated in Gradle 8/9 (breaks configuration-cache compatibility, hard error in Gradle 10) and nebula-test'sIntegrationTestKitSpecfails builds on deprecation warnings by default — this was caught by the functional tests, not by./gradlew buildalone.zap.skipis enforced viaonlyIf { !zapExtension.skip }on each task (not an early-return inside the task action) soSKIPPEDshows correctly in Gradle's task outcome reporting.
build.gradle pulls in net.continuous-security-tools:zap-utils,
net.continuous-security-tools:zap-client-api, and net.continuous-security-tools:zap-report-parser
at a shared zapJavaVersion (ext.zapJavaVersion in build.gradle). Note: zap-maven-plugin's
pom.xml pins these to 1.0.11, but that version was never published — verify against
https://search.maven.org/solrsearch/select?q=g:net.continuous-security-tools before bumping;
0.4.1 was the latest actually-published version as of 2026-07.
- Group:
net.continuous-security-tools - Artifact:
zap-gradle-plugin - Plugin id:
net.continuous-security-tools.zap-gradle - Implementation class:
net.continuoussecuritytools.zap.ZapPlugin
Note the group uses a hyphen (continuous-security-tools), but the Java package name can't
contain one, hence net.continuoussecuritytools.zap (no hyphen) for the actual class package.
Publishes to two independent destinations, both configured in build.gradle:
- Maven Central, via the
com.vanniktech.maven.publishplugin (mavenPublishing {}block), configured withGradlePublishPlugin()sincecom.gradle.plugin-publishis also applied — that combination is what makes vanniktech emit sources/javadoc jars and wire the plugin marker publication correctly. Do not addwithSourcesJar()/withJavadocJar()to thejava {}block — vanniktech'sGradlePublishPlugin()already registers those tasks, and doing both causes duplicate task registration.- Auth:
ORG_GRADLE_PROJECT_mavenCentralUsername/ORG_GRADLE_PROJECT_mavenCentralPassword(a Central Portal user token, not your login password — generate at central.sonatype.com). - Signing:
ORG_GRADLE_PROJECT_signingInMemoryKey(ASCII-armored private key) +ORG_GRADLE_PROJECT_signingInMemoryKeyPassword. No keyring import needed — Gradle'ssigningplugin consumes the armored key directly in memory. - The old
oss.sonatype.org(legacy OSSRH) endpoint is dead; don't reintroduce it.
- Auth:
- Gradle Plugin Portal, via
com.gradle.plugin-publish(gradlePlugin {}block) +./gradlew publishPlugins.- Auth:
GRADLE_PUBLISH_KEY/GRADLE_PUBLISH_SECRETenv vars, orgradle.publish.key/gradle.publish.secretin~/.gradle/gradle.properties. A local.env(gitignored) holds these for convenience — it is not consumed automatically by Gradle, it's just where the values live locally. - Gotcha (Renovate):
com.gradle.plugin-publish2.x makesdisplayNamea required property on eachplugins { create(...) { ... } }declaration (optional under 1.x, which this repo started on). Missing it failspublishPluginsat execution time withPlugin 'zapPlugin' has no 'displayName' property set—validatePlugins/./gradlew buildwon't catch it locally, only an actualpublishPluginsrun does. ThezapPlugindeclaration ingradlePlugin {}sets bothdisplayNameanddescriptionfor this reason.
- Auth:
jcenter() is gone from this repo (shut down in 2021) — repositories use mavenCentral().
version in build.gradle defaults to 0.0.1-SNAPSHOT, but reads
findProperty('releaseVersion') first:
version = findProperty('releaseVersion') ?: '0.0.1-SNAPSHOT'This exists specifically so release.yml can pass
-PreleaseVersion=X.Y.Z without editing/committing a version bump into build.gradle (unlike the
Maven sibling project, which uses maven-release-plugin to mutate and commit pom.xml). Local
and CI builds without that property stay on the -SNAPSHOT version.
.github/workflows/release.yml is manually triggered
(workflow_dispatch, releaseversion input) and:
- Mints a GitHub App token (
vars.CI_APP_ID/secrets.CI_PRIVATE_KEY) for bot-authored git operations — same app used byzap-maven-plugin. - Runs
publishAndReleaseToMavenCentral publishPlugins -PreleaseVersion=.... - Generates changelog entries with
git-cliff(cliff.toml), commitsCHANGELOG.md, tagsvX.Y.Z, pushes. - Drafts a GitHub release via
avakar/tag-and-release.
pipeline.yml is the separate, unrelated CI workflow that just runs ./gradlew build on every
push.
Two test specs under src/test/groovy, both Spock:
ZapPluginExtensionSpec— unit tests for the extension's builder/validation logic, usingProjectBuilder(fromgradleTestKit()) to construct a throwawayProjectwithout a full build.ZapPluginFunctionalSpec— applies the plugin in a real (temp) build via nebula-test'sIntegrationTestKitSpec(runTasks/runTasksAndFail), verifying task registration,zap.skip, and required-property validation. Deliberately doesn't exercisezapAnalyze/zapSeleniumAnalyzeagainst a real ZAP instance — no ZAP binary in CI — only the failure paths that don't need one.
Gotcha: Gradle's test task defaults to JUnit4 detection, but Spock 2.x is JUnit
Platform-based. Without test { useJUnitPlatform() } in build.gradle, ./gradlew test reports
BUILD SUCCESSFUL while silently running zero tests — check build/test-results/test/*.xml
for actual test counts if a change to this area seems suspiciously green.
Gotcha (Renovate): nebula-test:10.6.2 used to bring in spock-core/spock-junit4
transitively. Starting with nebula-test:12.x, its Gradle module metadata dropped Spock entirely
(only assertj-core, jspecify, junit-platform-launcher remain) even though
IntegrationTestKitSpec still extends spock.lang.Specification — a Renovate bump to 12.x alone
breaks compileTestGroovy with an unresolved spock.lang.Specification symbol. Fixed by declaring
org.spockframework:spock-core and spock-junit4 explicitly as testImplementation, pinned to
2.3-groovy-3.0 (must match the Groovy line Gradle bundles — check ./gradlew --version; Gradle
8.14.5 bundles Groovy 3.0.25). If a future Gradle bump moves to Groovy 4.x, this pin needs to move
to a -groovy-4.0 Spock build too.
gradleTestKit() is declared explicitly as testImplementation (not left to
java-gradle-plugin's auto-wiring) since it's needed for both ProjectBuilder (unit tests) and
GradleRunner (functional tests, via nebula-test).