Skip to content

Commit f5ed6af

Browse files
committed
Move ScanTypeController from AutoDiscovery to the Operator
Signed-off-by: Jannik Hollenbach <jannik.hollenbach@iteratec.com>
1 parent d67d72b commit f5ed6af

17 files changed

Lines changed: 693 additions & 4 deletions

operator/Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ test: manifests generate fmt vet ## Run tests.
6767
test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.2/hack/setup-envtest.sh
6868
source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test ./... -coverprofile cover.out
6969

70+
view-coverage:
71+
go tool cover -html=cover.out
72+
7073
##@ Build
7174

7275
build: generate fmt vet ## Build manager binary.

operator/apis/cascading/v1/zz_generated.deepcopy.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

operator/apis/execution/v1/scheduledscan_types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ type ScheduledScanSpec struct {
3131

3232
// ScanSpec describes the scan which should be started regularly
3333
ScanSpec *ScanSpec `json:"scanSpec"`
34+
35+
// RetriggerOnScanTypeChange will automatically trigger a new scan for the scheduledScan if the referenced ScanType was updated
36+
// +kubebuilder:validation:Optional
37+
// +kubebuilder:default=false
38+
RetriggerOnScanTypeChange bool `json:"retriggerOnScanTypeChange,omitempty"`
3439
}
3540

3641
// ScheduledScanStatus defines the observed state of ScheduledScan
@@ -42,6 +47,11 @@ type ScheduledScanStatus struct {
4247

4348
// Findings Contains the findings stats of the most recent completed scan
4449
Findings FindingStats `json:"findings,omitempty"`
50+
51+
// Note this is stored in a string not a uint64 as OpenAPI doesn't support unsigned data types and the normal int64 format is obviously one bit too short for uint64's...
52+
53+
// ScanTypeHash contains a hash of the scanType used. Hash is generated after the ScheduledScan is applied to the cluster and is currently not guaranteed to be the one used by the scan controller.
54+
ScanTypeHash string `json:"scanTypeHash,omitempty"`
4555
}
4656

4757
// +kubebuilder:object:root=true

operator/apis/execution/v1/zz_generated.deepcopy.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

