From 5c45c7c7cd0de3f4a79a49a964d409f289e3df84 Mon Sep 17 00:00:00 2001 From: elliotxx <951376975@qq.com> Date: Mon, 11 Dec 2023 15:34:00 +0800 Subject: [PATCH] feat: support score api, refine severity part in audit pkg --- pkg/core/handler/audit/audit.go | 79 +++++++++++++++++-------- pkg/core/handler/audit/util.go | 33 +++++++++++ pkg/core/manager/audit/manager.go | 35 ++++++++++- pkg/core/manager/audit/types.go | 25 ++++++++ pkg/core/manager/audit/util.go | 18 ++++++ pkg/core/server/server.go | 1 + pkg/scanner/kubeaudit/kubeaudit.go | 42 +++++++++---- pkg/scanner/kubeaudit/kubeaudit_test.go | 2 +- pkg/scanner/kubeaudit/util.go | 37 ++++++++++-- pkg/scanner/types.go | 25 ++++---- 10 files changed, 245 insertions(+), 52 deletions(-) create mode 100644 pkg/core/handler/audit/util.go create mode 100644 pkg/core/manager/audit/types.go create mode 100644 pkg/core/manager/audit/util.go diff --git a/pkg/core/handler/audit/audit.go b/pkg/core/handler/audit/audit.go index 13ca3e58..3420962b 100644 --- a/pkg/core/handler/audit/audit.go +++ b/pkg/core/handler/audit/audit.go @@ -15,7 +15,6 @@ package audit import ( - "io" "net/http" "github.com/KusionStack/karbour/pkg/core/handler" @@ -44,28 +43,11 @@ func Audit(auditMgr *audit.AuditManager) http.HandlerFunc { // Begin the auditing process, logging the start. log.Info("Starting audit of the specified manifest in handler ...") - // Initialize an empty payload to hold the manifest data. + // Decode the request body into the payload. payload := &Payload{} - - // Check if the content type is plain text, read it as such. - if render.GetRequestContentType(r) == render.ContentTypePlainText { - // Read the request body. - body, err := io.ReadAll(r.Body) - defer r.Body.Close() // Ensure the body is closed after reading. - if err != nil { - // Handle any reading errors by sending a failure response. - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - // Set the read content as the manifest payload. - payload.Manifest = string(body) - } else { - // For non-plain text, decode the JSON body into the payload. - if err := render.DecodeJSON(r.Body, payload); err != nil { - // Handle JSON decoding errors. - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } + if err := decode(r, payload); err != nil { + render.Render(w, r, handler.FailureResponse(ctx, err)) + return } // Log successful decoding of the request body. @@ -75,12 +57,61 @@ func Audit(auditMgr *audit.AuditManager) http.HandlerFunc { // Perform the audit using the manager and the provided manifest. issues, err := auditMgr.Audit(ctx, payload.Manifest) if err != nil { - // Handle audit errors by sending a failure response. render.Render(w, r, handler.FailureResponse(ctx, err)) return } - // Send a success response with the audit issues. render.JSON(w, r, handler.SuccessResponse(ctx, issues)) } } + +// Score returns an HTTP handler function that calculates a score for the +// audited manifest. It utilizes an AuditManager to compute the score based +// on detected issues. +// @Summary ScoreHandler calculates a score for the audited manifest. +// @Description This endpoint calculates a score for the provided manifest based +// on the number and severity of issues detected during the audit. +// @Tags audit +// @Accept plain, json +// @Produce json +// @Param manifest body string true "Manifest data to calculate score for (either plain text or JSON format)" +// @Success 200 {object} ScoreResponse "Score calculation results" +// @Failure 400 {object} FailureResponse "Error details if manifest cannot be processed or score cannot be calculated" +// @Router /audit/score [post] +func Score(auditMgr *audit.AuditManager) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Extract the context and logger from the request. + ctx := r.Context() + log := ctxutil.GetLogger(ctx) + + // Begin the auditing process, logging the start. + log.Info("Starting calculate score with specified manifest in handler...") + + // Decode the request body into the payload. + payload := &Payload{} + if err := decode(r, payload); err != nil { + render.Render(w, r, handler.FailureResponse(ctx, err)) + return + } + + // Log successful decoding of the request body. + log.Info("Successfully decoded the request body to payload", + "payload", payload) + + // Perform the audit to gather issues for score calculation. + issues, err := auditMgr.Audit(ctx, payload.Manifest) + if err != nil { + render.Render(w, r, handler.FailureResponse(ctx, err)) + return + } + + // Calculate score using the audit issues. + data, err := auditMgr.Score(ctx, issues) + if err != nil { + render.Render(w, r, handler.FailureResponse(ctx, err)) + return + } + + render.JSON(w, r, handler.SuccessResponse(ctx, data)) + } +} diff --git a/pkg/core/handler/audit/util.go b/pkg/core/handler/audit/util.go new file mode 100644 index 00000000..1fa409e6 --- /dev/null +++ b/pkg/core/handler/audit/util.go @@ -0,0 +1,33 @@ +package audit + +import ( + "io" + "net/http" + + "github.com/go-chi/render" +) + +// decode detects the correct decoder for use on an HTTP request and +// marshals into a given interface. +func decode(r *http.Request, payload *Payload) error { + // Check if the content type is plain text, read it as such. + if render.GetRequestContentType(r) == render.ContentTypePlainText { + // Read the request body. + body, err := io.ReadAll(r.Body) + defer r.Body.Close() // Ensure the body is closed after reading. + if err != nil { + // // Handle any reading errors by sending a failure response. + // render.Render(w, r, handler.FailureResponse(ctx, err)) + return err + } + // Set the read content as the manifest payload. + payload.Manifest = string(body) + } else { + // For non-plain text, decode the JSON body into the payload. + if err := render.DecodeJSON(r.Body, payload); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/core/manager/audit/manager.go b/pkg/core/manager/audit/manager.go index 13024083..c8b3ea2b 100644 --- a/pkg/core/manager/audit/manager.go +++ b/pkg/core/manager/audit/manager.go @@ -32,10 +32,11 @@ type AuditManager struct { // NewAuditManager initializes a new instance of AuditManager with a KubeScanner. func NewAuditManager() (*AuditManager, error) { // Create a new Kubernetes scanner instance. - kubeauditScanner, err := kubeaudit.New() + kubeauditScanner, err := kubeaudit.Default() if err != nil { return nil, err } + return &AuditManager{ s: kubeauditScanner, // Set the scanner in the AuditManager. }, nil @@ -51,3 +52,35 @@ func (m *AuditManager) Audit(ctx context.Context, manifest string) ([]*scanner.I // Execute the scan using the scanner's ScanManifest method. return m.s.ScanManifest(ctx, strings.NewReader(manifest)) } + +// Score calculates a score based on the severity and total number of issues +// identified during the audit. It aggregates statistics on different severity +// levels and generates a cumulative score. +func (m *AuditManager) Score(ctx context.Context, issues []*scanner.Issue) (*ScoreData, error) { + // Retrieve logger from context and log the start of the audit. + log := ctxutil.GetLogger(ctx) + log.Info("Starting calculate score with specified issues list in AuditManager ...") + + // Initialize variables to calculate the score. + issueTotal, severitySum := len(issues), 0 + severityStats := map[string]int{} + + // Summarize severity statistics for all issues. + for _, issue := range issues { + severitySum += int(issue.Severity) + severityStats[issue.Severity.String()] += 1 + } + + // Use the aggregated data to calculate the score. + score := CalculateScore(issueTotal, severitySum) + + // Prepare the score data including the total, sum and statistics. + data := &ScoreData{ + Score: score, + IssuesTotal: issueTotal, + SeveritySum: severitySum, + SeverityStatistic: severityStats, + } + + return data, nil +} diff --git a/pkg/core/manager/audit/types.go b/pkg/core/manager/audit/types.go new file mode 100644 index 00000000..81ba09e4 --- /dev/null +++ b/pkg/core/manager/audit/types.go @@ -0,0 +1,25 @@ +package audit + +// ScoreData encapsulates the results of scoring an audited manifest. It provides +// a numerical score along with statistics about the total number of issues and +// their severities. +type ScoreData struct { + // Score represents the calculated score of the audited manifest based on + // the number and severity of issues. It provides a quantitative measure + // of the security posture of the resources in the manifest. + Score float64 `json:"score"` + + // IssuesTotal is the total count of all issues found during the audit. + // This count can be used to understand the overall number of problems + // that need to be addressed. + IssuesTotal int `json:"issuesTotal"` + + // SeveritySum is the sum of severity scores of all issues, which can be + // used to gauge the cumulative severity of all problems found. + SeveritySum int `json:"severitySum"` + + // SeverityStatistic is a mapping of severity levels to their respective + // number of occurrences. It allows for a quick overview of the distribution + // of issues across different severity categories. + SeverityStatistic map[string]int `json:"severityStatistic"` +} diff --git a/pkg/core/manager/audit/util.go b/pkg/core/manager/audit/util.go new file mode 100644 index 00000000..795ffd3b --- /dev/null +++ b/pkg/core/manager/audit/util.go @@ -0,0 +1,18 @@ +package audit + +import ( + "math" +) + +// P is the number of issues, and S is the sum of the severity (range 1-5) of +// the issue S will not be less than P. +// +// Example: +// - When there is one high-level issue, P=1 and S=3. +// - When there are three high-level issues, P=3 and S=9. +// - When there are ten low-level issues, P=10 and S=10. +func CalculateScore(p, s int) float64 { + a, b := -0.04, -0.06 + param := a*float64(p) + b*float64(s) + return 100 * math.Exp(param) +} diff --git a/pkg/core/server/server.go b/pkg/core/server/server.go index 0c2e3c97..377d10bb 100644 --- a/pkg/core/server/server.go +++ b/pkg/core/server/server.go @@ -123,6 +123,7 @@ func setupAPIV1( r.Route("/audit", func(r chi.Router) { r.Post("/", audithandler.Audit(auditMgr)) + r.Post("/score", audithandler.Score(auditMgr)) }) } diff --git a/pkg/scanner/kubeaudit/kubeaudit.go b/pkg/scanner/kubeaudit/kubeaudit.go index 8d4dc2ec..1e6b4b67 100644 --- a/pkg/scanner/kubeaudit/kubeaudit.go +++ b/pkg/scanner/kubeaudit/kubeaudit.go @@ -30,21 +30,28 @@ import ( "sigs.k8s.io/yaml" ) -// Ensure that kubeauditScanner implements the scanner.KubeScanner interface. -var _ scanner.KubeScanner = &kubeauditScanner{} - // ScannerName is the name of the scanner. const ScannerName = "KubeAudit" +// Ensure that kubeauditScanner implements the scanner.KubeScanner interface. +var _ scanner.KubeScanner = &kubeauditScanner{} + // kubeauditScanner is an implementation of scanner.KubeScanner that utilizes // the functionality from the kubeaudit package to perform security audits. type kubeauditScanner struct { - kubeAuditor *kubeauditpkg.Kubeaudit - serializer *json.Serializer + kubeAuditor *kubeauditpkg.Kubeaudit + attentionLevel scanner.IssueSeverityLevel + serializer *json.Serializer } -// New creates a new instance of a kubeaudit-based scanner. -func New() (scanner.KubeScanner, error) { +// New creates a new instance of a kubeaudit-based scanner with the specified +// attention level. +// The attentionLevel sets a threshold, and only issues that meet or exceed this +// threshold are included in the audit results. +// For example, if the attentionLevel is set to "Medium", then only issues +// classified at the "Medium" level or higher ("Medium", "High", "Critical") +// will be returned to the caller. +func New(attentionLevel scanner.IssueSeverityLevel) (scanner.KubeScanner, error) { // Initialize auditors with the kubeaudit configuration. auditors, err := all.Auditors(config.KubeauditConfig{}) if err != nil { @@ -63,12 +70,24 @@ func New() (scanner.KubeScanner, error) { json.SerializerOptions{Yaml: true, Pretty: false, Strict: false}, ) + // Default attentionLevel to Low if it's invalid (less than zero). + if int(attentionLevel) < 0 { + attentionLevel = scanner.Low + } + return &kubeauditScanner{ - kubeAuditor: kubeAuditor, - serializer: serializer, + kubeAuditor: kubeAuditor, + attentionLevel: attentionLevel, + serializer: serializer, }, nil } +// New creates a default instance of a kubeaudit-based scanner with the default +// attention level. +func Default() (scanner.KubeScanner, error) { + return New(scanner.Low) +} + // Name returns the name of the kubeaudit scanner. func (s *kubeauditScanner) Name() string { return ScannerName @@ -101,7 +120,10 @@ func (s *kubeauditScanner) ScanManifest(ctx context.Context, manifest io.Reader) for _, result := range report.Results() { // Process the audit results and convert them to scanner.Issue. for _, auditResult := range result.GetAuditResults() { - issues = append(issues, AuditResult2Issue(auditResult)) + newIssue := AuditResult2Issue(auditResult) + if int(newIssue.Severity) >= int(s.attentionLevel) { + issues = append(issues, newIssue) + } } } diff --git a/pkg/scanner/kubeaudit/kubeaudit_test.go b/pkg/scanner/kubeaudit/kubeaudit_test.go index 797cb8b0..9eff4639 100644 --- a/pkg/scanner/kubeaudit/kubeaudit_test.go +++ b/pkg/scanner/kubeaudit/kubeaudit_test.go @@ -107,7 +107,7 @@ func commonCheck(t *testing.T, issues []*scanner.Issue) { } func createTestScanner(t *testing.T) scanner.KubeScanner { - ks, err := New() + ks, err := Default() if err != nil { t.Fatalf("Failed to create kubeauditScanner: %s", err) } diff --git a/pkg/scanner/kubeaudit/util.go b/pkg/scanner/kubeaudit/util.go index 8978f414..94405570 100644 --- a/pkg/scanner/kubeaudit/util.go +++ b/pkg/scanner/kubeaudit/util.go @@ -19,12 +19,39 @@ import ( kubeauditpkg "github.com/elliotxx/kubeaudit" ) -// AuditResult2Issue converts a kubeaudit.AuditResult to a scanner.Issue. +// AuditResult2Issue converts a kubeaudit.AuditResult to a scanner.Issue, +// which can be used to report security findings in a standardized format. func AuditResult2Issue(auditResult *kubeauditpkg.AuditResult) *scanner.Issue { return &scanner.Issue{ - Scanner: ScannerName, // The name of the scanner that identified the issue. - Severity: scanner.IssueSeverityLevel(auditResult.Severity), // The severity of the issue based on the audit result. - Title: auditResult.Rule, // The rule that was violated, serving as the issue's title. - Message: auditResult.Message, // A human-readable message describing the issue. + // Scanner is the name of the scanner that identified the issue. + Scanner: ScannerName, + + // Severity represents the severity level of the issue as determined by + // the audit result. + Severity: ConvertSeverity(auditResult.Severity), + + // Title is the rule that was violated, which serves as a concise + // description of the issue. + Title: auditResult.Rule, + + // Message provides a detailed, human-readable description of the issue. + Message: auditResult.Message, + } +} + +// ConvertSeverity translates a kubeaudit.SeverityLevel into a +// scanner.IssueSeverityLevel, which standardizes severity levels across +// different scanners. +func ConvertSeverity(level kubeauditpkg.SeverityLevel) scanner.IssueSeverityLevel { + switch level { + case kubeauditpkg.Warn: + // Low severity corresponds to warnings in kubeaudit findings. + return scanner.Low + case kubeauditpkg.Error: + // High severity corresponds to errors in kubeaudit findings. + return scanner.High + default: + // Safe represents no security risk or an informational finding. + return scanner.Safe } } diff --git a/pkg/scanner/types.go b/pkg/scanner/types.go index 6146d8c3..3c99344e 100644 --- a/pkg/scanner/types.go +++ b/pkg/scanner/types.go @@ -25,13 +25,14 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -// IssueSeverityLevel defines the severity level of an issue. -// It is an enumeration starting from 0 (Low) and increases with severity. +// IssueSeverityLevel defines the severity levels for issues identified by +// scanners. const ( - Low IssueSeverityLevel = iota // Low indicates a minor issue that should be addressed. - Medium // Medium indicates a potential issue that may have a moderate impact. - High // High indicates a serious issue that has a significant impact. - Critical // Critical indicates an extremely serious issue that must be addressed immediately. + Safe IssueSeverityLevel = 0 // Safe indicates the absence of any security risk or an informational finding that does not require action. + Low IssueSeverityLevel = 1 // Low indicates a minor issue that should be addressed. + Medium IssueSeverityLevel = 2 // Medium indicates a potential issue that may have a moderate impact. + High IssueSeverityLevel = 3 // High indicates a serious issue that has a significant impact. + Critical IssueSeverityLevel = 5 // Critical indicates an extremely serious issue that must be addressed immediately. ) // KubeScanner is an interface for scanners that analyze Kubernetes resources. @@ -62,16 +63,18 @@ type IssueSeverityLevel int // String returns the string representation of the IssueSeverityLevel. func (s IssueSeverityLevel) String() string { switch s { + case Safe: + return "Safe" case Low: - return "Low" // Represents low severity level. + return "Low" case Medium: - return "Medium" // Represents medium severity level. + return "Medium" case High: - return "High" // Represents high severity level. + return "High" case Critical: - return "Critical" // Represents critical severity level. + return "Critical" default: - return "Unknown" // Indicates an unknown severity level. + return "Unknown" } }