mirror of
https://github.com/morten-olsen/homelab-nuclei-operator.git
synced 2026-02-08 02:16:23 +01:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6969f13fa7 | ||
|
|
7c014b9898 | ||
|
|
707f0dcaad | ||
|
|
4c14e2294a | ||
|
|
1677d02aa7 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -87,7 +87,7 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
# platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -60,6 +60,7 @@ USER 65532:65532
|
||||
|
||||
# Environment variables for nuclei
|
||||
ENV NUCLEI_TEMPLATES_PATH=/nuclei-templates
|
||||
ENV NUCLEI_BINARY_PATH=/usr/local/bin/nuclei
|
||||
ENV HOME=/home/nonroot
|
||||
|
||||
ENTRYPOINT ["/manager"]
|
||||
|
||||
@@ -77,6 +77,9 @@ type JobReference struct {
|
||||
// Name of the Job
|
||||
Name string `json:"name"`
|
||||
|
||||
// Namespace of the Job (may differ from NucleiScan namespace)
|
||||
Namespace string `json:"namespace"`
|
||||
|
||||
// UID of the Job
|
||||
UID string `json:"uid"`
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.19.0
|
||||
name: nucleiscans.nuclei.homelab.mortenolsen.pro
|
||||
labels:
|
||||
{{- include "nuclei-operator.labels" . | nindent 4 }}
|
||||
spec:
|
||||
group: nuclei.homelab.mortenolsen.pro
|
||||
names:
|
||||
@@ -14,310 +12,456 @@ spec:
|
||||
listKind: NucleiScanList
|
||||
plural: nucleiscans
|
||||
shortNames:
|
||||
- ns
|
||||
- nscan
|
||||
- ns
|
||||
- nscan
|
||||
singular: nucleiscan
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .status.phase
|
||||
name: Phase
|
||||
type: string
|
||||
- jsonPath: .status.summary.totalFindings
|
||||
name: Findings
|
||||
type: integer
|
||||
- jsonPath: .spec.sourceRef.kind
|
||||
name: Source
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: NucleiScan is the Schema for the nucleiscans API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: NucleiScanSpec defines the desired state of NucleiScan
|
||||
properties:
|
||||
schedule:
|
||||
description: |-
|
||||
Schedule for periodic rescanning in cron format
|
||||
If empty, scan runs once
|
||||
type: string
|
||||
severity:
|
||||
description: Severity filters scan results by severity level
|
||||
items:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .status.phase
|
||||
name: Phase
|
||||
type: string
|
||||
- jsonPath: .status.summary.totalFindings
|
||||
name: Findings
|
||||
type: integer
|
||||
- jsonPath: .spec.sourceRef.kind
|
||||
name: Source
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: NucleiScan is the Schema for the nucleiscans API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: NucleiScanSpec defines the desired state of NucleiScan
|
||||
properties:
|
||||
scannerConfig:
|
||||
description: ScannerConfig allows overriding scanner settings for
|
||||
this scan
|
||||
properties:
|
||||
image:
|
||||
description: Image overrides the default scanner image
|
||||
type: string
|
||||
type: array
|
||||
sourceRef:
|
||||
description:
|
||||
SourceRef references the Ingress or VirtualService being
|
||||
scanned
|
||||
properties:
|
||||
apiVersion:
|
||||
description: APIVersion of the source resource
|
||||
nodeSelector:
|
||||
additionalProperties:
|
||||
type: string
|
||||
kind:
|
||||
description: Kind of the source resource - Ingress or VirtualService
|
||||
enum:
|
||||
- Ingress
|
||||
- VirtualService
|
||||
type: string
|
||||
name:
|
||||
description: Name of the source resource
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the source resource
|
||||
type: string
|
||||
uid:
|
||||
description: UID of the source resource for owner reference
|
||||
type: string
|
||||
required:
|
||||
- apiVersion
|
||||
- kind
|
||||
- name
|
||||
- namespace
|
||||
- uid
|
||||
type: object
|
||||
suspend:
|
||||
description: Suspend prevents scheduled scans from running
|
||||
type: boolean
|
||||
targets:
|
||||
description:
|
||||
Targets is the list of URLs to scan, extracted from the
|
||||
source resource
|
||||
items:
|
||||
type: string
|
||||
minItems: 1
|
||||
type: array
|
||||
templates:
|
||||
description: |-
|
||||
Templates specifies which Nuclei templates to use
|
||||
If empty, uses default templates
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- sourceRef
|
||||
- targets
|
||||
type: object
|
||||
status:
|
||||
description: NucleiScanStatus defines the observed state of NucleiScan
|
||||
properties:
|
||||
completionTime:
|
||||
description: CompletionTime is when the last scan completed
|
||||
format: date-time
|
||||
type: string
|
||||
conditions:
|
||||
description: Conditions represent the latest available observations
|
||||
items:
|
||||
description:
|
||||
Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
description: NodeSelector for scanner pod scheduling
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
findings:
|
||||
description: |-
|
||||
Findings contains the array of scan results from Nuclei JSONL output
|
||||
Each element is a parsed JSON object from Nuclei output
|
||||
items:
|
||||
description: Finding represents a single Nuclei scan finding
|
||||
resources:
|
||||
description: Resources defines resource requirements for the scanner
|
||||
pod
|
||||
properties:
|
||||
description:
|
||||
description: Description provides details about the finding
|
||||
type: string
|
||||
extractedResults:
|
||||
description:
|
||||
ExtractedResults contains any data extracted by
|
||||
the template
|
||||
claims:
|
||||
description: |-
|
||||
Claims lists the names of resources, defined in spec.resourceClaims,
|
||||
that are used by this container.
|
||||
|
||||
This field depends on the
|
||||
DynamicResourceAllocation feature gate.
|
||||
|
||||
This field is immutable. It can only be set for containers.
|
||||
items:
|
||||
type: string
|
||||
description: ResourceClaim references one entry in PodSpec.ResourceClaims.
|
||||
properties:
|
||||
name:
|
||||
description: |-
|
||||
Name must match the name of one entry in pod.spec.resourceClaims of
|
||||
the Pod where this field is used. It makes that resource available
|
||||
inside a container.
|
||||
type: string
|
||||
request:
|
||||
description: |-
|
||||
Request is the name chosen for a request in the referenced claim.
|
||||
If empty, everything from the claim is made available, otherwise
|
||||
only the result of this request.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: array
|
||||
host:
|
||||
description: Host that was scanned
|
||||
type: string
|
||||
matchedAt:
|
||||
description:
|
||||
MatchedAt is the specific URL or endpoint where
|
||||
the issue was found
|
||||
type: string
|
||||
metadata:
|
||||
description: Metadata contains additional template metadata
|
||||
x-kubernetes-list-map-keys:
|
||||
- name
|
||||
x-kubernetes-list-type: map
|
||||
limits:
|
||||
additionalProperties:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||
x-kubernetes-int-or-string: true
|
||||
description: |-
|
||||
Limits describes the maximum amount of compute resources allowed.
|
||||
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
type: object
|
||||
requests:
|
||||
additionalProperties:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||
x-kubernetes-int-or-string: true
|
||||
description: |-
|
||||
Requests describes the minimum amount of compute resources required.
|
||||
If Requests is omitted for a container, it defaults to Limits if that is explicitly specified,
|
||||
otherwise to an implementation-defined value. Requests cannot exceed Limits.
|
||||
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
reference:
|
||||
description:
|
||||
Reference contains URLs to additional information
|
||||
about the finding
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
severity:
|
||||
description: Severity of the finding
|
||||
type: string
|
||||
tags:
|
||||
description: Tags associated with the finding
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
templateId:
|
||||
description: TemplateID is the Nuclei template identifier
|
||||
type: string
|
||||
templateName:
|
||||
description: TemplateName is the human-readable template name
|
||||
type: string
|
||||
timestamp:
|
||||
description: Timestamp when the finding was discovered
|
||||
format: date-time
|
||||
type: string
|
||||
type:
|
||||
description: Type of the finding - http, dns, ssl, etc.
|
||||
type: string
|
||||
required:
|
||||
- host
|
||||
- severity
|
||||
- templateId
|
||||
- timestamp
|
||||
type: object
|
||||
type: array
|
||||
lastError:
|
||||
description: LastError contains the error message if the scan failed
|
||||
type: string
|
||||
lastRetryTime:
|
||||
description:
|
||||
LastRetryTime is when the last availability check retry
|
||||
occurred
|
||||
format: date-time
|
||||
type: string
|
||||
lastScanTime:
|
||||
description: LastScanTime is when the last scan was initiated
|
||||
format: date-time
|
||||
type: string
|
||||
nextScheduledTime:
|
||||
description:
|
||||
NextScheduledTime is when the next scheduled scan will
|
||||
run
|
||||
format: date-time
|
||||
type: string
|
||||
observedGeneration:
|
||||
description:
|
||||
ObservedGeneration is the generation observed by the
|
||||
controller
|
||||
format: int64
|
||||
type: integer
|
||||
phase:
|
||||
description: Phase represents the current scan phase
|
||||
enum:
|
||||
- Pending
|
||||
- Running
|
||||
- Completed
|
||||
- Failed
|
||||
type: string
|
||||
retryCount:
|
||||
description: |-
|
||||
RetryCount tracks the number of consecutive availability check retries
|
||||
Used for exponential backoff when waiting for targets
|
||||
type: integer
|
||||
summary:
|
||||
description: Summary provides aggregated scan statistics
|
||||
properties:
|
||||
durationSeconds:
|
||||
description: DurationSeconds is the duration of the scan in seconds
|
||||
format: int64
|
||||
type: integer
|
||||
findingsBySeverity:
|
||||
additionalProperties:
|
||||
type: integer
|
||||
description:
|
||||
FindingsBySeverity breaks down findings by severity
|
||||
level
|
||||
templateURLs:
|
||||
description: TemplateURLs specifies additional template repositories
|
||||
to clone
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
timeout:
|
||||
description: Timeout overrides the default scan timeout
|
||||
type: string
|
||||
tolerations:
|
||||
description: Tolerations for scanner pod scheduling
|
||||
items:
|
||||
description: |-
|
||||
The pod this Toleration is attached to tolerates any taint that matches
|
||||
the triple <key,value,effect> using the matching operator <operator>.
|
||||
properties:
|
||||
effect:
|
||||
description: |-
|
||||
Effect indicates the taint effect to match. Empty means match all taint effects.
|
||||
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
|
||||
type: string
|
||||
key:
|
||||
description: |-
|
||||
Key is the taint key that the toleration applies to. Empty means match all taint keys.
|
||||
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
|
||||
type: string
|
||||
operator:
|
||||
description: |-
|
||||
Operator represents a key's relationship to the value.
|
||||
Valid operators are Exists and Equal. Defaults to Equal.
|
||||
Exists is equivalent to wildcard for value, so that a pod can
|
||||
tolerate all taints of a particular category.
|
||||
type: string
|
||||
tolerationSeconds:
|
||||
description: |-
|
||||
TolerationSeconds represents the period of time the toleration (which must be
|
||||
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
|
||||
it is not set, which means tolerate the taint forever (do not evict). Zero and
|
||||
negative values will be treated as 0 (evict immediately) by the system.
|
||||
format: int64
|
||||
type: integer
|
||||
value:
|
||||
description: |-
|
||||
Value is the taint value the toleration matches to.
|
||||
If the operator is Exists, the value should be empty, otherwise just a regular string.
|
||||
type: string
|
||||
type: object
|
||||
targetsScanned:
|
||||
description:
|
||||
TargetsScanned is the number of targets that were
|
||||
scanned
|
||||
type: integer
|
||||
totalFindings:
|
||||
description: TotalFindings is the total number of findings
|
||||
type: array
|
||||
type: object
|
||||
schedule:
|
||||
description: |-
|
||||
Schedule for periodic rescanning in cron format
|
||||
If empty, scan runs once
|
||||
type: string
|
||||
severity:
|
||||
description: Severity filters scan results by severity level
|
||||
enum:
|
||||
- info
|
||||
- low
|
||||
- medium
|
||||
- high
|
||||
- critical
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
sourceRef:
|
||||
description: SourceRef references the Ingress or VirtualService being
|
||||
scanned
|
||||
properties:
|
||||
apiVersion:
|
||||
description: APIVersion of the source resource
|
||||
type: string
|
||||
kind:
|
||||
description: Kind of the source resource - Ingress or VirtualService
|
||||
enum:
|
||||
- Ingress
|
||||
- VirtualService
|
||||
type: string
|
||||
name:
|
||||
description: Name of the source resource
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the source resource
|
||||
type: string
|
||||
uid:
|
||||
description: UID of the source resource for owner reference
|
||||
type: string
|
||||
required:
|
||||
- apiVersion
|
||||
- kind
|
||||
- name
|
||||
- namespace
|
||||
- uid
|
||||
type: object
|
||||
suspend:
|
||||
description: Suspend prevents scheduled scans from running
|
||||
type: boolean
|
||||
targets:
|
||||
description: Targets is the list of URLs to scan, extracted from the
|
||||
source resource
|
||||
items:
|
||||
type: string
|
||||
minItems: 1
|
||||
type: array
|
||||
templates:
|
||||
description: |-
|
||||
Templates specifies which Nuclei templates to use
|
||||
If empty, uses default templates
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- sourceRef
|
||||
- targets
|
||||
type: object
|
||||
status:
|
||||
description: NucleiScanStatus defines the observed state of NucleiScan
|
||||
properties:
|
||||
completionTime:
|
||||
description: CompletionTime is when the last scan completed
|
||||
format: date-time
|
||||
type: string
|
||||
conditions:
|
||||
description: Conditions represent the latest available observations
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- targetsScanned
|
||||
- totalFindings
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
findings:
|
||||
description: |-
|
||||
Findings contains the array of scan results from Nuclei JSONL output
|
||||
Each element is a parsed JSON object from Nuclei output
|
||||
items:
|
||||
description: Finding represents a single Nuclei scan finding
|
||||
properties:
|
||||
description:
|
||||
description: Description provides details about the finding
|
||||
type: string
|
||||
extractedResults:
|
||||
description: ExtractedResults contains any data extracted by
|
||||
the template
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
host:
|
||||
description: Host that was scanned
|
||||
type: string
|
||||
matchedAt:
|
||||
description: MatchedAt is the specific URL or endpoint where
|
||||
the issue was found
|
||||
type: string
|
||||
metadata:
|
||||
description: Metadata contains additional template metadata
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
reference:
|
||||
description: Reference contains URLs to additional information
|
||||
about the finding
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
severity:
|
||||
description: Severity of the finding
|
||||
type: string
|
||||
tags:
|
||||
description: Tags associated with the finding
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
templateId:
|
||||
description: TemplateID is the Nuclei template identifier
|
||||
type: string
|
||||
templateName:
|
||||
description: TemplateName is the human-readable template name
|
||||
type: string
|
||||
timestamp:
|
||||
description: Timestamp when the finding was discovered
|
||||
format: date-time
|
||||
type: string
|
||||
type:
|
||||
description: Type of the finding - http, dns, ssl, etc.
|
||||
type: string
|
||||
required:
|
||||
- host
|
||||
- severity
|
||||
- templateId
|
||||
- timestamp
|
||||
type: object
|
||||
type: array
|
||||
jobRef:
|
||||
description: JobRef references the current or last scanner job
|
||||
properties:
|
||||
name:
|
||||
description: Name of the Job
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the Job (may differ from NucleiScan
|
||||
namespace)
|
||||
type: string
|
||||
podName:
|
||||
description: PodName is the name of the scanner pod (for log retrieval)
|
||||
type: string
|
||||
startTime:
|
||||
description: StartTime when the job was created
|
||||
format: date-time
|
||||
type: string
|
||||
uid:
|
||||
description: UID of the Job
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
- uid
|
||||
type: object
|
||||
lastError:
|
||||
description: LastError contains the error message if the scan failed
|
||||
type: string
|
||||
lastRetryTime:
|
||||
description: LastRetryTime is when the last availability check retry
|
||||
occurred
|
||||
format: date-time
|
||||
type: string
|
||||
lastScanTime:
|
||||
description: LastScanTime is when the last scan was initiated
|
||||
format: date-time
|
||||
type: string
|
||||
nextScheduledTime:
|
||||
description: NextScheduledTime is when the next scheduled scan will
|
||||
run
|
||||
format: date-time
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: ObservedGeneration is the generation observed by the
|
||||
controller
|
||||
format: int64
|
||||
type: integer
|
||||
phase:
|
||||
description: Phase represents the current scan phase
|
||||
enum:
|
||||
- Pending
|
||||
- Running
|
||||
- Completed
|
||||
- Failed
|
||||
type: string
|
||||
retryCount:
|
||||
description: |-
|
||||
RetryCount tracks the number of consecutive availability check retries
|
||||
Used for exponential backoff when waiting for targets
|
||||
type: integer
|
||||
scanStartTime:
|
||||
description: ScanStartTime is when the scanner pod actually started
|
||||
scanning
|
||||
format: date-time
|
||||
type: string
|
||||
summary:
|
||||
description: Summary provides aggregated scan statistics
|
||||
properties:
|
||||
durationSeconds:
|
||||
description: DurationSeconds is the duration of the scan in seconds
|
||||
format: int64
|
||||
type: integer
|
||||
findingsBySeverity:
|
||||
additionalProperties:
|
||||
type: integer
|
||||
description: FindingsBySeverity breaks down findings by severity
|
||||
level
|
||||
type: object
|
||||
targetsScanned:
|
||||
description: TargetsScanned is the number of targets that were
|
||||
scanned
|
||||
type: integer
|
||||
totalFindings:
|
||||
description: TotalFindings is the total number of findings
|
||||
type: integer
|
||||
required:
|
||||
- targetsScanned
|
||||
- totalFindings
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
|
||||
@@ -70,6 +70,8 @@ spec:
|
||||
value: {{ .Values.scanner.ttlAfterFinished | quote }}
|
||||
- name: SCANNER_SERVICE_ACCOUNT
|
||||
value: {{ include "nuclei-operator.fullname" . }}-scanner
|
||||
- name: OPERATOR_NAMESPACE
|
||||
value: {{ .Release.Namespace | quote }}
|
||||
{{- if .Values.scanner.defaultTemplates }}
|
||||
- name: DEFAULT_TEMPLATES
|
||||
value: {{ join "," .Values.scanner.defaultTemplates | quote }}
|
||||
|
||||
11
cmd/main.go
11
cmd/main.go
@@ -241,6 +241,16 @@ func main() {
|
||||
scannerServiceAccount = "nuclei-scanner"
|
||||
}
|
||||
|
||||
operatorNamespace := os.Getenv("OPERATOR_NAMESPACE")
|
||||
if operatorNamespace == "" {
|
||||
// Try to read from the downward API file
|
||||
if data, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil {
|
||||
operatorNamespace = string(data)
|
||||
} else {
|
||||
operatorNamespace = "nuclei-operator-system"
|
||||
}
|
||||
}
|
||||
|
||||
defaultTemplates := []string{}
|
||||
if v := os.Getenv("DEFAULT_TEMPLATES"); v != "" {
|
||||
defaultTemplates = strings.Split(v, ",")
|
||||
@@ -259,6 +269,7 @@ func main() {
|
||||
BackoffLimit: 2,
|
||||
MaxConcurrent: maxConcurrentScans,
|
||||
ServiceAccountName: scannerServiceAccount,
|
||||
OperatorNamespace: operatorNamespace,
|
||||
DefaultResources: jobmanager.DefaultConfig().DefaultResources,
|
||||
DefaultTemplates: defaultTemplates,
|
||||
DefaultSeverity: defaultSeverity,
|
||||
|
||||
@@ -376,6 +376,10 @@ spec:
|
||||
name:
|
||||
description: Name of the Job
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the Job (may differ from NucleiScan
|
||||
namespace)
|
||||
type: string
|
||||
podName:
|
||||
description: PodName is the name of the scanner pod (for log retrieval)
|
||||
type: string
|
||||
@@ -388,6 +392,7 @@ spec:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
- uid
|
||||
type: object
|
||||
lastError:
|
||||
|
||||
@@ -257,8 +257,13 @@ func (r *NucleiScanReconciler) handleDeletion(ctx context.Context, nucleiScan *n
|
||||
|
||||
// Clean up any running scanner job
|
||||
if nucleiScan.Status.JobRef != nil {
|
||||
log.Info("Deleting scanner job", "job", nucleiScan.Status.JobRef.Name)
|
||||
if err := r.JobManager.DeleteJob(ctx, nucleiScan.Status.JobRef.Name, nucleiScan.Namespace); err != nil {
|
||||
jobNamespace := nucleiScan.Status.JobRef.Namespace
|
||||
if jobNamespace == "" {
|
||||
// Fallback for backwards compatibility
|
||||
jobNamespace = nucleiScan.Namespace
|
||||
}
|
||||
log.Info("Deleting scanner job", "job", nucleiScan.Status.JobRef.Name, "namespace", jobNamespace)
|
||||
if err := r.JobManager.DeleteJob(ctx, nucleiScan.Status.JobRef.Name, jobNamespace); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
log.Error(err, "Failed to delete scanner job", "job", nucleiScan.Status.JobRef.Name)
|
||||
}
|
||||
@@ -324,6 +329,7 @@ func (r *NucleiScanReconciler) handlePendingPhase(ctx context.Context, nucleiSca
|
||||
nucleiScan.Status.Phase = nucleiv1alpha1.ScanPhaseRunning
|
||||
nucleiScan.Status.JobRef = &nucleiv1alpha1.JobReference{
|
||||
Name: job.Name,
|
||||
Namespace: job.Namespace,
|
||||
UID: string(job.UID),
|
||||
StartTime: &now,
|
||||
}
|
||||
@@ -401,8 +407,13 @@ func (r *NucleiScanReconciler) handleRunningPhase(ctx context.Context, nucleiSca
|
||||
return ctrl.Result{Requeue: true}, nil
|
||||
}
|
||||
|
||||
// Get the job
|
||||
job, err := r.JobManager.GetJob(ctx, nucleiScan.Status.JobRef.Name, nucleiScan.Namespace)
|
||||
// Get the job - use namespace from JobRef (may be different from scan namespace)
|
||||
jobNamespace := nucleiScan.Status.JobRef.Namespace
|
||||
if jobNamespace == "" {
|
||||
// Fallback for backwards compatibility
|
||||
jobNamespace = nucleiScan.Namespace
|
||||
}
|
||||
job, err := r.JobManager.GetJob(ctx, nucleiScan.Status.JobRef.Name, jobNamespace)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
logger.Info("Scanner job not found, resetting to Pending")
|
||||
|
||||
@@ -82,6 +82,9 @@ type Config struct {
|
||||
// ServiceAccountName is the service account to use for scanner pods
|
||||
ServiceAccountName string
|
||||
|
||||
// OperatorNamespace is the namespace where the operator runs and where scanner jobs will be created
|
||||
OperatorNamespace string
|
||||
|
||||
// DefaultResources are the default resource requirements for scanner pods
|
||||
DefaultResources corev1.ResourceRequirements
|
||||
|
||||
@@ -101,6 +104,7 @@ func DefaultConfig() Config {
|
||||
BackoffLimit: DefaultBackoffLimit,
|
||||
MaxConcurrent: 5,
|
||||
ServiceAccountName: "nuclei-scanner",
|
||||
OperatorNamespace: "nuclei-operator-system",
|
||||
DefaultResources: corev1.ResourceRequirements{
|
||||
Requests: corev1.ResourceList{
|
||||
corev1.ResourceCPU: resource.MustParse("100m"),
|
||||
@@ -136,14 +140,22 @@ func (m *JobManager) CreateScanJob(ctx context.Context, scan *nucleiv1alpha1.Nuc
|
||||
|
||||
job := m.buildJob(scan)
|
||||
|
||||
// Set owner reference so the job is garbage collected when the scan is deleted
|
||||
if err := controllerutil.SetControllerReference(scan, job, m.Scheme); err != nil {
|
||||
return nil, fmt.Errorf("failed to set controller reference: %w", err)
|
||||
// Only set owner reference if the job is in the same namespace as the scan
|
||||
// Cross-namespace owner references are not allowed in Kubernetes
|
||||
if job.Namespace == scan.Namespace {
|
||||
if err := controllerutil.SetControllerReference(scan, job, m.Scheme); err != nil {
|
||||
return nil, fmt.Errorf("failed to set controller reference: %w", err)
|
||||
}
|
||||
}
|
||||
// When job is in a different namespace (operator namespace), we rely on:
|
||||
// 1. TTLSecondsAfterFinished for automatic cleanup of completed jobs
|
||||
// 2. Labels (LabelScanName, LabelScanNamespace) to track which scan the job belongs to
|
||||
// 3. CleanupOrphanedJobs to clean up jobs whose scans no longer exist
|
||||
|
||||
logger.Info("Creating scanner job",
|
||||
"job", job.Name,
|
||||
"namespace", job.Namespace,
|
||||
"jobNamespace", job.Namespace,
|
||||
"scanNamespace", scan.Namespace,
|
||||
"image", job.Spec.Template.Spec.Containers[0].Image,
|
||||
"targets", len(scan.Spec.Targets))
|
||||
|
||||
@@ -277,14 +289,41 @@ func (m *JobManager) CleanupOrphanedJobs(ctx context.Context) error {
|
||||
}
|
||||
|
||||
for _, job := range jobList.Items {
|
||||
// Check if owner reference exists and the owner still exists
|
||||
ownerRef := metav1.GetControllerOf(&job)
|
||||
if ownerRef == nil {
|
||||
logger.Info("Deleting orphaned job without owner", "job", job.Name, "namespace", job.Namespace)
|
||||
if err := m.DeleteJob(ctx, job.Name, job.Namespace); err != nil && !apierrors.IsNotFound(err) {
|
||||
logger.Error(err, "Failed to delete orphaned job", "job", job.Name)
|
||||
// Check if the associated NucleiScan still exists using labels
|
||||
scanName := job.Labels[LabelScanName]
|
||||
scanNamespace := job.Labels[LabelScanNamespace]
|
||||
|
||||
if scanName != "" && scanNamespace != "" {
|
||||
// Try to get the associated NucleiScan
|
||||
scan := &nucleiv1alpha1.NucleiScan{}
|
||||
err := m.Get(ctx, types.NamespacedName{Name: scanName, Namespace: scanNamespace}, scan)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// The scan no longer exists - delete the job
|
||||
logger.Info("Deleting orphaned job (scan not found)",
|
||||
"job", job.Name,
|
||||
"namespace", job.Namespace,
|
||||
"scanName", scanName,
|
||||
"scanNamespace", scanNamespace)
|
||||
if err := m.DeleteJob(ctx, job.Name, job.Namespace); err != nil && !apierrors.IsNotFound(err) {
|
||||
logger.Error(err, "Failed to delete orphaned job", "job", job.Name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Other error - log and continue
|
||||
logger.Error(err, "Failed to check if scan exists", "scanName", scanName, "scanNamespace", scanNamespace)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Job doesn't have proper labels - check owner reference as fallback
|
||||
ownerRef := metav1.GetControllerOf(&job)
|
||||
if ownerRef == nil {
|
||||
logger.Info("Deleting orphaned job without owner or labels", "job", job.Name, "namespace", job.Namespace)
|
||||
if err := m.DeleteJob(ctx, job.Name, job.Namespace); err != nil && !apierrors.IsNotFound(err) {
|
||||
logger.Error(err, "Failed to delete orphaned job", "job", job.Name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the job is stuck (running longer than 2x the timeout)
|
||||
@@ -305,12 +344,18 @@ func (m *JobManager) CleanupOrphanedJobs(ctx context.Context) error {
|
||||
|
||||
// buildJob creates a Job specification for the given NucleiScan
|
||||
func (m *JobManager) buildJob(scan *nucleiv1alpha1.NucleiScan) *batchv1.Job {
|
||||
// Generate a unique job name
|
||||
jobName := fmt.Sprintf("nucleiscan-%s-%d", scan.Name, time.Now().Unix())
|
||||
// Generate a unique job name that includes the scan namespace to avoid collisions
|
||||
jobName := fmt.Sprintf("nucleiscan-%s-%s-%d", scan.Namespace, scan.Name, time.Now().Unix())
|
||||
if len(jobName) > 63 {
|
||||
jobName = jobName[:63]
|
||||
}
|
||||
|
||||
// Determine the namespace for the job - use operator namespace if configured
|
||||
jobNamespace := m.Config.OperatorNamespace
|
||||
if jobNamespace == "" {
|
||||
jobNamespace = scan.Namespace
|
||||
}
|
||||
|
||||
// Determine the scanner image
|
||||
image := m.Config.ScannerImage
|
||||
if scan.Spec.ScannerConfig != nil && scan.Spec.ScannerConfig.Image != "" {
|
||||
@@ -360,7 +405,7 @@ func (m *JobManager) buildJob(scan *nucleiv1alpha1.NucleiScan) *batchv1.Job {
|
||||
job := &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jobName,
|
||||
Namespace: scan.Namespace,
|
||||
Namespace: jobNamespace,
|
||||
Labels: labels,
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
|
||||
@@ -43,14 +43,22 @@ func TestBuildJob(t *testing.T) {
|
||||
|
||||
job := manager.buildJob(scan)
|
||||
|
||||
// Verify job name prefix
|
||||
// Verify job name prefix - should include scan namespace to avoid collisions
|
||||
if len(job.Name) == 0 {
|
||||
t.Error("Job name should not be empty")
|
||||
}
|
||||
|
||||
// Verify namespace
|
||||
if job.Namespace != "default" {
|
||||
t.Errorf("Expected namespace 'default', got '%s'", job.Namespace)
|
||||
// Verify namespace - job should be created in operator namespace
|
||||
if job.Namespace != config.OperatorNamespace {
|
||||
t.Errorf("Expected namespace '%s', got '%s'", config.OperatorNamespace, job.Namespace)
|
||||
}
|
||||
|
||||
// Verify scan labels are set correctly for cross-namespace tracking
|
||||
if job.Labels[LabelScanName] != scan.Name {
|
||||
t.Errorf("Expected scan name label '%s', got '%s'", scan.Name, job.Labels[LabelScanName])
|
||||
}
|
||||
if job.Labels[LabelScanNamespace] != scan.Namespace {
|
||||
t.Errorf("Expected scan namespace label '%s', got '%s'", scan.Namespace, job.Labels[LabelScanNamespace])
|
||||
}
|
||||
|
||||
// Verify labels
|
||||
@@ -115,3 +123,29 @@ func TestBuildJobWithCustomConfig(t *testing.T) {
|
||||
t.Errorf("Expected deadline %d, got %d", expectedDeadline, *job.Spec.ActiveDeadlineSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildJobInSameNamespace(t *testing.T) {
|
||||
config := DefaultConfig()
|
||||
// Clear operator namespace to test same-namespace behavior
|
||||
config.OperatorNamespace = ""
|
||||
manager := &JobManager{
|
||||
Config: config,
|
||||
}
|
||||
|
||||
scan := &nucleiv1alpha1.NucleiScan{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-scan",
|
||||
Namespace: "my-namespace",
|
||||
},
|
||||
Spec: nucleiv1alpha1.NucleiScanSpec{
|
||||
Targets: []string{"https://example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
job := manager.buildJob(scan)
|
||||
|
||||
// Verify namespace - when operator namespace is empty, job should be in scan's namespace
|
||||
if job.Namespace != scan.Namespace {
|
||||
t.Errorf("Expected namespace '%s', got '%s'", scan.Namespace, job.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,8 +118,11 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
|
||||
logger.Info("Starting scan",
|
||||
"targets", len(scan.Spec.Targets),
|
||||
"targetList", scan.Spec.Targets,
|
||||
"templates", scan.Spec.Templates,
|
||||
"severity", scan.Spec.Severity)
|
||||
"templatesCount", len(scan.Spec.Templates),
|
||||
"severity", scan.Spec.Severity,
|
||||
"severityCount", len(scan.Spec.Severity))
|
||||
|
||||
// Update status to indicate scan has started
|
||||
startTime := metav1.Now()
|
||||
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
nucleiv1alpha1 "github.com/mortenolsen/nuclei-operator/api/v1alpha1"
|
||||
)
|
||||
|
||||
@@ -95,6 +97,8 @@ func NewNucleiScannerWithDefaults() *NucleiScanner {
|
||||
|
||||
// Scan executes a Nuclei scan against the given targets
|
||||
func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options ScanOptions) (*ScanResult, error) {
|
||||
logger := log.FromContext(ctx).WithName("nuclei-scanner")
|
||||
|
||||
if len(targets) == 0 {
|
||||
return nil, fmt.Errorf("no targets provided for scan")
|
||||
}
|
||||
@@ -110,10 +114,66 @@ func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options Scan
|
||||
|
||||
// Write targets to a file
|
||||
targetsFile := filepath.Join(tmpDir, "targets.txt")
|
||||
if err := os.WriteFile(targetsFile, []byte(strings.Join(targets, "\n")), 0600); err != nil {
|
||||
targetsContent := strings.Join(targets, "\n")
|
||||
if err := os.WriteFile(targetsFile, []byte(targetsContent), 0600); err != nil {
|
||||
return nil, fmt.Errorf("failed to write targets file: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Targets file created", "targetsFile", targetsFile, "targetCount", len(targets))
|
||||
|
||||
// Check if nuclei binary exists and is executable
|
||||
// First try the exact path
|
||||
if _, err := os.Stat(s.nucleiBinaryPath); os.IsNotExist(err) {
|
||||
// If not found at exact path, try to find it in PATH
|
||||
if path, err := exec.LookPath(s.nucleiBinaryPath); err == nil {
|
||||
logger.Info("Found nuclei binary in PATH", "path", path, "originalPath", s.nucleiBinaryPath)
|
||||
s.nucleiBinaryPath = path
|
||||
} else {
|
||||
return nil, fmt.Errorf("nuclei binary not found at %s and not in PATH: %w", s.nucleiBinaryPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify nuclei is executable by running version command
|
||||
if err := exec.Command(s.nucleiBinaryPath, "-version").Run(); err != nil {
|
||||
logger.Error(err, "Failed to execute nuclei -version, nuclei may not be properly installed", "path", s.nucleiBinaryPath)
|
||||
// Don't fail here, just log - the actual scan will fail if nuclei is truly broken
|
||||
} else {
|
||||
logger.Info("Nuclei binary verified", "path", s.nucleiBinaryPath)
|
||||
}
|
||||
|
||||
// Check templates availability if templates path is set
|
||||
templatesAvailable := false
|
||||
if s.templatesPath != "" {
|
||||
if info, err := os.Stat(s.templatesPath); err != nil || !info.IsDir() {
|
||||
logger.Info("Templates path does not exist or is not a directory, nuclei will use default templates",
|
||||
"templatesPath", s.templatesPath,
|
||||
"error", err)
|
||||
} else {
|
||||
// Count template files
|
||||
entries, err := os.ReadDir(s.templatesPath)
|
||||
if err == nil {
|
||||
templateCount := 0
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && (strings.HasSuffix(entry.Name(), ".yaml") || strings.HasSuffix(entry.Name(), ".yml")) {
|
||||
templateCount++
|
||||
}
|
||||
}
|
||||
templatesAvailable = templateCount > 0
|
||||
logger.Info("Templates directory found", "templatesPath", s.templatesPath, "templateCount", templateCount)
|
||||
if templateCount == 0 {
|
||||
logger.Info("Templates directory is empty, nuclei will download templates on first run or use default location")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.Info("No templates path configured, nuclei will use default template location (~/.nuclei/templates)")
|
||||
}
|
||||
|
||||
// If no specific templates are provided and templates path is empty, warn
|
||||
if len(options.Templates) == 0 && !templatesAvailable && s.templatesPath != "" {
|
||||
logger.Info("Warning: No templates specified and templates directory appears empty. Nuclei may not run any scans.")
|
||||
}
|
||||
|
||||
// Build the nuclei command arguments
|
||||
args := s.buildArgs(targetsFile, options)
|
||||
|
||||
@@ -123,6 +183,16 @@ func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options Scan
|
||||
timeout = 30 * time.Minute
|
||||
}
|
||||
|
||||
// Log the command being executed
|
||||
fullCommand := fmt.Sprintf("%s %s", s.nucleiBinaryPath, strings.Join(args, " "))
|
||||
logger.Info("Executing nuclei scan",
|
||||
"command", fullCommand,
|
||||
"timeout", timeout,
|
||||
"templates", len(options.Templates),
|
||||
"templatesList", options.Templates,
|
||||
"severity", options.Severity,
|
||||
"templatesPath", s.templatesPath)
|
||||
|
||||
// Create context with timeout
|
||||
scanCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
@@ -134,14 +204,31 @@ func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options Scan
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
logger.Info("Starting nuclei execution")
|
||||
err = cmd.Run()
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// Log stderr output (nuclei often outputs warnings/info to stderr)
|
||||
stderrStr := stderr.String()
|
||||
if stderrStr != "" {
|
||||
logger.Info("Nuclei stderr output", "stderr", stderrStr)
|
||||
}
|
||||
|
||||
// Log stdout size for debugging
|
||||
stdoutSize := len(stdout.Bytes())
|
||||
logger.Info("Nuclei execution completed",
|
||||
"duration", duration,
|
||||
"exitCode", cmd.ProcessState.ExitCode(),
|
||||
"stdoutSize", stdoutSize,
|
||||
"stderrSize", len(stderrStr))
|
||||
|
||||
// Check for context cancellation
|
||||
if scanCtx.Err() == context.DeadlineExceeded {
|
||||
logger.Error(nil, "Scan timed out", "timeout", timeout, "stderr", stderrStr)
|
||||
return nil, fmt.Errorf("scan timed out after %v", timeout)
|
||||
}
|
||||
if scanCtx.Err() == context.Canceled {
|
||||
logger.Error(nil, "Scan was cancelled", "stderr", stderrStr)
|
||||
return nil, fmt.Errorf("scan was cancelled")
|
||||
}
|
||||
|
||||
@@ -151,22 +238,40 @@ func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options Scan
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
// Exit code 1 can mean "no results found" which is not an error
|
||||
if exitErr.ExitCode() != 1 {
|
||||
return nil, fmt.Errorf("nuclei execution failed: %w, stderr: %s", err, stderr.String())
|
||||
logger.Error(err, "Nuclei execution failed",
|
||||
"exitCode", exitErr.ExitCode(),
|
||||
"stderr", stderrStr,
|
||||
"stdout", stdout.String())
|
||||
return nil, fmt.Errorf("nuclei execution failed: %w, stderr: %s", err, stderrStr)
|
||||
}
|
||||
logger.Info("Nuclei exited with code 1 (no results found)", "stderr", stderrStr)
|
||||
} else {
|
||||
logger.Error(err, "Failed to execute nuclei", "stderr", stderrStr)
|
||||
return nil, fmt.Errorf("failed to execute nuclei: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the JSONL output
|
||||
findings, err := ParseJSONLOutput(stdout.Bytes())
|
||||
stdoutBytes := stdout.Bytes()
|
||||
logger.Info("Parsing nuclei output", "outputSize", len(stdoutBytes))
|
||||
findings, err := ParseJSONLOutput(stdoutBytes)
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to parse nuclei output",
|
||||
"stdout", string(stdoutBytes),
|
||||
"stderr", stderrStr)
|
||||
return nil, fmt.Errorf("failed to parse nuclei output: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Parsed findings", "count", len(findings))
|
||||
|
||||
// Calculate summary
|
||||
summary := calculateSummary(findings, len(targets), duration)
|
||||
|
||||
logger.Info("Scan completed",
|
||||
"findings", len(findings),
|
||||
"duration", duration,
|
||||
"targetsScanned", len(targets))
|
||||
|
||||
return &ScanResult{
|
||||
Findings: findings,
|
||||
Summary: summary,
|
||||
@@ -181,11 +286,8 @@ func (s *NucleiScanner) buildArgs(targetsFile string, options ScanOptions) []str
|
||||
"-jsonl",
|
||||
"-silent",
|
||||
"-no-color",
|
||||
}
|
||||
|
||||
// Add templates path if configured
|
||||
if s.templatesPath != "" {
|
||||
args = append(args, "-t", s.templatesPath)
|
||||
"-rate-limit", "150", // Limit rate to avoid overwhelming targets
|
||||
"-bulk-size", "25", // Process targets in bulk
|
||||
}
|
||||
|
||||
// Add specific templates if provided
|
||||
@@ -193,6 +295,31 @@ func (s *NucleiScanner) buildArgs(targetsFile string, options ScanOptions) []str
|
||||
for _, t := range options.Templates {
|
||||
args = append(args, "-t", t)
|
||||
}
|
||||
} else {
|
||||
// When no templates are specified, nuclei should use all available templates
|
||||
// Only add templates path if it's configured AND contains templates
|
||||
// Otherwise, let nuclei use its default template location (~/.nuclei/templates)
|
||||
if s.templatesPath != "" {
|
||||
// Check if templates directory exists and has content
|
||||
if info, err := os.Stat(s.templatesPath); err == nil && info.IsDir() {
|
||||
entries, err := os.ReadDir(s.templatesPath)
|
||||
if err == nil {
|
||||
hasTemplates := false
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && (strings.HasSuffix(entry.Name(), ".yaml") || strings.HasSuffix(entry.Name(), ".yml")) {
|
||||
hasTemplates = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasTemplates {
|
||||
args = append(args, "-t", s.templatesPath)
|
||||
}
|
||||
// If no templates found, don't add -t flag, let nuclei use default location
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no templates path or it's empty, nuclei will use default location
|
||||
// which it will download templates to on first run if needed
|
||||
}
|
||||
|
||||
// Add severity filter if provided
|
||||
|
||||
Reference in New Issue
Block a user