operator/config/crd/bases/execution.securecodebox.io_scheduledscans.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ spec:
7373
description: 'Interval describes how often the scan should be repeated
7474
Examples: ''12h'', ''30m'''
7575
type: string
76+
retriggerOnScanTypeChange:
77+
default: false
78+
description: RetriggerOnScanTypeChange will automatically trigger
79+
a new scan for the scheduledScan if the referenced ScanType was
80+
updated
81+
type: boolean
7682
scanSpec:
7783
description: ScanSpec describes the scan which should be started regularly
7884
properties:
@@ -1839,6 +1845,11 @@ spec:
18391845
lastScheduleTime:
18401846
format: date-time
18411847
type: string
1848+
scanTypeHash:
1849+
description: ScanTypeHash contains a hash of the scanType used. Hash
1850+
is generated after the ScheduledScan is applied to the cluster and
1851+
is currently not guaranteed to be the one used by the scan controller.
1852+
type: string
18421853
type: object
18431854
type: object
18441855
served: true

operator/config/rbac/role.yaml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
# SPDX-FileCopyrightText: 2021 iteratec GmbH
2-
#
3-
# SPDX-License-Identifier: Apache-2.0
41

52
---
63
apiVersion: rbac.authorization.k8s.io/v1
@@ -100,6 +97,14 @@ rules:
10097
- get
10198
- patch
10299
- update
100+
- apiGroups:
101+
- execution.securecodebox.io/status
102+
resources:
103+
- scheduledscans
104+
verbs:
105+
- get
106+
- patch
107+
- update
103108
- apiGroups:
104109
- rbac.authorization.k8s.io
105110
resources:
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package controllers
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
executionv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"sigs.k8s.io/controller-runtime/pkg/client"
11+
)
12+
13+
func restartScheduledScan(ctx context.Context, statusWriter client.StatusWriter, scheduledScan executionv1.ScheduledScan) error {
14+
// create a new faked lastScheduledTime in the past to force the scheduledScan to be repeated immediately
15+
// past timestamp is calculated by subtracting the repeat Interval and 24 hours to ensure that it will work even when the auto-discovery and scheduledScan controller have a clock skew
16+
fakedLastSchedule := metav1.Time{Time: time.Now().Add(-scheduledScan.Spec.Interval.Duration - 24*time.Hour)}
17+
scheduledScan.Status.LastScheduleTime = &fakedLastSchedule
18+
err := statusWriter.Update(ctx, &scheduledScan)
19+
if err != nil {
20+
return fmt.Errorf("Failed to restart ScheduledScan: %w", err)
21+
}
22+
23+
return nil
24+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// SPDX-FileCopyrightText: 2021 iteratec GmbH
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package controllers
6+
7+
import (
8+
"context"
9+
"strconv"
10+
"time"
11+
12+
"github.com/go-logr/logr"
13+
executionv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1"
14+
util "github.com/secureCodeBox/secureCodeBox/operator/utils"
15+
16+
"k8s.io/apimachinery/pkg/runtime"
17+
"k8s.io/client-go/tools/record"
18+
ctrl "sigs.k8s.io/controller-runtime"
19+
"sigs.k8s.io/controller-runtime/pkg/client"
20+
)
21+
22+
// ServiceScanReconciler reconciles a Service object
23+
type ScanTypeReconciler struct {
24+
client.Client
25+
Log logr.Logger
26+
Scheme *runtime.Scheme
27+
Recorder record.EventRecorder
28+
}
29+
30+
// +kubebuilder:rbac:groups="execution.securecodebox.io",resources=scantypes,verbs=get;list;watch
31+
// +kubebuilder:rbac:groups="execution.securecodebox.io",resources=scheduledscans,verbs=get;list;watch;create;update;patch
32+
// +kubebuilder:rbac:groups="execution.securecodebox.io/status",resources=scheduledscans,verbs=get;update;patch
33+
34+
// Reconcile compares the Service object against the state of the cluster and updates both if needed
35+
func (r *ScanTypeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
36+
log := r.Log
37+
38+
// fetch service
39+
var scanType executionv1.ScanType
40+
if err := r.Get(ctx, req.NamespacedName, &scanType); err != nil {
41+
return ctrl.Result{Requeue: true, RequeueAfter: 3 * time.Second}, client.IgnoreNotFound(err)
42+
}
43+
44+
currentScanTypeHash := util.HashScanType(scanType)
45+
log.V(9).Info("Running ScanTypeReconciler", "scanType", req.Name, "namespace", req.Namespace, "hash", currentScanTypeHash)
46+
47+
// look for scheduledscans started with the scantype
48+
var scheduledScans executionv1.ScheduledScanList
49+
r.List(ctx, &scheduledScans, client.InNamespace(scanType.Namespace))
50+
51+
shouldRequeue := false
52+
53+
for _, scheduledScan := range scheduledScans.Items {
54+
55+
if scheduledScan.Spec.ScanSpec.ScanType != scanType.Name {
56+
log.V(9).Info("ScanType doesn't match, skipping", "scheduledScan", scheduledScan.Name, "namespace", scheduledScan.Namespace, "scanType", scanType.Name)
57+
continue
58+
}
59+
if scheduledScan.Spec.RetriggerOnScanTypeChange == false {
60+
log.V(9).Info("ScheduledScan isn't configured for automatic scan retriggering, skipping", "scheduledScan", scheduledScan.Name, "namespace", scheduledScan.Namespace, "scanType", scanType.Name)
61+
continue
62+
}
63+
if scheduledScan.Status.ScanTypeHash == "" {
64+
log.V(8).Info("ScheduledScan doesn't have a checksum yet.", "scheduledScan", scheduledScan.Name, "namespace", scheduledScan.Namespace, "scanType", scanType.Name)
65+
shouldRequeue = true
66+
continue
67+
}
68+
69+
log.V(8).Info("Checking if ScheduledScan has to be restarted", "scheduledScan", scheduledScan.Name, "namespace", scheduledScan.Namespace)
70+
scheduledScanChecksum, err := strconv.ParseUint(scheduledScan.Status.ScanTypeHash, 10, 64)
71+
if err != nil {
72+
log.Error(err, "Failed to convert string encoded hash into uint64")
73+
shouldRequeue = true
74+
continue
75+
}
76+
77+
if scheduledScanChecksum != currentScanTypeHash {
78+
log.V(4).Info("Retriggering ScheduledScan as the underlying ScanType has been updated.", "checksumForScheduledScan", scheduledScanChecksum, "currentScanTypeHash", currentScanTypeHash, "scheduledScan", scheduledScan.Name, "namespace", scheduledScan.Namespace, "scanType", scanType.Name)
79+
80+
err := restartScheduledScan(ctx, r.Status(), scheduledScan)
81+
if err != nil {
82+
return ctrl.Result{
83+
Requeue: true,
84+
RequeueAfter: 10 * time.Second,
85+
}, err
86+
}
87+
r.Recorder.Event(&scheduledScan, "Normal", "Retriggered", "ScheduledScan was retriggered beforehand, as the underlying scanType was updated.")
88+
} else {
89+
log.V(9).Info("ScanType and ScheduledScan Checksum match. No reason to restart the ScheduledScan", "checksumForScheduledScan", scheduledScanChecksum, "currentScanTypeHash", currentScanTypeHash, "scheduledScan", scheduledScan.Name, "namespace", scheduledScan.Namespace, "scanType", scanType.Name)
90+
}
91+
}
92+
93+
return ctrl.Result{
94+
Requeue: shouldRequeue,
95+
RequeueAfter: 3 * time.Second,
96+
}, nil
97+
}
98+
99+
// SetupWithManager sets up the controller and initializes every thing it needs
100+
func (r *ScanTypeReconciler) SetupWithManager(mgr ctrl.Manager) error {
101+
return ctrl.NewControllerManagedBy(mgr).
102+
For(&executionv1.ScanType{}).
103+
Complete(r)
104+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// SPDX-FileCopyrightText: 2021 iteratec GmbH
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package controllers
6+
7+
import (
8+
"context"
9+
"time"
10+
11+
. "github.com/onsi/ginkgo"
12+
. "github.com/onsi/gomega"
13+
executionv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1"
14+
"k8s.io/apimachinery/pkg/api/errors"
15+
16+
"k8s.io/apimachinery/pkg/types"
17+
//+kubebuilder:scaffold:imports
18+
)
19+
20+
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
21+
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
22+
// Define utility constants for object names and testing timeouts and intervals.
23+
const (
24+
timeout = time.Second * 10
25+
interval = time.Millisecond * 250
26+
)
27+
28+
var _ = Describe("ScanType controller", func() {
29+
Context("Restarting ScheduledScans on ScanType Config Changes", func() {
30+
It("Should restart a scheduledScan when the scantype was update", func() {
31+
ctx := context.Background()
32+
namespace := "scantype-autorestart-config-change-test"
33+
34+
createNamespace(ctx, namespace)
35+
createScanType(ctx, namespace)
36+
scheduledScan := createScheduledScan(ctx, namespace)
37+
38+
// ensure that the ScheduledScan has been triggered
39+
waitForScheduledScanToBeTriggered(ctx, namespace)
40+
k8sClient.Get(ctx, types.NamespacedName{Name: "test-scan", Namespace: namespace}, &scheduledScan)
41+
initialExecutionTime := *scheduledScan.Status.LastScheduleTime
42+
43+
// wait at least one second to ensure that the unix timestamps are at least one second apart.
44+
time.Sleep(1 * time.Second)
45+
46+
By("Update ScanType to trigger rescan")
47+
var scanType executionv1.ScanType
48+
k8sClient.Get(ctx, types.NamespacedName{Name: "nmap", Namespace: namespace}, &scanType)
49+
if scanType.ObjectMeta.Annotations == nil {
50+
scanType.ObjectMeta.Annotations = map[string]string{}
51+
}
52+
scanType.ObjectMeta.Annotations["foobar.securecodebox.io/example"] = "barfoo"
53+
err := k8sClient.Update(ctx, &scanType)
54+
if err != nil {
55+
panic(err)
56+
}
57+
58+
By("Controller should set the lastScheduled Timestamp to the past to force a re-scan")
59+
Eventually(func() bool {
60+
err := k8sClient.Get(ctx, types.NamespacedName{Name: "test-scan", Namespace: namespace}, &scheduledScan)
61+
if errors.IsNotFound(err) {
62+
panic("ScheduledScan should be present for this check!")
63+
}
64+
65+
return scheduledScan.Status.LastScheduleTime.Unix() != initialExecutionTime.Unix()
66+
}, timeout, interval).Should(BeTrue())
67+
})
68+
})
69+
70+
Context("Should not trigger rescan when ScanType stays the same", func() {
71+
It("Should restart a scheduledScan when the scantype was update", func() {
72+
ctx := context.Background()
73+
namespace := "scantype-no-autorestart-config-change-test"
74+
75+
createNamespace(ctx, namespace)
76+
createScanType(ctx, namespace)
77+
scheduledScan := createScheduledScan(ctx, namespace)
78+
79+
// ensure that the ScheduledScan has been triggered
80+
waitForScheduledScanToBeTriggered(ctx, namespace)
81+
k8sClient.Get(ctx, types.NamespacedName{Name: "test-scan", Namespace: namespace}, &scheduledScan)
82+
initialExecutionTime := *scheduledScan.Status.LastScheduleTime
83+
84+
// wait at least one second to ensure that the unix timestamps would be at least one second apart.
85+
time.Sleep(1 * time.Second)
86+
87+
By("Controller should not restart scheduledscan")
88+
Consistently(func() bool {
89+
var scheduledScan executionv1.ScheduledScan
90+
err := k8sClient.Get(ctx, types.NamespacedName{Name: "test-scan", Namespace: namespace}, &scheduledScan)
91+
if errors.IsNotFound(err) {
92+
panic("ScheduledScan should be present for this check!")
93+
}
94+
95+
return scheduledScan.Status.LastScheduleTime.Unix() == initialExecutionTime.Unix()
96+
}, timeout, interval).Should(BeTrue(), "Scan was restarted without need")
97+
})
98+
})
99+
})
100+
101+
func waitForScheduledScanToBeTriggered(ctx context.Context, namespace string) {
102+
var scheduledScan executionv1.ScheduledScan
103+
By("Wait for ScheduledScan to trigger the initial Scan")
104+
Eventually(func() bool {
105+
err := k8sClient.Get(ctx, types.NamespacedName{Name: "test-scan", Namespace: namespace}, &scheduledScan)
106+
if errors.IsNotFound(err) {
107+
panic("ScheduledScan should be present for this check!")
108+
}
109+
110+
return scheduledScan.Status.LastScheduleTime != nil
111+
}, timeout, interval).Should(BeTrue())
112+
}

operator/controllers/execution/scheduledscan_controller.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ import (
1515
"github.com/go-logr/logr"
1616
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1717
"k8s.io/apimachinery/pkg/runtime"
18+
"k8s.io/apimachinery/pkg/types"
1819
ctrl "sigs.k8s.io/controller-runtime"
1920
"sigs.k8s.io/controller-runtime/pkg/client"
2021

2122
executionv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1"
23+
"github.com/secureCodeBox/secureCodeBox/operator/utils"
2224
)
2325

2426
var (
@@ -48,7 +50,6 @@ func (r *ScheduledScanReconciler) Reconcile(ctx context.Context, req ctrl.Reques
4850
// we'll ignore not-found errors, since they can't be fixed by an immediate
4951
// requeue (we'll need to wait for a new notification), and we can get them
5052
// on deleted requests.
51-
log.V(7).Info("Unable to fetch ScheduledScan")
5253
return ctrl.Result{}, client.IgnoreNotFound(err)
5354
}
5455

@@ -108,6 +109,25 @@ func (r *ScheduledScanReconciler) Reconcile(ctx context.Context, req ctrl.Reques
108109

109110
// check if it is time to start the next Scan
110111
if !time.Now().Before(nextSchedule) {
112+
if scheduledScan.Spec.RetriggerOnScanTypeChange == true {
113+
// generate hash for current state of the configured ScanType
114+
var scanType executionv1.ScanType
115+
if err := r.Get(ctx, types.NamespacedName{Name: scheduledScan.Spec.ScanSpec.ScanType, Namespace: scheduledScan.Namespace}, &scanType); err != nil {
116+
log.V(5).Info("Unable to fetch ScanType for ScheduledScan", "scanType", scanType.Name, "namespace", scanType.Namespace)
117+
return ctrl.Result{}, client.IgnoreNotFound(err)
118+
}
119+
120+
oldScheduledScan := scheduledScan.DeepCopy()
121+
hash := utils.HashScanType(scanType)
122+
scheduledScan.Status.ScanTypeHash = fmt.Sprintf("%d", hash)
123+
log.V(9).Info("Setting hash:", "hash", scheduledScan.Status.ScanTypeHash, "scheduledScan", scheduledScan, "namespace", req.Namespace)
124+
if err := r.Status().Patch(ctx, &scheduledScan, client.MergeFrom(oldScheduledScan)); err != nil {
125+
return ctrl.Result{}, fmt.Errorf("Failed to update ScheduledScan with the current ScanType hash: %w", err)
126+
} else {
127+
log.V(7).Info("Updated ScanType Hash", "scheduledScan", req.Name, "scanType", scanType.Name, "hash", hash)
128+
}
129+
}
130+
111131
// It's time!
112132
var scan = &executionv1.Scan{
113133
ObjectMeta: metav1.ObjectMeta{

0 commit comments

Comments
 (0)