mirror of
https://github.com/morten-olsen/homelab-nuclei-operator.git
synced 2026-02-08 02:16:23 +01:00
Compare commits
1 Commits
v0.2.5
...
use-prebui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c3f7f3869 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -87,7 +87,7 @@ jobs:
|
|||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
# platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ FROM alpine:3.19 AS final
|
|||||||
# Build arguments for nuclei version and architecture
|
# Build arguments for nuclei version and architecture
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG NUCLEI_VERSION=3.6.0
|
ARG NUCLEI_VERSION=3.3.7
|
||||||
|
|
||||||
# Install ca-certificates for HTTPS requests, curl for downloading, and create non-root user
|
# Install ca-certificates for HTTPS requests, curl for downloading, and create non-root user
|
||||||
RUN apk --no-cache add ca-certificates tzdata curl unzip && \
|
RUN apk --no-cache add ca-certificates tzdata curl unzip && \
|
||||||
|
|||||||
@@ -77,9 +77,6 @@ type JobReference struct {
|
|||||||
// Name of the Job
|
// Name of the Job
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
||||||
// Namespace of the Job (may differ from NucleiScan namespace)
|
|
||||||
Namespace string `json:"namespace"`
|
|
||||||
|
|
||||||
// UID of the Job
|
// UID of the Job
|
||||||
UID string `json:"uid"`
|
UID string `json:"uid"`
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ metadata:
|
|||||||
annotations:
|
annotations:
|
||||||
controller-gen.kubebuilder.io/version: v0.19.0
|
controller-gen.kubebuilder.io/version: v0.19.0
|
||||||
name: nucleiscans.nuclei.homelab.mortenolsen.pro
|
name: nucleiscans.nuclei.homelab.mortenolsen.pro
|
||||||
|
labels:
|
||||||
|
{{- include "nuclei-operator.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
group: nuclei.homelab.mortenolsen.pro
|
group: nuclei.homelab.mortenolsen.pro
|
||||||
names:
|
names:
|
||||||
@@ -55,127 +57,6 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
description: NucleiScanSpec defines the desired state of NucleiScan
|
description: NucleiScanSpec defines the desired state of NucleiScan
|
||||||
properties:
|
properties:
|
||||||
scannerConfig:
|
|
||||||
description: ScannerConfig allows overriding scanner settings for
|
|
||||||
this scan
|
|
||||||
properties:
|
|
||||||
image:
|
|
||||||
description: Image overrides the default scanner image
|
|
||||||
type: string
|
|
||||||
nodeSelector:
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
description: NodeSelector for scanner pod scheduling
|
|
||||||
type: object
|
|
||||||
resources:
|
|
||||||
description: Resources defines resource requirements for the scanner
|
|
||||||
pod
|
|
||||||
properties:
|
|
||||||
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:
|
|
||||||
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
|
|
||||||
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
|
|
||||||
type: object
|
|
||||||
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
|
|
||||||
type: array
|
|
||||||
type: object
|
|
||||||
schedule:
|
schedule:
|
||||||
description: |-
|
description: |-
|
||||||
Schedule for periodic rescanning in cron format
|
Schedule for periodic rescanning in cron format
|
||||||
@@ -183,17 +64,12 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
severity:
|
severity:
|
||||||
description: Severity filters scan results by severity level
|
description: Severity filters scan results by severity level
|
||||||
enum:
|
|
||||||
- info
|
|
||||||
- low
|
|
||||||
- medium
|
|
||||||
- high
|
|
||||||
- critical
|
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
sourceRef:
|
sourceRef:
|
||||||
description: SourceRef references the Ingress or VirtualService being
|
description:
|
||||||
|
SourceRef references the Ingress or VirtualService being
|
||||||
scanned
|
scanned
|
||||||
properties:
|
properties:
|
||||||
apiVersion:
|
apiVersion:
|
||||||
@@ -225,7 +101,8 @@ spec:
|
|||||||
description: Suspend prevents scheduled scans from running
|
description: Suspend prevents scheduled scans from running
|
||||||
type: boolean
|
type: boolean
|
||||||
targets:
|
targets:
|
||||||
description: Targets is the list of URLs to scan, extracted from the
|
description:
|
||||||
|
Targets is the list of URLs to scan, extracted from the
|
||||||
source resource
|
source resource
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
@@ -252,7 +129,8 @@ spec:
|
|||||||
conditions:
|
conditions:
|
||||||
description: Conditions represent the latest available observations
|
description: Conditions represent the latest available observations
|
||||||
items:
|
items:
|
||||||
description: Condition contains details for one aspect of the current
|
description:
|
||||||
|
Condition contains details for one aspect of the current
|
||||||
state of this API Resource.
|
state of this API Resource.
|
||||||
properties:
|
properties:
|
||||||
lastTransitionTime:
|
lastTransitionTime:
|
||||||
@@ -320,7 +198,8 @@ spec:
|
|||||||
description: Description provides details about the finding
|
description: Description provides details about the finding
|
||||||
type: string
|
type: string
|
||||||
extractedResults:
|
extractedResults:
|
||||||
description: ExtractedResults contains any data extracted by
|
description:
|
||||||
|
ExtractedResults contains any data extracted by
|
||||||
the template
|
the template
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
@@ -329,7 +208,8 @@ spec:
|
|||||||
description: Host that was scanned
|
description: Host that was scanned
|
||||||
type: string
|
type: string
|
||||||
matchedAt:
|
matchedAt:
|
||||||
description: MatchedAt is the specific URL or endpoint where
|
description:
|
||||||
|
MatchedAt is the specific URL or endpoint where
|
||||||
the issue was found
|
the issue was found
|
||||||
type: string
|
type: string
|
||||||
metadata:
|
metadata:
|
||||||
@@ -337,7 +217,8 @@ spec:
|
|||||||
type: object
|
type: object
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
reference:
|
reference:
|
||||||
description: Reference contains URLs to additional information
|
description:
|
||||||
|
Reference contains URLs to additional information
|
||||||
about the finding
|
about the finding
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
@@ -370,36 +251,12 @@ spec:
|
|||||||
- timestamp
|
- timestamp
|
||||||
type: object
|
type: object
|
||||||
type: array
|
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:
|
lastError:
|
||||||
description: LastError contains the error message if the scan failed
|
description: LastError contains the error message if the scan failed
|
||||||
type: string
|
type: string
|
||||||
lastRetryTime:
|
lastRetryTime:
|
||||||
description: LastRetryTime is when the last availability check retry
|
description:
|
||||||
|
LastRetryTime is when the last availability check retry
|
||||||
occurred
|
occurred
|
||||||
format: date-time
|
format: date-time
|
||||||
type: string
|
type: string
|
||||||
@@ -408,12 +265,14 @@ spec:
|
|||||||
format: date-time
|
format: date-time
|
||||||
type: string
|
type: string
|
||||||
nextScheduledTime:
|
nextScheduledTime:
|
||||||
description: NextScheduledTime is when the next scheduled scan will
|
description:
|
||||||
|
NextScheduledTime is when the next scheduled scan will
|
||||||
run
|
run
|
||||||
format: date-time
|
format: date-time
|
||||||
type: string
|
type: string
|
||||||
observedGeneration:
|
observedGeneration:
|
||||||
description: ObservedGeneration is the generation observed by the
|
description:
|
||||||
|
ObservedGeneration is the generation observed by the
|
||||||
controller
|
controller
|
||||||
format: int64
|
format: int64
|
||||||
type: integer
|
type: integer
|
||||||
@@ -430,11 +289,6 @@ spec:
|
|||||||
RetryCount tracks the number of consecutive availability check retries
|
RetryCount tracks the number of consecutive availability check retries
|
||||||
Used for exponential backoff when waiting for targets
|
Used for exponential backoff when waiting for targets
|
||||||
type: integer
|
type: integer
|
||||||
scanStartTime:
|
|
||||||
description: ScanStartTime is when the scanner pod actually started
|
|
||||||
scanning
|
|
||||||
format: date-time
|
|
||||||
type: string
|
|
||||||
summary:
|
summary:
|
||||||
description: Summary provides aggregated scan statistics
|
description: Summary provides aggregated scan statistics
|
||||||
properties:
|
properties:
|
||||||
@@ -445,11 +299,13 @@ spec:
|
|||||||
findingsBySeverity:
|
findingsBySeverity:
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: integer
|
type: integer
|
||||||
description: FindingsBySeverity breaks down findings by severity
|
description:
|
||||||
|
FindingsBySeverity breaks down findings by severity
|
||||||
level
|
level
|
||||||
type: object
|
type: object
|
||||||
targetsScanned:
|
targetsScanned:
|
||||||
description: TargetsScanned is the number of targets that were
|
description:
|
||||||
|
TargetsScanned is the number of targets that were
|
||||||
scanned
|
scanned
|
||||||
type: integer
|
type: integer
|
||||||
totalFindings:
|
totalFindings:
|
||||||
|
|||||||
@@ -70,8 +70,6 @@ spec:
|
|||||||
value: {{ .Values.scanner.ttlAfterFinished | quote }}
|
value: {{ .Values.scanner.ttlAfterFinished | quote }}
|
||||||
- name: SCANNER_SERVICE_ACCOUNT
|
- name: SCANNER_SERVICE_ACCOUNT
|
||||||
value: {{ include "nuclei-operator.fullname" . }}-scanner
|
value: {{ include "nuclei-operator.fullname" . }}-scanner
|
||||||
- name: OPERATOR_NAMESPACE
|
|
||||||
value: {{ .Release.Namespace | quote }}
|
|
||||||
{{- if .Values.scanner.defaultTemplates }}
|
{{- if .Values.scanner.defaultTemplates }}
|
||||||
- name: DEFAULT_TEMPLATES
|
- name: DEFAULT_TEMPLATES
|
||||||
value: {{ join "," .Values.scanner.defaultTemplates | quote }}
|
value: {{ join "," .Values.scanner.defaultTemplates | quote }}
|
||||||
|
|||||||
11
cmd/main.go
11
cmd/main.go
@@ -241,16 +241,6 @@ func main() {
|
|||||||
scannerServiceAccount = "nuclei-scanner"
|
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{}
|
defaultTemplates := []string{}
|
||||||
if v := os.Getenv("DEFAULT_TEMPLATES"); v != "" {
|
if v := os.Getenv("DEFAULT_TEMPLATES"); v != "" {
|
||||||
defaultTemplates = strings.Split(v, ",")
|
defaultTemplates = strings.Split(v, ",")
|
||||||
@@ -269,7 +259,6 @@ func main() {
|
|||||||
BackoffLimit: 2,
|
BackoffLimit: 2,
|
||||||
MaxConcurrent: maxConcurrentScans,
|
MaxConcurrent: maxConcurrentScans,
|
||||||
ServiceAccountName: scannerServiceAccount,
|
ServiceAccountName: scannerServiceAccount,
|
||||||
OperatorNamespace: operatorNamespace,
|
|
||||||
DefaultResources: jobmanager.DefaultConfig().DefaultResources,
|
DefaultResources: jobmanager.DefaultConfig().DefaultResources,
|
||||||
DefaultTemplates: defaultTemplates,
|
DefaultTemplates: defaultTemplates,
|
||||||
DefaultSeverity: defaultSeverity,
|
DefaultSeverity: defaultSeverity,
|
||||||
|
|||||||
@@ -376,10 +376,6 @@ spec:
|
|||||||
name:
|
name:
|
||||||
description: Name of the Job
|
description: Name of the Job
|
||||||
type: string
|
type: string
|
||||||
namespace:
|
|
||||||
description: Namespace of the Job (may differ from NucleiScan
|
|
||||||
namespace)
|
|
||||||
type: string
|
|
||||||
podName:
|
podName:
|
||||||
description: PodName is the name of the scanner pod (for log retrieval)
|
description: PodName is the name of the scanner pod (for log retrieval)
|
||||||
type: string
|
type: string
|
||||||
@@ -392,7 +388,6 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- namespace
|
|
||||||
- uid
|
- uid
|
||||||
type: object
|
type: object
|
||||||
lastError:
|
lastError:
|
||||||
|
|||||||
@@ -257,13 +257,8 @@ func (r *NucleiScanReconciler) handleDeletion(ctx context.Context, nucleiScan *n
|
|||||||
|
|
||||||
// Clean up any running scanner job
|
// Clean up any running scanner job
|
||||||
if nucleiScan.Status.JobRef != nil {
|
if nucleiScan.Status.JobRef != nil {
|
||||||
jobNamespace := nucleiScan.Status.JobRef.Namespace
|
log.Info("Deleting scanner job", "job", nucleiScan.Status.JobRef.Name)
|
||||||
if jobNamespace == "" {
|
if err := r.JobManager.DeleteJob(ctx, nucleiScan.Status.JobRef.Name, nucleiScan.Namespace); err != nil {
|
||||||
// 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) {
|
if !apierrors.IsNotFound(err) {
|
||||||
log.Error(err, "Failed to delete scanner job", "job", nucleiScan.Status.JobRef.Name)
|
log.Error(err, "Failed to delete scanner job", "job", nucleiScan.Status.JobRef.Name)
|
||||||
}
|
}
|
||||||
@@ -329,7 +324,6 @@ func (r *NucleiScanReconciler) handlePendingPhase(ctx context.Context, nucleiSca
|
|||||||
nucleiScan.Status.Phase = nucleiv1alpha1.ScanPhaseRunning
|
nucleiScan.Status.Phase = nucleiv1alpha1.ScanPhaseRunning
|
||||||
nucleiScan.Status.JobRef = &nucleiv1alpha1.JobReference{
|
nucleiScan.Status.JobRef = &nucleiv1alpha1.JobReference{
|
||||||
Name: job.Name,
|
Name: job.Name,
|
||||||
Namespace: job.Namespace,
|
|
||||||
UID: string(job.UID),
|
UID: string(job.UID),
|
||||||
StartTime: &now,
|
StartTime: &now,
|
||||||
}
|
}
|
||||||
@@ -407,13 +401,8 @@ func (r *NucleiScanReconciler) handleRunningPhase(ctx context.Context, nucleiSca
|
|||||||
return ctrl.Result{Requeue: true}, nil
|
return ctrl.Result{Requeue: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the job - use namespace from JobRef (may be different from scan namespace)
|
// Get the job
|
||||||
jobNamespace := nucleiScan.Status.JobRef.Namespace
|
job, err := r.JobManager.GetJob(ctx, nucleiScan.Status.JobRef.Name, nucleiScan.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 err != nil {
|
||||||
if apierrors.IsNotFound(err) {
|
if apierrors.IsNotFound(err) {
|
||||||
logger.Info("Scanner job not found, resetting to Pending")
|
logger.Info("Scanner job not found, resetting to Pending")
|
||||||
|
|||||||
@@ -82,9 +82,6 @@ type Config struct {
|
|||||||
// ServiceAccountName is the service account to use for scanner pods
|
// ServiceAccountName is the service account to use for scanner pods
|
||||||
ServiceAccountName string
|
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 are the default resource requirements for scanner pods
|
||||||
DefaultResources corev1.ResourceRequirements
|
DefaultResources corev1.ResourceRequirements
|
||||||
|
|
||||||
@@ -104,7 +101,6 @@ func DefaultConfig() Config {
|
|||||||
BackoffLimit: DefaultBackoffLimit,
|
BackoffLimit: DefaultBackoffLimit,
|
||||||
MaxConcurrent: 5,
|
MaxConcurrent: 5,
|
||||||
ServiceAccountName: "nuclei-scanner",
|
ServiceAccountName: "nuclei-scanner",
|
||||||
OperatorNamespace: "nuclei-operator-system",
|
|
||||||
DefaultResources: corev1.ResourceRequirements{
|
DefaultResources: corev1.ResourceRequirements{
|
||||||
Requests: corev1.ResourceList{
|
Requests: corev1.ResourceList{
|
||||||
corev1.ResourceCPU: resource.MustParse("100m"),
|
corev1.ResourceCPU: resource.MustParse("100m"),
|
||||||
@@ -140,22 +136,14 @@ func (m *JobManager) CreateScanJob(ctx context.Context, scan *nucleiv1alpha1.Nuc
|
|||||||
|
|
||||||
job := m.buildJob(scan)
|
job := m.buildJob(scan)
|
||||||
|
|
||||||
// Only set owner reference if the job is in the same namespace as the scan
|
// Set owner reference so the job is garbage collected when the scan is deleted
|
||||||
// Cross-namespace owner references are not allowed in Kubernetes
|
|
||||||
if job.Namespace == scan.Namespace {
|
|
||||||
if err := controllerutil.SetControllerReference(scan, job, m.Scheme); err != nil {
|
if err := controllerutil.SetControllerReference(scan, job, m.Scheme); err != nil {
|
||||||
return nil, fmt.Errorf("failed to set controller reference: %w", err)
|
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",
|
logger.Info("Creating scanner job",
|
||||||
"job", job.Name,
|
"job", job.Name,
|
||||||
"jobNamespace", job.Namespace,
|
"namespace", job.Namespace,
|
||||||
"scanNamespace", scan.Namespace,
|
|
||||||
"image", job.Spec.Template.Spec.Containers[0].Image,
|
"image", job.Spec.Template.Spec.Containers[0].Image,
|
||||||
"targets", len(scan.Spec.Targets))
|
"targets", len(scan.Spec.Targets))
|
||||||
|
|
||||||
@@ -289,42 +277,15 @@ func (m *JobManager) CleanupOrphanedJobs(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, job := range jobList.Items {
|
for _, job := range jobList.Items {
|
||||||
// Check if the associated NucleiScan still exists using labels
|
// Check if owner reference exists and the owner still exists
|
||||||
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)
|
ownerRef := metav1.GetControllerOf(&job)
|
||||||
if ownerRef == nil {
|
if ownerRef == nil {
|
||||||
logger.Info("Deleting orphaned job without owner or labels", "job", job.Name, "namespace", job.Namespace)
|
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) {
|
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)
|
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)
|
// Check if the job is stuck (running longer than 2x the timeout)
|
||||||
if job.Status.StartTime != nil {
|
if job.Status.StartTime != nil {
|
||||||
@@ -344,18 +305,12 @@ func (m *JobManager) CleanupOrphanedJobs(ctx context.Context) error {
|
|||||||
|
|
||||||
// buildJob creates a Job specification for the given NucleiScan
|
// buildJob creates a Job specification for the given NucleiScan
|
||||||
func (m *JobManager) buildJob(scan *nucleiv1alpha1.NucleiScan) *batchv1.Job {
|
func (m *JobManager) buildJob(scan *nucleiv1alpha1.NucleiScan) *batchv1.Job {
|
||||||
// Generate a unique job name that includes the scan namespace to avoid collisions
|
// Generate a unique job name
|
||||||
jobName := fmt.Sprintf("nucleiscan-%s-%s-%d", scan.Namespace, scan.Name, time.Now().Unix())
|
jobName := fmt.Sprintf("nucleiscan-%s-%d", scan.Name, time.Now().Unix())
|
||||||
if len(jobName) > 63 {
|
if len(jobName) > 63 {
|
||||||
jobName = 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
|
// Determine the scanner image
|
||||||
image := m.Config.ScannerImage
|
image := m.Config.ScannerImage
|
||||||
if scan.Spec.ScannerConfig != nil && scan.Spec.ScannerConfig.Image != "" {
|
if scan.Spec.ScannerConfig != nil && scan.Spec.ScannerConfig.Image != "" {
|
||||||
@@ -405,7 +360,7 @@ func (m *JobManager) buildJob(scan *nucleiv1alpha1.NucleiScan) *batchv1.Job {
|
|||||||
job := &batchv1.Job{
|
job := &batchv1.Job{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: jobName,
|
Name: jobName,
|
||||||
Namespace: jobNamespace,
|
Namespace: scan.Namespace,
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
},
|
},
|
||||||
Spec: batchv1.JobSpec{
|
Spec: batchv1.JobSpec{
|
||||||
|
|||||||
@@ -43,22 +43,14 @@ func TestBuildJob(t *testing.T) {
|
|||||||
|
|
||||||
job := manager.buildJob(scan)
|
job := manager.buildJob(scan)
|
||||||
|
|
||||||
// Verify job name prefix - should include scan namespace to avoid collisions
|
// Verify job name prefix
|
||||||
if len(job.Name) == 0 {
|
if len(job.Name) == 0 {
|
||||||
t.Error("Job name should not be empty")
|
t.Error("Job name should not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify namespace - job should be created in operator namespace
|
// Verify namespace
|
||||||
if job.Namespace != config.OperatorNamespace {
|
if job.Namespace != "default" {
|
||||||
t.Errorf("Expected namespace '%s', got '%s'", config.OperatorNamespace, job.Namespace)
|
t.Errorf("Expected namespace 'default', got '%s'", 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
|
// Verify labels
|
||||||
@@ -123,29 +115,3 @@ func TestBuildJobWithCustomConfig(t *testing.T) {
|
|||||||
t.Errorf("Expected deadline %d, got %d", expectedDeadline, *job.Spec.ActiveDeadlineSeconds)
|
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,11 +118,8 @@ func (r *Runner) Run(ctx context.Context) error {
|
|||||||
|
|
||||||
logger.Info("Starting scan",
|
logger.Info("Starting scan",
|
||||||
"targets", len(scan.Spec.Targets),
|
"targets", len(scan.Spec.Targets),
|
||||||
"targetList", scan.Spec.Targets,
|
|
||||||
"templates", scan.Spec.Templates,
|
"templates", scan.Spec.Templates,
|
||||||
"templatesCount", len(scan.Spec.Templates),
|
"severity", scan.Spec.Severity)
|
||||||
"severity", scan.Spec.Severity,
|
|
||||||
"severityCount", len(scan.Spec.Severity))
|
|
||||||
|
|
||||||
// Update status to indicate scan has started
|
// Update status to indicate scan has started
|
||||||
startTime := metav1.Now()
|
startTime := metav1.Now()
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
|
||||||
|
|
||||||
nucleiv1alpha1 "github.com/mortenolsen/nuclei-operator/api/v1alpha1"
|
nucleiv1alpha1 "github.com/mortenolsen/nuclei-operator/api/v1alpha1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -97,8 +95,6 @@ func NewNucleiScannerWithDefaults() *NucleiScanner {
|
|||||||
|
|
||||||
// Scan executes a Nuclei scan against the given targets
|
// Scan executes a Nuclei scan against the given targets
|
||||||
func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options ScanOptions) (*ScanResult, error) {
|
func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options ScanOptions) (*ScanResult, error) {
|
||||||
logger := log.FromContext(ctx).WithName("nuclei-scanner")
|
|
||||||
|
|
||||||
if len(targets) == 0 {
|
if len(targets) == 0 {
|
||||||
return nil, fmt.Errorf("no targets provided for scan")
|
return nil, fmt.Errorf("no targets provided for scan")
|
||||||
}
|
}
|
||||||
@@ -114,56 +110,10 @@ func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options Scan
|
|||||||
|
|
||||||
// Write targets to a file
|
// Write targets to a file
|
||||||
targetsFile := filepath.Join(tmpDir, "targets.txt")
|
targetsFile := filepath.Join(tmpDir, "targets.txt")
|
||||||
targetsContent := strings.Join(targets, "\n")
|
if err := os.WriteFile(targetsFile, []byte(strings.Join(targets, "\n")), 0600); err != nil {
|
||||||
if err := os.WriteFile(targetsFile, []byte(targetsContent), 0600); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to write targets file: %w", err)
|
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
|
|
||||||
if _, err := os.Stat(s.nucleiBinaryPath); os.IsNotExist(err) {
|
|
||||||
return nil, fmt.Errorf("nuclei binary not found at %s", s.nucleiBinaryPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify nuclei is executable
|
|
||||||
if err := exec.Command(s.nucleiBinaryPath, "-version").Run(); err != nil {
|
|
||||||
logger.Error(err, "Failed to execute nuclei -version, nuclei may not be properly installed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
// Build the nuclei command arguments
|
||||||
args := s.buildArgs(targetsFile, options)
|
args := s.buildArgs(targetsFile, options)
|
||||||
|
|
||||||
@@ -173,16 +123,6 @@ func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options Scan
|
|||||||
timeout = 30 * time.Minute
|
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
|
// Create context with timeout
|
||||||
scanCtx, cancel := context.WithTimeout(ctx, timeout)
|
scanCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -194,31 +134,14 @@ func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options Scan
|
|||||||
cmd.Stdout = &stdout
|
cmd.Stdout = &stdout
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
logger.Info("Starting nuclei execution")
|
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
duration := time.Since(startTime)
|
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
|
// Check for context cancellation
|
||||||
if scanCtx.Err() == context.DeadlineExceeded {
|
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)
|
return nil, fmt.Errorf("scan timed out after %v", timeout)
|
||||||
}
|
}
|
||||||
if scanCtx.Err() == context.Canceled {
|
if scanCtx.Err() == context.Canceled {
|
||||||
logger.Error(nil, "Scan was cancelled", "stderr", stderrStr)
|
|
||||||
return nil, fmt.Errorf("scan was cancelled")
|
return nil, fmt.Errorf("scan was cancelled")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,40 +151,22 @@ func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options Scan
|
|||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
// Exit code 1 can mean "no results found" which is not an error
|
// Exit code 1 can mean "no results found" which is not an error
|
||||||
if exitErr.ExitCode() != 1 {
|
if exitErr.ExitCode() != 1 {
|
||||||
logger.Error(err, "Nuclei execution failed",
|
return nil, fmt.Errorf("nuclei execution failed: %w, stderr: %s", err, stderr.String())
|
||||||
"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 {
|
} else {
|
||||||
logger.Error(err, "Failed to execute nuclei", "stderr", stderrStr)
|
|
||||||
return nil, fmt.Errorf("failed to execute nuclei: %w", err)
|
return nil, fmt.Errorf("failed to execute nuclei: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the JSONL output
|
// Parse the JSONL output
|
||||||
stdoutBytes := stdout.Bytes()
|
findings, err := ParseJSONLOutput(stdout.Bytes())
|
||||||
logger.Info("Parsing nuclei output", "outputSize", len(stdoutBytes))
|
|
||||||
findings, err := ParseJSONLOutput(stdoutBytes)
|
|
||||||
if err != nil {
|
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)
|
return nil, fmt.Errorf("failed to parse nuclei output: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Parsed findings", "count", len(findings))
|
|
||||||
|
|
||||||
// Calculate summary
|
// Calculate summary
|
||||||
summary := calculateSummary(findings, len(targets), duration)
|
summary := calculateSummary(findings, len(targets), duration)
|
||||||
|
|
||||||
logger.Info("Scan completed",
|
|
||||||
"findings", len(findings),
|
|
||||||
"duration", duration,
|
|
||||||
"targetsScanned", len(targets))
|
|
||||||
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Findings: findings,
|
Findings: findings,
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
@@ -276,8 +181,11 @@ func (s *NucleiScanner) buildArgs(targetsFile string, options ScanOptions) []str
|
|||||||
"-jsonl",
|
"-jsonl",
|
||||||
"-silent",
|
"-silent",
|
||||||
"-no-color",
|
"-no-color",
|
||||||
"-rate-limit", "150", // Limit rate to avoid overwhelming targets
|
}
|
||||||
"-bulk-size", "25", // Process targets in bulk
|
|
||||||
|
// Add templates path if configured
|
||||||
|
if s.templatesPath != "" {
|
||||||
|
args = append(args, "-t", s.templatesPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add specific templates if provided
|
// Add specific templates if provided
|
||||||
@@ -285,31 +193,6 @@ func (s *NucleiScanner) buildArgs(targetsFile string, options ScanOptions) []str
|
|||||||
for _, t := range options.Templates {
|
for _, t := range options.Templates {
|
||||||
args = append(args, "-t", t)
|
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
|
// Add severity filter if provided
|
||||||
|
|||||||
Reference in New Issue
Block a user