Skip to content

Commit 55d0527

Browse files
authored
Merge pull request kubescape#113 from kubescape/attack-chains-engine
support in root to leaf attack chain calculation
2 parents 8e021e0 + 4f71eff commit 55d0527

File tree

6 files changed

+694
-12
lines changed

6 files changed

+694
-12
lines changed

reporthandling/attacktrack/v1alpha1/attacktrackmethods.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package v1alpha1
22

33
import (
4+
"reflect"
5+
46
"github.com/kubescape/k8s-interface/workloadinterface"
57
"go.uber.org/zap"
68
)
@@ -172,6 +174,160 @@ func (handler *AttackTrackAllPathsHandler) CalculateAllPaths() [][]IAttackTrackS
172174
return allPaths
173175
}
174176

177+
// CalculatePathsRootToLeaf calculates all paths from the root to the leaf nodes of the attack track
178+
// The paths are returned as a slice of slices of IAttackTrackStep
179+
// Each path is a slice of IAttackTrackStep
180+
// The first element of each path is the root of the attack track
181+
// The last element of each path is a leaf node
182+
// The paths are calculated using DFS
183+
184+
func (handler *AttackTrackAllPathsHandler) CalculatePathsRootToLeaf() [][]IAttackTrackStep {
185+
handler.visited = make(map[string]bool)
186+
var paths [][]IAttackTrackStep
187+
currentPath := []IAttackTrackStep{}
188+
189+
var traverse func(step IAttackTrackStep)
190+
traverse = func(step IAttackTrackStep) {
191+
if len(step.GetControls()) > 0 {
192+
currentPath = append(currentPath, step)
193+
}
194+
195+
if step.IsLeaf() {
196+
// Reached a leaf node
197+
if step.IsPartOfAttackTrackPath() {
198+
// Add current path to paths only if controls are not empty
199+
path := make([]IAttackTrackStep, len(currentPath))
200+
copy(path, currentPath)
201+
paths = append(paths, path)
202+
}
203+
} else {
204+
205+
// Traverse substeps recursively
206+
// Keep track of whether any substep with controls was traversed
207+
traversedSubstepWithControls := false
208+
for i := 0; i < step.Length(); i++ {
209+
subStep := step.SubStepAt(i)
210+
211+
// Check if the subStep has been visited before
212+
if !handler.visited[subStep.GetName()] {
213+
handler.visited[subStep.GetName()] = true
214+
215+
// Only include nodes with controls in the path
216+
if step.IsPartOfAttackTrackPath() {
217+
traverse(subStep)
218+
traversedSubstepWithControls = true
219+
} else {
220+
// Exclude the substep and its connected path
221+
handler.visited[subStep.GetName()] = false
222+
continue
223+
}
224+
}
225+
}
226+
227+
// Add the current path to paths only if it ends with a leaf node with controls
228+
if step.IsPartOfAttackTrackPath() && !traversedSubstepWithControls && step.IsLeaf() {
229+
path := make([]IAttackTrackStep, len(currentPath))
230+
copy(path, currentPath)
231+
paths = append(paths, path)
232+
}
233+
}
234+
235+
if step.IsPartOfAttackTrackPath() {
236+
currentPath = currentPath[:len(currentPath)-1] // Remove last step from current path in order to explore other paths
237+
}
238+
}
239+
240+
traverse(handler.attackTrack.GetData())
241+
242+
if len(paths) == 0 {
243+
return nil
244+
}
245+
246+
return paths
247+
}
248+
249+
// GenerateAttackTrackFromPaths - generates a new attack track from the given paths
250+
// The new attack track will contain only nodes that have controls
251+
func (handler *AttackTrackAllPathsHandler) GenerateAttackTrackFromPaths(paths [][]IAttackTrackStep) *AttackTrack {
252+
if len(paths) == 0 {
253+
return nil
254+
}
255+
data := handler.filterNodesWithControls(handler.attackTrack.GetData(), paths)
256+
// Create a new AttackTrack with only nodes that have controls
257+
updatedAttackTrack := AttackTrack{
258+
ApiVersion: handler.attackTrack.GetApiVersion(),
259+
Kind: handler.attackTrack.GetKind(),
260+
Metadata: map[string]interface{}{"name": handler.attackTrack.GetName()},
261+
Spec: AttackTrackSpecification{
262+
Version: handler.attackTrack.GetVersion(),
263+
Description: handler.attackTrack.GetDescription(),
264+
Data: *data,
265+
},
266+
}
267+
268+
return &updatedAttackTrack
269+
}
270+
271+
// filterNodesWithControls - filters out nodes that do not have controls
272+
func (handler *AttackTrackAllPathsHandler) filterNodesWithControls(step IAttackTrackStep, paths [][]IAttackTrackStep) *AttackTrackStep {
273+
filteredStep := AttackTrackStep{
274+
Name: step.GetName(),
275+
Description: step.GetDescription(),
276+
SubSteps: nil,
277+
Controls: step.GetControls(),
278+
}
279+
280+
if step.Length() == 0 {
281+
if len(step.GetControls()) > 0 {
282+
return &filteredStep
283+
}
284+
285+
return nil
286+
}
287+
288+
subSteps := make([]AttackTrackStep, 0, step.Length())
289+
290+
for i := 0; i < step.Length(); i++ {
291+
subStep := step.SubStepAt(i)
292+
293+
// Check if the subStep is present in any of the paths
294+
isInPath := false
295+
for _, path := range paths {
296+
if containsStep(path, subStep) {
297+
isInPath = true
298+
break
299+
}
300+
}
301+
302+
// Only include substeps that have controls or are present in a path
303+
if len(subStep.GetControls()) > 0 || isInPath {
304+
filteredSubStep := handler.filterNodesWithControls(subStep, paths)
305+
if filteredSubStep != nil {
306+
subSteps = append(subSteps, *filteredSubStep)
307+
}
308+
}
309+
}
310+
if len(subSteps) > 0 {
311+
filteredStep.SubSteps = subSteps
312+
}
313+
314+
if len(filteredStep.Controls) == 0 && len(filteredStep.SubSteps) == 0 {
315+
return nil
316+
}
317+
318+
return &filteredStep
319+
}
320+
321+
// containsStep - checks if the given step is present in the given path
322+
func containsStep(path []IAttackTrackStep, step IAttackTrackStep) bool {
323+
for _, s := range path {
324+
if reflect.DeepEqual(s, step) {
325+
return true
326+
}
327+
}
328+
return false
329+
}
330+
175331
func NewAttackTrackControlsLookup(attackTracks []IAttackTrack, failedControlIds []string, allControls map[string]IAttackTrackControl) AttackTrackControlsLookup {
176332
lookup := make(AttackTrackControlsLookup)
177333

0 commit comments

Comments
 (0)