Skip to content
Open
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
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ subprojects {

dependencies {
detektPlugins(project(":import-extension"))
detektPlugins(project(":micronaut-extension"))
}

tasks.withType<Detekt>().configureEach {
Expand Down
124 changes: 124 additions & 0 deletions micronaut-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Micronaut Extension for Detekt

A detekt extension that provides security and best practice rules for Micronaut applications.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are we going to be using micronaut controllers over spring?

I also assume this extension doesn't work for spring annotated controllers with annotations like @GetMapping @PutMapping, @PostMapping, right?

Also for the roles can you reference DSM rbac roles in the annotations like SUPER_USER, etc? Or where do you get roles from for the @RolesAllowed and @Secured annotations?


## Rules

### RequireSecuredAnnotation

**Severity:** Security
**Debt:** 5 minutes

Ensures that all Micronaut controller endpoint methods have security annotations to prevent accidentally creating unsecured endpoints.

#### Description

This rule checks that every method annotated with HTTP method annotations (`@Get`, `@Post`, `@Put`, `@Delete`, `@Patch`, `@Head`, `@Options`, `@Trace`) also has one of the following security annotations:

- `@Secured` (io.micronaut.security.annotation.Secured)
- `@PermitAll` (jakarta.annotation.security.PermitAll)
- `@RolesAllowed` (jakarta.annotation.security.RolesAllowed)
- `@DenyAll` (jakarta.annotation.security.DenyAll)

#### Noncompliant Code

```kotlin
@Controller("/api/users")
class UserController {
// ❌ Missing security annotation
@Get("/{id}")
fun getUser(id: String): User {
return userService.findById(id)
}
}
```

#### Compliant Code

```kotlin
@Controller("/api/users")
class UserController {
// ✅ Has @Secured annotation
@Secured("ROLE_USER")
@Get("/{id}")
fun getUser(id: String): User {
return userService.findById(id)
}

// ✅ Public endpoint explicitly marked with @PermitAll
@PermitAll
@Get("/public")
fun getPublicInfo(): PublicInfo {
return publicInfoService.getInfo()
}

// ✅ Admin-only endpoint
@RolesAllowed("ADMIN")
@Delete("/{id}")
fun deleteUser(id: String) {
userService.delete(id)
}

// ✅ Explicitly denied
@DenyAll
@Get("/forbidden")
fun forbiddenEndpoint() {
// This endpoint is always forbidden
}
}
```

## Installation

### Gradle (Kotlin DSL)

```kotlin
dependencies {
detektPlugins("com.pkware.detekt:micronaut-extension:1.3.0")
}
```

### Gradle (Groovy DSL)

```groovy
dependencies {
detektPlugins 'com.pkware.detekt:micronaut-extension:1.3.0'
}
```

## Configuration

Add to your `detekt.yml`:

```yaml
micronaut:
RequireSecuredAnnotation:
active: true
```

## Suppressing the Rule

If you need to suppress this rule for a specific method:

```kotlin
@Suppress("RequireSecuredAnnotation")
@Get("/special-case")
fun specialCase() {
// This endpoint won't be checked
}
```

## Why This Rule Matters

Accidentally exposing unsecured endpoints is a common security vulnerability. This rule helps prevent:

- **Data leaks**: Sensitive data exposed through unsecured endpoints
- **Unauthorized operations**: Critical operations (delete, update) accessible without authentication
- **Privilege escalation**: Admin-only endpoints accessible to regular users

By requiring explicit security annotations on all endpoints, this rule enforces a "secure by default" approach where developers must consciously choose the security level for each endpoint.

## References

- [Micronaut Security Documentation](https://micronaut-projects.github.io/micronaut-security/latest/guide/)
- [Jakarta Security Annotations](https://jakarta.ee/specifications/security/)
150 changes: 150 additions & 0 deletions micronaut-extension/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
plugins {
kotlin("jvm")
`maven-publish`
signing
alias(libs.plugins.ksp)
}

val detektExtensionVersion: String by project
version = detektExtensionVersion

ksp {
arg("autoserviceKsp.verify", "true")
}

dependencies {
ksp(libs.auto.service.ksp)
implementation(libs.auto.service.annotations)
implementation(libs.detekt.tooling)
implementation(libs.detekt.api)

testImplementation(libs.detekt.test)
testImplementation(libs.detekt.parser)
testImplementation(libs.junit.jupiter.params)
testImplementation(libs.assertj)

testRuntimeOnly(libs.junit.jupiter.engine)
}

kotlin {
jvmToolchain { languageVersion.set(JavaLanguageVersion.of(8)) }
}

// <editor-fold desc="Publishing and Signing">

java {
withJavadocJar()
withSourcesJar()
}

publishing {
publications {
create<MavenPublication>("mavenJava") {
artifactId = pomArtifactId
from(components["java"])
pom {
name.set(pomName)
packaging = pomPackaging
description.set(pomDescription)
url.set("https://github.com/pkware/detektExtensions")
setPkwareOrganization()

developers {
developer {
id.set("all")
name.set("PKWARE, Inc.")
}
}

scm {
connection.set("scm:git:git://github.com/pkware/detektExtensions.git")
developerConnection.set("scm:git:ssh://github.com/pkware/detektExtensions.git")
url.set("https://github.com/pkware/detektExtensions")
}

licenses {
license {
name.set("MIT License")
distribution.set("repo")
url.set("https://github.com/pkware/detektExtensions/blob/master/LICENSE")
}
}
}
}
}
repositories {
maven {
name = "MavenCentral"
url = uri(if (version.toString().isReleaseBuild) releaseRepositoryUrl else snapshotRepositoryUrl)
credentials {
username = repositoryUsername
password = repositoryPassword
}
}
}
}

signing {
// Signing credentials are stored as secrets in GitHub.
// See https://docs.gradle.org/current/userguide/signing_plugin.html#sec:signatory_credentials for more information.

useInMemoryPgpKeys(
signingKeyId, // ID of the GPG key
signingKey, // GPG key
signingPassword, // Password for the GPG key
)

sign(publishing.publications["mavenJava"])
}

val String.isReleaseBuild
get() = !contains("SNAPSHOT")

val Project.releaseRepositoryUrl: String
get() =
properties.getOrDefault(
"RELEASE_REPOSITORY_URL",
"https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2",
).toString()

val Project.snapshotRepositoryUrl: String
get() =
properties.getOrDefault(
"SNAPSHOT_REPOSITORY_URL",
"https://central.sonatype.com/repository/maven-snapshots/",
).toString()

val Project.repositoryUsername: String
get() = properties.getOrDefault("NEXUS_USERNAME", "").toString()

val Project.repositoryPassword: String
get() = properties.getOrDefault("NEXUS_PASSWORD", "").toString()

val Project.signingKeyId: String
get() = properties.getOrDefault("SIGNING_KEY_ID", "").toString()

val Project.signingKey: String
get() = properties.getOrDefault("SIGNING_KEY", "").toString()

val Project.signingPassword: String
get() = properties.getOrDefault("SIGNING_PASSWORD", "").toString()

val Project.pomPackaging: String
get() = properties.getOrDefault("POM_PACKAGING", "jar").toString()

val Project.pomName: String?
get() = properties["POM_NAME"]?.toString()

val Project.pomDescription: String?
get() = properties["POM_DESCRIPTION"]?.toString()

val Project.pomArtifactId
get() = properties.getOrDefault("POM_ARTIFACT_ID", name).toString()

fun MavenPom.setPkwareOrganization() {
organization {
name.set("PKWARE, Inc.")
url.set("https://www.pkware.com")
}
}
// </editor-fold>
6 changes: 6 additions & 0 deletions micronaut-extension/detekt-example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example detekt configuration for using the Micronaut extension

micronaut:
active: true
RequireSecuredAnnotation:
active: true
4 changes: 4 additions & 0 deletions micronaut-extension/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
POM_ARTIFACT_ID=micronaut-extension
POM_NAME=Micronaut Extension for Detekt
POM_DESCRIPTION=A Detekt extension providing security and best practice rules for Micronaut applications
POM_PACKAGING=jar
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.pkware.detekt.extensions.rules.micronaut

import com.google.auto.service.AutoService
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.RuleSet
import io.gitlab.arturbosch.detekt.api.RuleSetProvider

/**
* The Micronaut extension rule set provider for this detekt extension.
* Provides security and best practice rules for Micronaut applications.
*
* @see <a href="https://detekt.github.io/detekt/extensions.html">https://detekt.github.io/detekt/extensions.html</a>
*/
@AutoService(RuleSetProvider::class)
class MicronautExtensionProvider : RuleSetProvider {
override val ruleSetId: String = "micronaut"

override fun instance(config: Config): RuleSet = RuleSet(
ruleSetId,
listOf(
RequireSecuredAnnotation(config),
),
)
}
Loading
Loading