Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/admission controller #226

Merged
merged 37 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
92f58fe
Adding admission pkg
amitschendel Jun 18, 2024
931c296
Adding support for admission controller in the main server init
amitschendel Jun 18, 2024
cab0884
Adding admission config
amitschendel Jun 18, 2024
b343ce6
Adding testing json
amitschendel Jun 18, 2024
92d9d2d
Adding needed pkgs
amitschendel Jun 18, 2024
e3cf97b
Resolved conflicts
amitschendel Jun 18, 2024
8482107
Bumping go to 1.22
amitschendel Jun 19, 2024
172c924
Bumping go to 1.22
amitschendel Jun 19, 2024
8435848
Removing some rules
amitschendel Jun 19, 2024
5fa777a
Changing context to notify context
amitschendel Jun 19, 2024
cf805f7
Adding some CR fixes
amitschendel Jun 19, 2024
f4e85a0
go mod tidy
amitschendel Jun 19, 2024
3052023
Fixing exporter to not alert multiple times on alert limit reached
amitschendel Jun 19, 2024
f2dc5d5
Adding skeleton of bindings
amitschendel Jun 22, 2024
b51c633
Adding packages
amitschendel Jun 22, 2024
d3ca6a0
Adding main
amitschendel Jun 22, 2024
c6cc2e6
Adding some code
amitschendel Jun 22, 2024
9bb5d6a
Adding improved code
amitschendel Jun 23, 2024
fc52fe9
Adding new code
amitschendel Jun 23, 2024
458d918
Fixing what needs to be fixed
amitschendel Jun 23, 2024
84ee781
Fixing what needs to be fixed2
amitschendel Jun 23, 2024
6d6a477
Adding fixed code
amitschendel Jul 2, 2024
63d0438
Adding new packages
amitschendel Jul 2, 2024
59293a2
Fixing pod conversion
amitschendel Jul 2, 2024
560e154
Removing unused code
amitschendel Jul 2, 2024
f3c7e16
Adding rule test
amitschendel Jul 2, 2024
104df0f
go mod tidy
amitschendel Jul 2, 2024
7628a13
Pretty code
amitschendel Jul 2, 2024
c3fc344
Adding portforward rule
amitschendel Jul 4, 2024
3b0d474
Adding timestamp
amitschendel Jul 4, 2024
4c0e92b
Fixing timestamp
amitschendel Jul 4, 2024
787ccc6
Adding tests for cache and helpers
amitschendel Jul 14, 2024
32289db
Fixing conflicts
amitschendel Jul 14, 2024
2511183
Changing import pkg of node-agent
amitschendel Jul 14, 2024
7c7c360
Adding server tests
amitschendel Jul 14, 2024
8d4af97
Fixing conflicts
amitschendel Jul 15, 2024
f17c936
Removing comments
amitschendel Jul 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr-created.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ jobs:
pr-created:
uses: kubescape/workflows/.github/workflows/incluster-comp-pr-created.yaml@main
with:
GO_VERSION: "1.21"
GO_VERSION: "1.22"
secrets: inherit
2 changes: 1 addition & 1 deletion .github/workflows/pr-merged.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
CGO_ENABLED: 0
GO111MODULE: ""
BUILD_PLATFORM: linux/amd64,linux/arm64
GO_VERSION: "1.21"
GO_VERSION: "1.22"
REQUIRED_TESTS: '[
"vuln_scan",
"vuln_scan_trigger_scan_public_registry",
Expand Down
207 changes: 207 additions & 0 deletions admission/exporter/http_exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package exporters

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
"time"

"github.com/kubescape/go-logger"
"github.com/kubescape/go-logger/helpers"
"github.com/kubescape/operator/admission/rules"

apitypes "github.com/armosec/armoapi-go/armotypes"
)

type HTTPExporterConfig struct {
// URL is the URL to send the HTTP request to
URL string `json:"url"`
// Headers is a map of headers to send in the HTTP request
Headers map[string]string `json:"headers"`
// Timeout is the timeout for the HTTP request
TimeoutSeconds int `json:"timeoutSeconds"`
// Method is the HTTP method to use for the HTTP request
Method string `json:"method"`
MaxAlertsPerMinute int `json:"maxAlertsPerMinute"`
}

// we will have a CRD-like json struct to send in the HTTP request
type HTTPExporter struct {
config HTTPExporterConfig
Host string `json:"host"`
ClusterName string `json:"clusterName"`
httpClient *http.Client
// alertCount is the number of alerts sent in the last minute, used to limit the number of alerts sent, so we don't overload the system or reach the rate limit
alertCount int
alertCountLock sync.Mutex
alertCountStart time.Time
alertLimitNotified bool
}

type HTTPAlertsList struct {
Kind string `json:"kind"`
ApiVersion string `json:"apiVersion"`
Spec HTTPAlertsListSpec `json:"spec"`
}

type HTTPAlertsListSpec struct {
Alerts []apitypes.RuntimeAlert `json:"alerts"`
ProcessTree apitypes.ProcessTree `json:"processTree"`
}

func (config *HTTPExporterConfig) Validate() error {
if config.Method == "" {
config.Method = "POST"
} else if config.Method != "POST" && config.Method != "PUT" {
return fmt.Errorf("method must be POST or PUT")
}
if config.TimeoutSeconds == 0 {
config.TimeoutSeconds = 5
}
if config.MaxAlertsPerMinute == 0 {
config.MaxAlertsPerMinute = 100
}
if config.Headers == nil {
config.Headers = make(map[string]string)
}
if config.URL == "" {
return fmt.Errorf("URL is required")
}
return nil
}

