Skip to content

Commit

Permalink
feat: support score api, refine severity part in audit pkg
Browse files Browse the repository at this point in the history
  • Loading branch information
elliotxx committed Dec 11, 2023
1 parent bc9d428 commit 5c45c7c
Show file tree
Hide file tree
Showing 10 changed files with 245 additions and 52 deletions.
79 changes: 55 additions & 24 deletions pkg/core/handler/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package audit

import (
"io"
"net/http"

"github.com/KusionStack/karbour/pkg/core/handler"
Expand Down Expand Up @@ -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.
Expand All @@ -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))
}
}
33 changes: 33 additions & 0 deletions pkg/core/handler/audit/util.go
Original file line number Diff line number Diff line change
@@ -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
}
35 changes: 34 additions & 1 deletion pkg/core/manager/audit/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
25 changes: 25 additions & 0 deletions pkg/core/manager/audit/types.go
Original file line number Diff line number Diff line change
@@ -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"`
}
18 changes: 18 additions & 0 deletions pkg/core/manager/audit/util.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions pkg/core/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ func setupAPIV1(

r.Route("/audit", func(r chi.Router) {
r.Post("/", audithandler.Audit(auditMgr))
r.Post("/score", audithandler.Score(auditMgr))
})
}

Expand Down
42 changes: 32 additions & 10 deletions pkg/scanner/kubeaudit/kubeaudit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/scanner/kubeaudit/kubeaudit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
37 changes: 32 additions & 5 deletions pkg/scanner/kubeaudit/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Loading

0 comments on commit 5c45c7c

Please sign in to comment.