Files
nuclei-operator/internal/annotations/annotations.go
Morten Olsen 335689da22 feat: implement pod-based scanning architecture
This major refactor moves from synchronous subprocess-based scanning to
asynchronous pod-based scanning using Kubernetes Jobs.

## Architecture Changes
- Scanner jobs are now Kubernetes Jobs with TTLAfterFinished for automatic cleanup
- Jobs have owner references for garbage collection when NucleiScan is deleted
- Configurable concurrency limits, timeouts, and resource requirements

## New Features
- Dual-mode binary: --mode=controller (default) or --mode=scanner
- Annotation-based configuration for Ingress/VirtualService resources
- Operator-level configuration via environment variables
- Startup recovery for orphaned scans after operator restart
- Periodic cleanup of stuck jobs

## New Files
- DESIGN.md: Comprehensive architecture design document
- internal/jobmanager/: Job Manager for creating/monitoring scanner jobs
- internal/scanner/runner.go: Scanner mode implementation
- internal/annotations/: Annotation parsing utilities
- charts/nuclei-operator/templates/scanner-rbac.yaml: Scanner RBAC

## API Changes
- Added ScannerConfig struct for per-scan scanner configuration
- Added JobReference struct for tracking scanner jobs
- Added ScannerConfig field to NucleiScanSpec
- Added JobRef and ScanStartTime fields to NucleiScanStatus

## Supported Annotations
- nuclei.homelab.mortenolsen.pro/enabled
- nuclei.homelab.mortenolsen.pro/templates
- nuclei.homelab.mortenolsen.pro/severity
- nuclei.homelab.mortenolsen.pro/schedule
- nuclei.homelab.mortenolsen.pro/timeout
- nuclei.homelab.mortenolsen.pro/scanner-image

## RBAC Updates
- Added Job and Pod permissions for operator
- Created separate scanner service account with minimal permissions

## Documentation
- Updated README, user-guide, api.md, and Helm chart README
- Added example annotated Ingress resources
2025-12-12 20:51:23 +01:00

212 lines
5.3 KiB
Go

/*
Copyright 2024.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package annotations
import (
"strconv"
"strings"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
nucleiv1alpha1 "github.com/mortenolsen/nuclei-operator/api/v1alpha1"
)
const (
// AnnotationPrefix is the prefix for all nuclei annotations
AnnotationPrefix = "nuclei.homelab.mortenolsen.pro/"
// AnnotationEnabled controls whether scanning is enabled for a resource
AnnotationEnabled = AnnotationPrefix + "enabled"
// AnnotationTemplates specifies comma-separated template paths or tags
AnnotationTemplates = AnnotationPrefix + "templates"
// AnnotationSeverity specifies comma-separated severity filter
AnnotationSeverity = AnnotationPrefix + "severity"
// AnnotationSchedule specifies the cron schedule for periodic scans
AnnotationSchedule = AnnotationPrefix + "schedule"
// AnnotationTimeout specifies the scan timeout
AnnotationTimeout = AnnotationPrefix + "timeout"
// AnnotationScannerImage overrides the scanner image
AnnotationScannerImage = AnnotationPrefix + "scanner-image"
// AnnotationExcludeTemplates specifies templates to exclude
AnnotationExcludeTemplates = AnnotationPrefix + "exclude-templates"
// AnnotationRateLimit specifies requests per second limit
AnnotationRateLimit = AnnotationPrefix + "rate-limit"
// AnnotationTags specifies template tags to include
AnnotationTags = AnnotationPrefix + "tags"
// AnnotationExcludeTags specifies template tags to exclude
AnnotationExcludeTags = AnnotationPrefix + "exclude-tags"
)
// ScanConfig holds parsed annotation configuration
type ScanConfig struct {
// Enabled indicates if scanning is enabled
Enabled bool
// Templates to use for scanning
Templates []string
// Severity filter
Severity []string
// Schedule for periodic scans (cron format)
Schedule string
// Timeout for the scan
Timeout *metav1.Duration
// ScannerImage overrides the default scanner image
ScannerImage string
// ExcludeTemplates to exclude from scanning
ExcludeTemplates []string
// RateLimit for requests per second
RateLimit int
// Tags to include
Tags []string
// ExcludeTags to exclude
ExcludeTags []string
}
// ParseAnnotations extracts scan configuration from resource annotations
func ParseAnnotations(annotations map[string]string) *ScanConfig {
config := &ScanConfig{
Enabled: true, // Default to enabled
}
if annotations == nil {
return config
}
// Parse enabled
if v, ok := annotations[AnnotationEnabled]; ok {
config.Enabled = strings.ToLower(v) == "true"
}
// Parse templates
if v, ok := annotations[AnnotationTemplates]; ok && v != "" {
config.Templates = splitAndTrim(v)
}
// Parse severity
if v, ok := annotations[AnnotationSeverity]; ok && v != "" {
config.Severity = splitAndTrim(v)
}
// Parse schedule
if v, ok := annotations[AnnotationSchedule]; ok {
config.Schedule = v
}
// Parse timeout
if v, ok := annotations[AnnotationTimeout]; ok {
if d, err := time.ParseDuration(v); err == nil {
config.Timeout = &metav1.Duration{Duration: d}
}
}
// Parse scanner image
if v, ok := annotations[AnnotationScannerImage]; ok {
config.ScannerImage = v
}
// Parse exclude templates
if v, ok := annotations[AnnotationExcludeTemplates]; ok && v != "" {
config.ExcludeTemplates = splitAndTrim(v)
}
// Parse rate limit
if v, ok := annotations[AnnotationRateLimit]; ok {
if n, err := strconv.Atoi(v); err == nil {
config.RateLimit = n
}
}
// Parse tags
if v, ok := annotations[AnnotationTags]; ok && v != "" {
config.Tags = splitAndTrim(v)
}
// Parse exclude tags
if v, ok := annotations[AnnotationExcludeTags]; ok && v != "" {
config.ExcludeTags = splitAndTrim(v)
}
return config
}
// ApplyToNucleiScanSpec applies the annotation config to a NucleiScan spec
func (c *ScanConfig) ApplyToNucleiScanSpec(spec *nucleiv1alpha1.NucleiScanSpec) {
// Apply templates if specified
if len(c.Templates) > 0 {
spec.Templates = c.Templates
}
// Apply severity if specified
if len(c.Severity) > 0 {
spec.Severity = c.Severity
}
// Apply schedule if specified
if c.Schedule != "" {
spec.Schedule = c.Schedule
}
// Apply scanner config if any scanner-specific settings are specified
if c.ScannerImage != "" || c.Timeout != nil {
if spec.ScannerConfig == nil {
spec.ScannerConfig = &nucleiv1alpha1.ScannerConfig{}
}
if c.ScannerImage != "" {
spec.ScannerConfig.Image = c.ScannerImage
}
if c.Timeout != nil {
spec.ScannerConfig.Timeout = c.Timeout
}
}
}
// IsEnabled returns true if scanning is enabled
func (c *ScanConfig) IsEnabled() bool {
return c.Enabled
}
// splitAndTrim splits a string by comma and trims whitespace from each element
func splitAndTrim(s string) []string {
parts := strings.Split(s, ",")
result := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
result = append(result, p)
}
}
return result
}