// InitHTTPExporter initializes an HTTPExporter with the given URL, headers, timeout, and method
func InitHTTPExporter(config HTTPExporterConfig, clusterName string) (*HTTPExporter, error) {
if err := config.Validate(); err != nil {
return nil, err
}

return &HTTPExporter{
ClusterName: clusterName,
config: config,
httpClient: &http.Client{
Timeout: time.Duration(config.TimeoutSeconds) * time.Second,
},
}, nil
}

func (exporter *HTTPExporter) sendAlertLimitReached() {
httpAlert := apitypes.RuntimeAlert{
Message: "Alert limit reached",
HostName: exporter.Host,
AlertType: apitypes.AlertTypeRule, // TODO: change this to a new alert type. @bez
BaseRuntimeAlert: apitypes.BaseRuntimeAlert{
AlertName: "AlertLimitReached",
Severity: 1000, // Replace with ruleengine.RulePrioritySystemIssue once node agent is bumping the types pkg @amitschendel.
FixSuggestions: "Check logs for more information",
},
RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{
ClusterName: exporter.ClusterName,
NodeName: "Operator",
},
}

logger.L().Error("Alert limit reached", helpers.Int("alerts", exporter.alertCount), helpers.String("since", exporter.alertCountStart.Format(time.RFC3339)))
exporter.sendInAlertList(httpAlert, apitypes.ProcessTree{})
}

func (exporter *HTTPExporter) SendAdmissionAlert(ruleFailure rules.RuleFailure) {
isLimitReached := exporter.checkAlertLimit()
if isLimitReached && !exporter.alertLimitNotified {
exporter.sendAlertLimitReached()

Choose a reason for hiding this comment

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

This sends an alert every time the limit is reached. Instead of sending multiple alerts for each instance that reaches the limit, we should send a single alert indicating that the limit has been reached. This can be managed by wrapping the alert logic in a boolean condition to ensure the 'limit reached' alert is sent only once.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

exporter.alertLimitNotified = true
return
}
// populate the RuntimeAlert struct with the data from the failedRule
k8sDetails := apitypes.RuntimeAlertK8sDetails{
ClusterName: exporter.ClusterName,
}

httpAlert := apitypes.RuntimeAlert{
Message: ruleFailure.GetRuleAlert().RuleDescription,
HostName: exporter.Host,
AlertType: apitypes.AlertTypeAdmission,
BaseRuntimeAlert: apitypes.BaseRuntimeAlert{
Timestamp: time.Now(),
},
AdmissionAlert: ruleFailure.GetAdmissionsAlert(),
RuntimeAlertK8sDetails: k8sDetails,
RuleAlert: apitypes.RuleAlert{
RuleDescription: ruleFailure.GetRuleAlert().RuleDescription,
},
RuleID: ruleFailure.GetRuleId(),
}
exporter.sendInAlertList(httpAlert, apitypes.ProcessTree{})
}

func (exporter *HTTPExporter) sendInAlertList(httpAlert apitypes.RuntimeAlert, processTree apitypes.ProcessTree) {
// create the HTTPAlertsListSpec struct
// TODO: accumulate alerts and send them in a batch
httpAlertsListSpec := HTTPAlertsListSpec{
Alerts: []apitypes.RuntimeAlert{httpAlert},
ProcessTree: processTree,
}
// create the HTTPAlertsList struct
httpAlertsList := HTTPAlertsList{
Kind: "RuntimeAlerts",

Choose a reason for hiding this comment

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

Is this runtimeAlerts? I dont think so.
I believe this should have a different kind so it can be treated as admission controller alerts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is a RuntimeAlert the admission kind is passed in other place.

Choose a reason for hiding this comment

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

This is the same kind of alert as coming from the node-agent, in other words, we will not know "down the line" the source of the alert. And I think we will want to know...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

ApiVersion: "kubescape.io/v1",
Spec: httpAlertsListSpec,
}

// create the JSON representation of the HTTPAlertsList struct
bodyBytes, err := json.Marshal(httpAlertsList)
if err != nil {
logger.L().Error("failed to marshal HTTPAlertsList", helpers.Error(err))
return
}
bodyReader := bytes.NewReader(bodyBytes)

// send the HTTP request
req, err := http.NewRequest(exporter.config.Method,
exporter.config.URL+"/v1/runtimealerts", bodyReader)
if err != nil {
logger.L().Error("failed to create HTTP request", helpers.Error(err))
return
}
for key, value := range exporter.config.Headers {
req.Header.Set(key, value)
}

resp, err := exporter.httpClient.Do(req)
if err != nil {
logger.L().Error("failed to send HTTP request", helpers.Error(err))
return
}
defer resp.Body.Close()

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
logger.L().Error("Received non-2xx status code", helpers.Int("status", resp.StatusCode))
return
}

// discard the body
Copy link
Contributor

Choose a reason for hiding this comment

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

I am still not convinced this is needed... do you have some pointers (I know we do it in different places, just curious)?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I prefer to do it anyway to avoid potential leaks.

if _, err := io.Copy(io.Discard, resp.Body); err != nil {
logger.L().Error("failed to clear response body", helpers.Error(err))
}
}

func (exporter *HTTPExporter) checkAlertLimit() bool {
exporter.alertCountLock.Lock()
defer exporter.alertCountLock.Unlock()

if exporter.alertCountStart.IsZero() {
exporter.alertCountStart = time.Now()
}

if time.Since(exporter.alertCountStart) > time.Minute {
exporter.alertCountStart = time.Now()
exporter.alertCount = 0
exporter.alertLimitNotified = false
}

exporter.alertCount++
return exporter.alertCount > exporter.config.MaxAlertsPerMinute
}
Loading
Loading