Skip to content

Commit

Permalink
Merge pull request #102 from kubescape/compliance-score
Browse files Browse the repository at this point in the history
Add compliance score
  • Loading branch information
YiscahLevySilas1 authored Apr 3, 2023
2 parents 066863f + fb03e31 commit bfaae1e
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 6 deletions.
13 changes: 7 additions & 6 deletions reporthandling/results/v1/reportsummary/datastructures.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ type SummaryDetails struct {

// FrameworkSummary summary of scanning from a single framework perspective
type FrameworkSummary struct {
Controls ControlSummaries `json:"controls,omitempty"` // mapping of control - map[<control ID>]<control summary>
Name string `json:"name"` // framework name
Status apis.ScanningStatus `json:"status"`
Version string `json:"version"`
StatusCounters StatusCounters `json:"ResourceCounters"` // Backward compatibility
Score float32 `json:"score"`
Controls ControlSummaries `json:"controls,omitempty"` // mapping of control - map[<control ID>]<control summary>
Name string `json:"name"` // framework name
Status apis.ScanningStatus `json:"status"`
Version string `json:"version"`
StatusCounters StatusCounters `json:"ResourceCounters"` // Backward compatibility
Score float32 `json:"score"`
ComplianceScore float32 `json:"complianceScore"`
}

// ControlSummary summary of scanning from a single control perspective
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ func (frameworkSummary *FrameworkSummary) initResourcesSummary(controlInfoMap ma
frameworkSummary.CalculateStatus()
}

// =================================== ComplianceScore ============================================

// GetComplianceScore returns framework ComplianceScore
func (frameworkSummary *FrameworkSummary) GetComplianceScore() float32 {
return frameworkSummary.ComplianceScore
}

// =================================== Score ============================================

// GetScore return framework score
Expand Down
1 change: 1 addition & 0 deletions reporthandling/results/v1/reportsummary/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type IFrameworkSummary interface {
IPolicies
ListControls() []IControlSummary
NumberOfControls() ICounters
GetComplianceScore() float32
}

type IControlSummary interface {
Expand Down
85 changes: 85 additions & 0 deletions score/score.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"strings"
"sync"

"github.com/kubescape/go-logger"
"github.com/kubescape/go-logger/helpers"
k8sinterface "github.com/kubescape/k8s-interface/k8sinterface"
"github.com/kubescape/k8s-interface/workloadinterface"
armoupautils "github.com/kubescape/opa-utils/objectsenvelopes"
Expand Down Expand Up @@ -345,3 +347,86 @@ func (su ScoreUtil) debugf(format string, args ...any) {
func max32(a, b float32) float32 {
return float32(math.Max(float64(a), float64(b)))
}

// ==================================== Compliance Score ====================================

// SetPostureReportComplianceScores calculates and populates scores for all controls, frameworks and whole scan.
func (su *ScoreUtil) SetPostureReportComplianceScores(report *v2.PostureReport) error {
// call CalculatePostureReportV2 to set frameworks.score for backward compatibility
// afterwards we will override the controls.score and summeryDetails.score
// and set frameworks.complianceScore
// TODO: remove CalculatePostureReportV2 call after we deprecate frameworks.score
if err := su.CalculatePostureReportV2(report); err != nil {
return err
}
// set compliance score for each framework
for i := range report.SummaryDetails.Frameworks {
// set compliance score for framework and all controls in framework
report.SummaryDetails.Frameworks[i].ComplianceScore = su.GetFrameworkComplianceScore(&report.SummaryDetails.Frameworks[i])
logger.L().Debug("set framework score", helpers.String("framework name", report.SummaryDetails.Frameworks[i].GetName()), helpers.Int("ComplianceScore", int(report.SummaryDetails.Frameworks[i].GetComplianceScore())))
}
// set compliance score per control
sumScore := su.ControlsSummariesComplianceScore(&report.SummaryDetails.Controls, "")
// set compliance score for whole scan
summaryScore := float32(0)
if len(report.SummaryDetails.Controls) > 0 {
summaryScore = sumScore / float32(len(report.SummaryDetails.Controls))
}
report.SummaryDetails.Score = summaryScore
return nil
}

// ControlsSummariesComplianceScore sets the controls compliance score
// and returns the sum of all controls scores
func (su *ScoreUtil) ControlsSummariesComplianceScore(ctrls *reportsummary.ControlSummaries, frameworkName string) (sumScore float32) {
for ctrlID := range *ctrls {
ctrl := (*ctrls)[ctrlID]
ctrl.Score = 0
ctrl.Score = su.GetControlComplianceScore(&ctrl, frameworkName)
(*ctrls)[ctrlID] = ctrl
logger.L().Debug("set control score", helpers.String("controlID", ctrl.GetID()), helpers.Int("score", int(ctrl.GetScore())))
sumScore += ctrl.GetScore()
}
return sumScore
}

// GetFrameworkComplianceScore returns the compliance score for a given framework (as a percentage)
// The framework compliance score is the average of all controls scores in that framework
func (su *ScoreUtil) GetFrameworkComplianceScore(framework *reportsummary.FrameworkSummary) (frameworkScore float32) {
sumScore := su.ControlsSummariesComplianceScore(&framework.Controls, framework.GetName())
if len(framework.Controls) > 0 {
frameworkScore = sumScore / float32(len(framework.Controls))
}
return frameworkScore
}

// GetControlComplianceScore returns the compliance score for a given control (as a percentage).
func (su *ScoreUtil) GetControlComplianceScore(ctrl reportsummary.IControlSummary, _ /*frameworkName*/ string) (ctrlScore float32) {
resourcesIDs := ctrl.ListResourcesIDs()
passedResourceIDS := resourcesIDs.Passed()
allResourcesIDSIter := resourcesIDs.All()

numOfPassedResources := float32(0)
numOfAllResources := float32(0)

for i := range passedResourceIDS {
if _, ok := su.resources[passedResourceIDS[i]]; ok {
numOfPassedResources += 1
}
}

for allResourcesIDSIter.HasNext() {
resourceID := allResourcesIDSIter.Next()
if _, ok := su.resources[resourceID]; ok {
numOfAllResources += 1
}
}

if numOfAllResources > 0 {
ctrlScore = (numOfPassedResources / numOfAllResources) * 100
} else {
logger.L().Debug("no resources were given for this control, score is 0", helpers.String("controlID", ctrl.GetID()))
}

return ctrlScore
}
168 changes: 168 additions & 0 deletions score/score_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -783,3 +783,171 @@ func mockResources(t testing.TB) map[string]workloadinterface.IMetadata {
"resource-10": reporthandling.NewResource(mocks.GetResourceByType(t, "Pod", mocks.WithName("resource-10"))),
}
}

// ================================ compliance score tests ================================

func TestGetControlComplianceScore(t *testing.T) {
var resourceWithFailed, resourceWithPassed helpers.AllLists
resourceWithFailed.Append(apis.StatusFailed, "resource-1", "resource-2")
resourceWithFailed.Append(apis.StatusPassed, "resource-3")
resourceWithPassed.Append(apis.StatusPassed, "resource-4")

var resourceWithFailed2, resourceWithPassed2 helpers.AllLists
resourceWithFailed2.Append(apis.StatusFailed, "resource-5", "resource-6")
resourceWithFailed2.Append(apis.StatusPassed, "resource-7", "resource-8")
resourceWithPassed2.Append(apis.StatusPassed, "resource-9", "resource-10")
t.Parallel()

t.Run("with empty control report", func(t *testing.T) {
t.Parallel()

resources := mockResources(t)
s := ScoreUtil{isDebugMode: true, resources: resources}
controlReport := reportsummary.ControlSummary{
Name: "empty",
ControlID: "empty",
ResourceIDs: helpers.AllLists{},
}

require.Equal(t, float32(0), s.GetControlComplianceScore(&controlReport, ""),
"empty control report should return a score equals to 0",
)
})

t.Run("with control report", func(t *testing.T) {
t.Parallel()

resources := mockResources(t)
s := ScoreUtil{isDebugMode: true, resources: resources}
controlReport := reportsummary.ControlSummary{
Name: "mock-control-1",
ControlID: "mock-control-1",
ResourceIDs: resourceWithFailed2,
}

require.Equal(t, float32(50), s.GetControlComplianceScore(&controlReport, ""),
"control report should return a score equals to 50",
)
})
}

func TestSetPostureReportComplianceScores(t *testing.T) {
t.Parallel()

t.Run("with empty report", func(t *testing.T) {
t.Parallel()

s := NewScore(map[string]workloadinterface.IMetadata{})
report := &v2.PostureReport{
SummaryDetails: reportsummary.SummaryDetails{Frameworks: []reportsummary.FrameworkSummary{{Name: "empty", Controls: reportsummary.ControlSummaries{}}}},
Results: []resourcesresults.Result{},
Resources: []reporthandling.Resource{},
}

require.Errorf(t, s.SetPostureReportComplianceScores(report),
"empty framework should return an error",
)

require.Equal(t, float32(0), report.SummaryDetails.Frameworks[0].Score,
"empty framework should return an error and have a score equals to 0",
)
})

t.Run("with skipped report", func(t *testing.T) {
t.Parallel()

s := NewScore(map[string]workloadinterface.IMetadata{})
report := &v2.PostureReport{
SummaryDetails: reportsummary.SummaryDetails{Frameworks: []reportsummary.FrameworkSummary{{Name: "skipped", Controls: reportsummary.ControlSummaries{
"skipped1": reportsummary.ControlSummary{
Name: "skipped1",
ControlID: "Skippie1",
Description: "skipper",
},
"skipped2": reportsummary.ControlSummary{
Name: "skipped2",
ControlID: "Skippie2",
Description: "skipper",
},
}}}},
Results: []resourcesresults.Result{},
Resources: []reporthandling.Resource{},
}

require.Errorf(t, s.SetPostureReportComplianceScores(report),
"empty framework should return an error",
)

require.Equal(t, float32(0), report.SummaryDetails.Frameworks[0].Score,
"empty framework should return an error and have a score equals to 0",
)
})

t.Run("with mock report", func(t *testing.T) {
t.Parallel()

resources, report := mockPostureReportV2(t)
s := ScoreUtil{
isDebugMode: true,
resources: resources,
}

require.NoErrorf(t, s.SetPostureReportComplianceScores(report),
"mock framework should not return an error",
)

const (
expectedScoreFramework1 = float32(62.577965)
expectedScoreFramework2 = float32(46.42857)
expectedComplianceScoreFramework1 = float32(66.66667)
expectedComplianceScoreFramework2 = float32(75)
expectedSummary = float32(70.833336)
)

t.Run("assert control scores", func(t *testing.T) {
require.Len(t, report.SummaryDetails.Controls, 4)
for _, control := range report.SummaryDetails.Controls {
var expectedForControl float64

switch control.ControlID {
case "control-1":
expectedForControl = 33.333336
case "control-2":
expectedForControl = 100 // passed
case "control-3":
expectedForControl = 50
case "control-4":
expectedForControl = 100 // passed
}

assert.InDeltaf(t, expectedForControl, control.Score, 1e-6,
"unexpected summarized score for control %q", control.ControlID,
)
}
})

t.Run("assert framework scores", func(t *testing.T) {
assert.InDeltaf(t, expectedScoreFramework1, report.SummaryDetails.Frameworks[0].Score, 1e-6,
"unexpected summarized score for framework[0]",
)
assert.InDeltaf(t, expectedScoreFramework2, report.SummaryDetails.Frameworks[1].Score, 1e-6,
"unexpected summarized score for framework[1]",
)
})

t.Run("assert framework compliance scores", func(t *testing.T) {
assert.InDeltaf(t, expectedComplianceScoreFramework1, report.SummaryDetails.Frameworks[0].ComplianceScore, 1e-6,
"unexpected summarized compliance score for framework[0]",
)
assert.InDeltaf(t, expectedComplianceScoreFramework2, report.SummaryDetails.Frameworks[1].ComplianceScore, 1e-6,
"unexpected summarized compliance score for framework[1]",
)
})

t.Run("assert final score", func(t *testing.T) {
assert.InDeltaf(t, expectedSummary, report.SummaryDetails.Score, 1e-6,
"unexpected summarized final score",
)
})
})
}

0 comments on commit bfaae1e

Please sign in to comment.