Compare commits

..

2 Commits

Author SHA1 Message Date
dwertent
208bb25118 fixed junit support 2022-02-27 16:33:21 +02:00
dwertent
23e73f5e88 extent host-sensor support 2022-02-27 14:30:32 +02:00
18 changed files with 434 additions and 990 deletions

View File

@@ -108,7 +108,7 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser
| `--use-from` | | Load local framework object from specified path. If not used will download latest ||
| `--use-artifacts-from` | | Load artifacts (frameworks, control-config, exceptions) from local directory. If not used will download them | |
| `--use-default` | `false` | Load local framework object from default path. If not used will download latest | `true`/`false` |
| `--exceptions` | | Path to an exceptions obj, [examples](examples/exceptions/README.md). Default will download exceptions from Kubescape SaaS ||
| `--exceptions` | | Path to an exceptions obj, [examples](https://github.com/armosec/kubescape/tree/master/examples/exceptions/README.md). Default will download exceptions from Kubescape SaaS ||
| `--controls-config` | | Path to a controls-config obj. If not set will download controls-config from ARMO management portal | |
| `--submit` | `false` | If set, Kubescape will send the scan results to Armo management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not sent | `true`/`false` |
| `--keep-local` | `false` | Kubescape will not send scan results to Armo management portal. Use this flag if you ran with the `--submit` flag in the past and you do not want to submit your current scan results | `true`/`false` |

239
cautils/fileutils.go Normal file
View File

@@ -0,0 +1,239 @@
package cautils
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/armosec/k8s-interface/workloadinterface"
"github.com/armosec/kubescape/cautils/logger"
"github.com/armosec/opa-utils/objectsenvelopes"
"gopkg.in/yaml.v2"
)
var (
YAML_PREFIX = []string{".yaml", ".yml"}
JSON_PREFIX = []string{".json"}
)
type FileFormat string
const (
YAML_FILE_FORMAT FileFormat = "yaml"
JSON_FILE_FORMAT FileFormat = "json"
)
func LoadResourcesFromFiles(inputPatterns []string) ([]workloadinterface.IMetadata, error) {
files, errs := listFiles(inputPatterns)
if len(errs) > 0 {
logger.L().Error(fmt.Sprintf("%v", errs))
}
if len(files) == 0 {
return nil, nil
}
workloads, errs := loadFiles(files)
if len(errs) > 0 {
logger.L().Error(fmt.Sprintf("%v", errs))
}
return workloads, nil
}
func loadFiles(filePaths []string) ([]workloadinterface.IMetadata, []error) {
workloads := []workloadinterface.IMetadata{}
errs := []error{}
for i := range filePaths {
f, err := loadFile(filePaths[i])
if err != nil {
errs = append(errs, err)
continue
}
w, e := ReadFile(f, GetFileFormat(filePaths[i]))
errs = append(errs, e...)
if w != nil {
workloads = append(workloads, w...)
}
}
return workloads, errs
}
func loadFile(filePath string) ([]byte, error) {
return os.ReadFile(filePath)
}
func ReadFile(fileContent []byte, fileFromat FileFormat) ([]workloadinterface.IMetadata, []error) {
switch fileFromat {
case YAML_FILE_FORMAT:
return readYamlFile(fileContent)
case JSON_FILE_FORMAT:
return readJsonFile(fileContent)
default:
return nil, nil // []error{fmt.Errorf("file extension %s not supported", fileFromat)}
}
}
func listFiles(patterns []string) ([]string, []error) {
files := []string{}
errs := []error{}
for i := range patterns {
if strings.HasPrefix(patterns[i], "http") {
continue
}
if !filepath.IsAbs(patterns[i]) {
o, _ := os.Getwd()
patterns[i] = filepath.Join(o, patterns[i])
}
if IsFile(patterns[i]) {
files = append(files, patterns[i])
} else {
f, err := glob(filepath.Split(patterns[i])) //filepath.Glob(patterns[i])
if err != nil {
errs = append(errs, err)
} else {
files = append(files, f...)
}
}
}
return files, errs
}
func readYamlFile(yamlFile []byte) ([]workloadinterface.IMetadata, []error) {
errs := []error{}
r := bytes.NewReader(yamlFile)
dec := yaml.NewDecoder(r)
yamlObjs := []workloadinterface.IMetadata{}
var t interface{}
for dec.Decode(&t) == nil {
j := convertYamlToJson(t)
if j == nil {
continue
}
if obj, ok := j.(map[string]interface{}); ok {
if o := objectsenvelopes.NewObject(obj); o != nil {
if o.GetKind() == "List" {
yamlObjs = append(yamlObjs, handleListObject(o)...)
} else {
yamlObjs = append(yamlObjs, o)
}
}
} else {
errs = append(errs, fmt.Errorf("failed to convert yaml file to map[string]interface, file content: %v", j))
}
}
return yamlObjs, errs
}
func readJsonFile(jsonFile []byte) ([]workloadinterface.IMetadata, []error) {
workloads := []workloadinterface.IMetadata{}
var jsonObj interface{}
if err := json.Unmarshal(jsonFile, &jsonObj); err != nil {
return workloads, []error{err}
}
convertJsonToWorkload(jsonObj, &workloads)
return workloads, nil
}
func convertJsonToWorkload(jsonObj interface{}, workloads *[]workloadinterface.IMetadata) {
switch x := jsonObj.(type) {
case map[string]interface{}:
if o := objectsenvelopes.NewObject(x); o != nil {
(*workloads) = append(*workloads, o)
}
case []interface{}:
for i := range x {
convertJsonToWorkload(x[i], workloads)
}
}
}
func convertYamlToJson(i interface{}) interface{} {
switch x := i.(type) {
case map[interface{}]interface{}:
m2 := map[string]interface{}{}
for k, v := range x {
if s, ok := k.(string); ok {
m2[s] = convertYamlToJson(v)
}
}
return m2
case []interface{}:
for i, v := range x {
x[i] = convertYamlToJson(v)
}
}
return i
}
func IsYaml(filePath string) bool {
return StringInSlice(YAML_PREFIX, filepath.Ext(filePath)) != ValueNotFound
}
func IsJson(filePath string) bool {
return StringInSlice(JSON_PREFIX, filepath.Ext(filePath)) != ValueNotFound
}
func glob(root, pattern string) ([]string, error) {
var matches []string
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if matched, err := filepath.Match(pattern, filepath.Base(path)); err != nil {
return err
} else if matched {
matches = append(matches, path)
}
return nil
})
if err != nil {
return nil, err
}
return matches, nil
}
func IsFile(name string) bool {
if fi, err := os.Stat(name); err == nil {
if fi.Mode().IsRegular() {
return true
}
}
return false
}
func GetFileFormat(filePath string) FileFormat {
if IsYaml(filePath) {
return YAML_FILE_FORMAT
} else if IsJson(filePath) {
return JSON_FILE_FORMAT
} else {
return FileFormat(filePath)
}
}
// handleListObject handle a List manifest
func handleListObject(obj workloadinterface.IMetadata) []workloadinterface.IMetadata {
yamlObjs := []workloadinterface.IMetadata{}
if i, ok := workloadinterface.InspectMap(obj.GetObject(), "items"); ok && i != nil {
if items, ok := i.([]interface{}); ok && items != nil {
for item := range items {
if m, ok := items[item].(map[string]interface{}); ok && m != nil {
if o := objectsenvelopes.NewObject(m); o != nil {
yamlObjs = append(yamlObjs, o)
}
}
}
}
}
return yamlObjs
}

View File

@@ -1,4 +1,4 @@
package resourcehandler
package cautils
import (
"os"

View File

@@ -86,7 +86,7 @@ func init() {
scanCmd.PersistentFlags().MarkHidden("silent") // this flag should be deprecated since we added the --logger support
scanCmd.PersistentFlags().MarkHidden("output-version") // meant for testing different output approaches and not for common use
hostF := scanCmd.PersistentFlags().VarPF(&scanInfo.HostSensorEnabled, "enable-host-scan", "", "Deploy ARMO K8s host-sensor daemonset in the scanned cluster. Deleting it right after we collecting the data. Required to collect valueable data from cluster nodes for certain controls")
hostF := scanCmd.PersistentFlags().VarPF(&scanInfo.HostSensorEnabled, "enable-host-scan", "", "Deploy ARMO K8s host-sensor daemonset in the scanned cluster. Deleting it right after we collecting the data. Required to collect valueable data from cluster nodes for certain controls. Yaml file: https://raw.githubusercontent.com/armosec/kubescape/master/hostsensorutils/hostsensor.yaml")
hostF.NoOptDefVal = "true"
hostF.DefValue = "false, for no TTY in stdin"

View File

@@ -19,10 +19,10 @@ e.g. When a `kube-system` resource fails and it is ok, simply add the resource t
* `cluster`: k8s cluster name (usually it is the `current-context`) (case-sensitive, regex supported)
* resource labels as key value (case-sensitive, regex NOT supported)
* `posturePolicies`- An attribute-based declaration {key: value}
* `frameworkName` - Framework names can be find [here](https://github.com/armosec/regolibrary/tree/master/frameworks)
* `controlName` - Control names can be find [here](https://github.com/armosec/regolibrary/tree/master/controls)
* `controlID` - Not yet supported
* `ruleName` - Rule names can be find [here](https://github.com/armosec/regolibrary/tree/master/rules)
* `frameworkName` - Framework names can be find [here](https://github.com/armosec/regolibrary/tree/master/frameworks) (regex supported)
* `controlName` - Control names can be find [here](https://github.com/armosec/regolibrary/tree/master/controls) (regex supported)
* `controlID` - Control ID can be find [here](https://github.com/armosec/regolibrary/tree/master/controls) (regex supported)
* `ruleName` - Rule names can be find [here](https://github.com/armosec/regolibrary/tree/master/rules) (regex supported)
## Usage
@@ -92,7 +92,7 @@ Here are some examples demonstrating the different ways the exceptions file can
### Exclude control
Exclude the ["Allowed hostPath" control](https://github.com/armosec/regolibrary/blob/master/controls/allowedhostpath.json#L2) by declaring the control in the `"posturePolicies"` section.
Exclude the [C-0060 control](https://github.com/armosec/regolibrary/blob/master/controls/allowedhostpath.json#L2) by declaring the control ID in the `"posturePolicies"` section.
The resources
@@ -114,7 +114,7 @@ The resources
],
"posturePolicies": [
{
"controlName": "Allowed hostPath"
"controlID": "C-0060"
}
]
}

View File

@@ -2,29 +2,31 @@ apiVersion: v1
kind: Namespace
metadata:
labels:
app: host-sensor
kubernetes.io/metadata.name: armo-kube-host-sensor
tier: armo-kube-host-sensor-control-plane
name: armo-kube-host-sensor
app: kubescape-host-scanner
k8s-app: kubescape-host-scanner
kubernetes.io/metadata.name: kubescape-host-scanner
tier: kubescape-host-scanner-control-plane
name: kubescape-host-scanner
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: host-sensor
namespace: armo-kube-host-sensor
name: host-scanner
namespace: kubescape-host-scanner
labels:
k8s-app: armo-kube-host-sensor
app: host-scanner
k8s-app: kubescape-host-scanner
spec:
selector:
matchLabels:
name: host-sensor
name: host-scanner
template:
metadata:
labels:
name: host-sensor
name: host-scanner
spec:
tolerations:
# this toleration is to have the daemonset runnable on master nodes
# this toleration is to have the DaemonDet runnable on master nodes
# remove it if your masters can't run pods
- key: node-role.kubernetes.io/master
operator: Exists
@@ -37,7 +39,7 @@ spec:
readOnlyRootFilesystem: true
procMount: Unmasked
ports:
- name: http
- name: scanner # Do not change port name
hostPort: 7888
containerPort: 7888
protocol: TCP

View File

@@ -2,24 +2,21 @@ package hostsensorutils
import (
_ "embed"
"encoding/json"
"fmt"
"io"
"os"
"strings"
"sync"
"time"
"github.com/armosec/k8s-interface/k8sinterface"
"github.com/armosec/k8s-interface/workloadinterface"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/logger"
"github.com/armosec/kubescape/cautils/logger/helpers"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/apimachinery/pkg/watch"
appsapplyv1 "k8s.io/client-go/applyconfigurations/apps/v1"
coreapplyv1 "k8s.io/client-go/applyconfigurations/core/v1"
)
var (
@@ -27,15 +24,17 @@ var (
hostSensorYAML string
)
const PortName string = "scanner"
type HostSensorHandler struct {
HostSensorPort int32
HostSensorPodNames map[string]string //map from pod names to node names
HostSensorUnshedulePodNames map[string]string //map from pod names to node names
IsReady <-chan bool //readonly chan
k8sObj *k8sinterface.KubernetesApi
DaemonSet *appsv1.DaemonSet
podListLock sync.RWMutex
gracePeriod int64
HostSensorPort int32
HostSensorPodNames map[string]string //map from pod names to node names
HostSensorUnscheduledPodNames map[string]string //map from pod names to node names
IsReady <-chan bool //readonly chan
k8sObj *k8sinterface.KubernetesApi
DaemonSet *appsv1.DaemonSet
podListLock sync.RWMutex
gracePeriod int64
}
func NewHostSensorHandler(k8sObj *k8sinterface.KubernetesApi, hostSensorYAMLFile string) (*HostSensorHandler, error) {
@@ -51,10 +50,10 @@ func NewHostSensorHandler(k8sObj *k8sinterface.KubernetesApi, hostSensorYAMLFile
hostSensorYAML = d
}
hsh := &HostSensorHandler{
k8sObj: k8sObj,
HostSensorPodNames: map[string]string{},
HostSensorUnshedulePodNames: map[string]string{},
gracePeriod: int64(15),
k8sObj: k8sObj,
HostSensorPodNames: map[string]string{},
HostSensorUnscheduledPodNames: map[string]string{},
gracePeriod: int64(15),
}
// Don't deploy on cluster with no nodes. Some cloud providers prevents termination of K8s objects for cluster with no nodes!!!
if nodeList, err := k8sObj.KubernetesClient.CoreV1().Nodes().List(k8sObj.Context, metav1.ListOptions{}); err != nil || len(nodeList.Items) == 0 {
@@ -78,7 +77,7 @@ func (hsh *HostSensorHandler) Init() error {
defer cautils.StopSpinner()
if err := hsh.applyYAML(); err != nil {
return fmt.Errorf("in HostSensorHandler init failed to apply YAML: %v", err)
return fmt.Errorf("failed to apply host sensor YAML, reason: %v", err)
}
hsh.populatePodNamesToNodeNames()
if err := hsh.checkPodForEachNode(); err != nil {
@@ -88,55 +87,83 @@ func (hsh *HostSensorHandler) Init() error {
}
func (hsh *HostSensorHandler) applyYAML() error {
dec := yaml.NewDocumentDecoder(io.NopCloser(strings.NewReader(hostSensorYAML)))
// apply namespace
singleYAMLBytes := make([]byte, 4096)
if readLen, err := dec.Read(singleYAMLBytes); err != nil {
return fmt.Errorf("failed to read YAML of namespace: %v", err)
} else {
singleYAMLBytes = singleYAMLBytes[:readLen]
workloads, err := cautils.ReadFile([]byte(hostSensorYAML), cautils.YAML_FILE_FORMAT)
if len(err) != 0 {
return fmt.Errorf("failed to read YAML files, reason: %v", err)
}
namespaceAC := &coreapplyv1.NamespaceApplyConfiguration{}
if err := yaml.Unmarshal(singleYAMLBytes, namespaceAC); err != nil {
return fmt.Errorf("failed to Unmarshal YAML of namespace: %v", err)
}
namespaceName := ""
if ns, err := hsh.k8sObj.KubernetesClient.CoreV1().Namespaces().Apply(hsh.k8sObj.Context, namespaceAC, metav1.ApplyOptions{
FieldManager: "kubescape",
}); err != nil {
return fmt.Errorf("failed to apply YAML of namespace: %v", err)
} else {
namespaceName = ns.Name
}
// apply DaemonSet
daemonAC := &appsapplyv1.DaemonSetApplyConfiguration{}
singleYAMLBytes = make([]byte, 4096)
if readLen, err := dec.Read(singleYAMLBytes); err != nil {
if erra := hsh.tearDownNamesapce(namespaceName); erra != nil {
err = fmt.Errorf("%v; In addidtion %v", err, erra)
// Get namespace name
namespaceName := ""
for i := range workloads {
if workloads[i].GetKind() == "Namespace" {
namespaceName = workloads[i].GetName()
break
}
return fmt.Errorf("failed to read YAML of DaemonSet: %v", err)
} else {
singleYAMLBytes = singleYAMLBytes[:readLen]
}
if err := yaml.Unmarshal(singleYAMLBytes, daemonAC); err != nil {
if erra := hsh.tearDownNamesapce(namespaceName); erra != nil {
err = fmt.Errorf("%v; In addidtion %v", err, erra)
// Update workload data before applying
for i := range workloads {
w := workloadinterface.NewWorkloadObj(workloads[i].GetObject())
if w == nil {
return fmt.Errorf("invalid workload: %v", workloads[i].GetObject())
}
return fmt.Errorf("failed to Unmarshal YAML of DaemonSet: %v", err)
}
daemonAC.Namespace = &namespaceName
if ds, err := hsh.k8sObj.KubernetesClient.AppsV1().DaemonSets(namespaceName).Apply(hsh.k8sObj.Context, daemonAC, metav1.ApplyOptions{
FieldManager: "kubescape",
}); err != nil {
if erra := hsh.tearDownNamesapce(namespaceName); erra != nil {
err = fmt.Errorf("%v; In addidtion %v", err, erra)
// set namespace in all objects
if w.GetKind() != "Namespace" {
w.SetNamespace(namespaceName)
}
// Get container port
if w.GetKind() == "DaemonSet" {
containers, err := w.GetContainers()
if err != nil {
if erra := hsh.tearDownNamespace(namespaceName); erra != nil {
logger.L().Warning("failed to tear down namespace", helpers.Error(erra))
}
return fmt.Errorf("container not found in DaemonSet: %v", err)
}
for j := range containers {
for k := range containers[j].Ports {
if containers[j].Ports[k].Name == PortName {
hsh.HostSensorPort = containers[j].Ports[k].ContainerPort
}
}
}
}
// Apply workload
var newWorkload k8sinterface.IWorkload
var e error
if g, err := hsh.k8sObj.GetWorkload(w.GetNamespace(), w.GetKind(), w.GetName()); err == nil && g != nil {
newWorkload, e = hsh.k8sObj.UpdateWorkload(w)
} else {
newWorkload, e = hsh.k8sObj.CreateWorkload(w)
}
if e != nil {
if erra := hsh.tearDownNamespace(namespaceName); erra != nil {
logger.L().Warning("failed to tear down namespace", helpers.Error(erra))
}
return fmt.Errorf("failed to create/update YAML, reason: %v", e)
}
// Save DaemonSet
if newWorkload.GetKind() == "DaemonSet" {
b, err := json.Marshal(newWorkload.GetObject())
if err != nil {
if erra := hsh.tearDownNamespace(namespaceName); erra != nil {
logger.L().Warning("failed to tear down namespace", helpers.Error(erra))
}
return fmt.Errorf("failed to Marshal YAML of DaemonSet, reason: %v", err)
}
var ds appsv1.DaemonSet
if err := json.Unmarshal(b, &ds); err != nil {
if erra := hsh.tearDownNamespace(namespaceName); erra != nil {
logger.L().Warning("failed to tear down namespace", helpers.Error(erra))
}
return fmt.Errorf("failed to Unmarshal YAML of DaemonSet, reason: %v", err)
}
hsh.DaemonSet = &ds
}
return fmt.Errorf("failed to apply YAML of DaemonSet: %v", err)
} else {
hsh.HostSensorPort = ds.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort
hsh.DaemonSet = ds
}
return nil
}
@@ -150,7 +177,7 @@ func (hsh *HostSensorHandler) checkPodForEachNode() error {
}
hsh.podListLock.RLock()
podsNum := len(hsh.HostSensorPodNames)
unschedPodNum := len(hsh.HostSensorUnshedulePodNames)
unschedPodNum := len(hsh.HostSensorUnscheduledPodNames)
hsh.podListLock.RUnlock()
if len(nodesList.Items) <= podsNum+unschedPodNum {
break
@@ -202,7 +229,7 @@ func (hsh *HostSensorHandler) updatePodInListAtomic(eventType watch.EventType, p
if podObj.Status.Phase == corev1.PodRunning && len(podObj.Status.ContainerStatuses) > 0 &&
podObj.Status.ContainerStatuses[0].Ready {
hsh.HostSensorPodNames[podObj.ObjectMeta.Name] = podObj.Spec.NodeName
delete(hsh.HostSensorUnshedulePodNames, podObj.ObjectMeta.Name)
delete(hsh.HostSensorUnscheduledPodNames, podObj.ObjectMeta.Name)
} else {
if podObj.Status.Phase == corev1.PodPending && len(podObj.Status.Conditions) > 0 &&
podObj.Status.Conditions[0].Reason == corev1.PodReasonUnschedulable {
@@ -219,7 +246,7 @@ func (hsh *HostSensorHandler) updatePodInListAtomic(eventType watch.EventType, p
helpers.String("nodeName", nodeName),
helpers.String("podName", podObj.ObjectMeta.Name))
if nodeName != "" {
hsh.HostSensorUnshedulePodNames[podObj.ObjectMeta.Name] = nodeName
hsh.HostSensorUnscheduledPodNames[podObj.ObjectMeta.Name] = nodeName
}
} else {
delete(hsh.HostSensorPodNames, podObj.ObjectMeta.Name)
@@ -230,7 +257,7 @@ func (hsh *HostSensorHandler) updatePodInListAtomic(eventType watch.EventType, p
}
}
func (hsh *HostSensorHandler) tearDownNamesapce(namespace string) error {
func (hsh *HostSensorHandler) tearDownNamespace(namespace string) error {
if err := hsh.k8sObj.KubernetesClient.CoreV1().Namespaces().Delete(hsh.k8sObj.Context, namespace, metav1.DeleteOptions{GracePeriodSeconds: &hsh.gracePeriod}); err != nil {
return fmt.Errorf("failed to delete host-sensor namespace: %v", err)
@@ -243,7 +270,7 @@ func (hsh *HostSensorHandler) TearDown() error {
if err := hsh.k8sObj.KubernetesClient.AppsV1().DaemonSets(hsh.GetNamespace()).Delete(hsh.k8sObj.Context, hsh.DaemonSet.Name, metav1.DeleteOptions{GracePeriodSeconds: &hsh.gracePeriod}); err != nil {
return fmt.Errorf("failed to delete host-sensor daemonset: %v", err)
}
if err := hsh.tearDownNamesapce(namespace); err != nil {
if err := hsh.tearDownNamespace(namespace); err != nil {
return fmt.Errorf("failed to delete host-sensor daemonset: %v", err)
}
// TODO: wait for termination? may take up to 120 seconds!!!

View File

@@ -1,12 +1,8 @@
package resourcehandler
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/armosec/armoapi-go/armotypes"
"github.com/armosec/k8s-interface/workloadinterface"
@@ -14,23 +10,7 @@ import (
"github.com/armosec/k8s-interface/k8sinterface"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/logger"
"github.com/armosec/opa-utils/objectsenvelopes"
"github.com/armosec/opa-utils/reporthandling"
"gopkg.in/yaml.v2"
)
var (
YAML_PREFIX = []string{".yaml", ".yml"}
JSON_PREFIX = []string{".json"}
)
type FileFormat string
const (
YAML_FILE_FORMAT FileFormat = "yaml"
JSON_FILE_FORMAT FileFormat = "json"
)
// FileResourceHandler handle resources from files and URLs
@@ -57,7 +37,7 @@ func (fileHandler *FileResourceHandler) GetResources(frameworks []reporthandling
workloads := []workloadinterface.IMetadata{}
// load resource from local file system
w, err := loadResourcesFromFiles(fileHandler.inputPatterns)
w, err := cautils.LoadResourcesFromFiles(fileHandler.inputPatterns)
if err != nil {
return nil, allResources, err
}
@@ -105,22 +85,6 @@ func (fileHandler *FileResourceHandler) GetClusterAPIServerInfo() *version.Info
return nil
}
func loadResourcesFromFiles(inputPatterns []string) ([]workloadinterface.IMetadata, error) {
files, errs := listFiles(inputPatterns)
if len(errs) > 0 {
logger.L().Error(fmt.Sprintf("%v", errs))
}
if len(files) == 0 {
return nil, nil
}
workloads, errs := loadFiles(files)
if len(errs) > 0 {
logger.L().Error(fmt.Sprintf("%v", errs))
}
return workloads, nil
}
// build resources map
func mapResources(workloads []workloadinterface.IMetadata) map[string][]workloadinterface.IMetadata {
@@ -149,199 +113,3 @@ func mapResources(workloads []workloadinterface.IMetadata) map[string][]workload
return allResources
}
func loadFiles(filePaths []string) ([]workloadinterface.IMetadata, []error) {
workloads := []workloadinterface.IMetadata{}
errs := []error{}
for i := range filePaths {
f, err := loadFile(filePaths[i])
if err != nil {
errs = append(errs, err)
continue
}
w, e := readFile(f, getFileFormat(filePaths[i]))
errs = append(errs, e...)
if w != nil {
workloads = append(workloads, w...)
}
}
return workloads, errs
}
func loadFile(filePath string) ([]byte, error) {
return os.ReadFile(filePath)
}
func readFile(fileContent []byte, fileFromat FileFormat) ([]workloadinterface.IMetadata, []error) {
switch fileFromat {
case YAML_FILE_FORMAT:
return readYamlFile(fileContent)
case JSON_FILE_FORMAT:
return readJsonFile(fileContent)
default:
return nil, nil // []error{fmt.Errorf("file extension %s not supported", fileFromat)}
}
}
func listFiles(patterns []string) ([]string, []error) {
files := []string{}
errs := []error{}
for i := range patterns {
if strings.HasPrefix(patterns[i], "http") {
continue
}
if !filepath.IsAbs(patterns[i]) {
o, _ := os.Getwd()
patterns[i] = filepath.Join(o, patterns[i])
}
if isFile(patterns[i]) {
files = append(files, patterns[i])
} else {
f, err := glob(filepath.Split(patterns[i])) //filepath.Glob(patterns[i])
if err != nil {
errs = append(errs, err)
} else {
files = append(files, f...)
}
}
}
return files, errs
}
func readYamlFile(yamlFile []byte) ([]workloadinterface.IMetadata, []error) {
errs := []error{}
r := bytes.NewReader(yamlFile)
dec := yaml.NewDecoder(r)
yamlObjs := []workloadinterface.IMetadata{}
var t interface{}
for dec.Decode(&t) == nil {
j := convertYamlToJson(t)
if j == nil {
continue
}
if obj, ok := j.(map[string]interface{}); ok {
if o := objectsenvelopes.NewObject(obj); o != nil {
if o.GetKind() == "List" {
yamlObjs = append(yamlObjs, handleListObject(o)...)
} else {
yamlObjs = append(yamlObjs, o)
}
}
} else {
errs = append(errs, fmt.Errorf("failed to convert yaml file to map[string]interface, file content: %v", j))
}
}
return yamlObjs, errs
}
func readJsonFile(jsonFile []byte) ([]workloadinterface.IMetadata, []error) {
workloads := []workloadinterface.IMetadata{}
var jsonObj interface{}
if err := json.Unmarshal(jsonFile, &jsonObj); err != nil {
return workloads, []error{err}
}
convertJsonToWorkload(jsonObj, &workloads)
return workloads, nil
}
func convertJsonToWorkload(jsonObj interface{}, workloads *[]workloadinterface.IMetadata) {
switch x := jsonObj.(type) {
case map[string]interface{}:
if o := objectsenvelopes.NewObject(x); o != nil {
(*workloads) = append(*workloads, o)
}
case []interface{}:
for i := range x {
convertJsonToWorkload(x[i], workloads)
}
}
}
func convertYamlToJson(i interface{}) interface{} {
switch x := i.(type) {
case map[interface{}]interface{}:
m2 := map[string]interface{}{}
for k, v := range x {
if s, ok := k.(string); ok {
m2[s] = convertYamlToJson(v)
}
}
return m2
case []interface{}:
for i, v := range x {
x[i] = convertYamlToJson(v)
}
}
return i
}
func isYaml(filePath string) bool {
return cautils.StringInSlice(YAML_PREFIX, filepath.Ext(filePath)) != cautils.ValueNotFound
}
func isJson(filePath string) bool {
return cautils.StringInSlice(JSON_PREFIX, filepath.Ext(filePath)) != cautils.ValueNotFound
}
func glob(root, pattern string) ([]string, error) {
var matches []string
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if matched, err := filepath.Match(pattern, filepath.Base(path)); err != nil {
return err
} else if matched {
matches = append(matches, path)
}
return nil
})
if err != nil {
return nil, err
}
return matches, nil
}
func isFile(name string) bool {
if fi, err := os.Stat(name); err == nil {
if fi.Mode().IsRegular() {
return true
}
}
return false
}
func getFileFormat(filePath string) FileFormat {
if isYaml(filePath) {
return YAML_FILE_FORMAT
} else if isJson(filePath) {
return JSON_FILE_FORMAT
} else {
return FileFormat(filePath)
}
}
// handleListObject handle a List manifest
func handleListObject(obj workloadinterface.IMetadata) []workloadinterface.IMetadata {
yamlObjs := []workloadinterface.IMetadata{}
if i, ok := workloadinterface.InspectMap(obj.GetObject(), "items"); ok && i != nil {
if items, ok := i.([]interface{}); ok && items != nil {
for item := range items {
if m, ok := items[item].(map[string]interface{}); ok && m != nil {
if o := objectsenvelopes.NewObject(m); o != nil {
yamlObjs = append(yamlObjs, o)
}
}
}
}
}
return yamlObjs
}

View File

@@ -8,6 +8,7 @@ import (
"strings"
"github.com/armosec/k8s-interface/workloadinterface"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/logger"
)
@@ -28,7 +29,7 @@ func listUrls(patterns []string) []string {
urls := []string{}
for i := range patterns {
if strings.HasPrefix(patterns[i], "http") {
if !isYaml(patterns[i]) && !isJson(patterns[i]) { // if url of repo
if !cautils.IsYaml(patterns[i]) && !cautils.IsJson(patterns[i]) { // if url of repo
if yamls, err := ScanRepository(patterns[i], ""); err == nil { // TODO - support branch
urls = append(urls, yamls...)
} else {
@@ -52,7 +53,7 @@ func downloadFiles(urls []string) ([]workloadinterface.IMetadata, []error) {
errs = append(errs, err)
continue
}
w, e := readFile(f, getFileFormat(urls[i]))
w, e := cautils.ReadFile(f, cautils.GetFileFormat(urls[i]))
errs = append(errs, e...)
if w != nil {
workloads = append(workloads, w...)

View File

@@ -1,125 +0,0 @@
package v1
import (
"encoding/xml"
"fmt"
"os"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/logger"
"github.com/armosec/kubescape/resultshandling/printer"
"github.com/armosec/opa-utils/reporthandling"
)
type JunitPrinter struct {
writer *os.File
}
func NewJunitPrinter() *JunitPrinter {
return &JunitPrinter{}
}
func (junitPrinter *JunitPrinter) SetWriter(outputFile string) {
junitPrinter.writer = printer.GetWriter(outputFile)
}
func (junitPrinter *JunitPrinter) Score(score float32) {
fmt.Fprintf(os.Stderr, "\nOverall risk-score (0- Excellent, 100- All failed): %d\n", int(score))
}
func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
cautils.ReportV2ToV1(opaSessionObj)
junitResult, err := convertPostureReportToJunitResult(opaSessionObj.PostureReport)
if err != nil {
logger.L().Fatal("failed to convert posture report object")
}
postureReportStr, err := xml.Marshal(junitResult)
if err != nil {
logger.L().Fatal("failed to convert posture report object")
}
junitPrinter.writer.Write(postureReportStr)
}
type JUnitTestSuites struct {
XMLName xml.Name `xml:"testsuites"`
Suites []JUnitTestSuite `xml:"testsuite"`
}
// JUnitTestSuite is a single JUnit test suite which may contain many
// testcases.
type JUnitTestSuite struct {
XMLName xml.Name `xml:"testsuite"`
Tests int `xml:"tests,attr"`
Time string `xml:"time,attr"`
Name string `xml:"name,attr"`
Resources int `xml:"resources,attr"`
Excluded int `xml:"excluded,attr"`
Failed int `xml:"filed,attr"`
Properties []JUnitProperty `xml:"properties>property,omitempty"`
TestCases []JUnitTestCase `xml:"testcase"`
}
// JUnitTestCase is a single test case with its result.
type JUnitTestCase struct {
XMLName xml.Name `xml:"testcase"`
Classname string `xml:"classname,attr"`
Name string `xml:"name,attr"`
Time string `xml:"time,attr"`
Resources int `xml:"resources,attr"`
Excluded int `xml:"excluded,attr"`
Failed int `xml:"filed,attr"`
SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"`
Failure *JUnitFailure `xml:"failure,omitempty"`
}
// JUnitSkipMessage contains the reason why a testcase was skipped.
type JUnitSkipMessage struct {
Message string `xml:"message,attr"`
}
// JUnitProperty represents a key/value pair used to define properties.
type JUnitProperty struct {
Name string `xml:"name,attr"`
Value string `xml:"value,attr"`
}
// JUnitFailure contains data related to a failed test.
type JUnitFailure struct {
Message string `xml:"message,attr"`
Type string `xml:"type,attr"`
Contents string `xml:",chardata"`
}
func convertPostureReportToJunitResult(postureResult *reporthandling.PostureReport) (*JUnitTestSuites, error) {
juResult := JUnitTestSuites{XMLName: xml.Name{Local: "Kubescape scan results"}}
for _, framework := range postureResult.FrameworkReports {
suite := JUnitTestSuite{
Name: framework.Name,
Resources: framework.GetNumberOfResources(),
Excluded: framework.GetNumberOfWarningResources(),
Failed: framework.GetNumberOfFailedResources(),
}
for _, controlReports := range framework.ControlReports {
suite.Tests = suite.Tests + 1
testCase := JUnitTestCase{}
testCase.Name = controlReports.Name
testCase.Classname = "Kubescape"
testCase.Time = postureResult.ReportGenerationTime.String()
if 0 < len(controlReports.RuleReports) && 0 < len(controlReports.RuleReports[0].RuleResponses) {
testCase.Resources = controlReports.GetNumberOfResources()
testCase.Excluded = controlReports.GetNumberOfWarningResources()
testCase.Failed = controlReports.GetNumberOfFailedResources()
failure := JUnitFailure{}
failure.Message = fmt.Sprintf("%d resources failed", testCase.Failed)
for _, ruleResponses := range controlReports.RuleReports[0].RuleResponses {
failure.Contents = fmt.Sprintf("%s\n%s", failure.Contents, ruleResponses.AlertMessage)
}
testCase.Failure = &failure
}
suite.TestCases = append(suite.TestCases, testCase)
}
juResult.Suites = append(juResult.Suites, suite)
}
return &juResult, nil
}

View File

@@ -1,270 +0,0 @@
package v1
import (
"fmt"
"os"
"sort"
"strings"
"github.com/armosec/k8s-interface/workloadinterface"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/resultshandling/printer"
"github.com/armosec/opa-utils/objectsenvelopes"
"github.com/armosec/opa-utils/reporthandling"
"github.com/enescakir/emoji"
"github.com/olekukonko/tablewriter"
)
type PrettyPrinter struct {
writer *os.File
summary Summary
verboseMode bool
sortedControlNames []string
frameworkSummary ResultSummary
}
func NewPrettyPrinter(verboseMode bool) *PrettyPrinter {
return &PrettyPrinter{
verboseMode: verboseMode,
summary: NewSummary(),
}
}
func (prettyPrinter *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
overallRiskScore := opaSessionObj.Report.SummaryDetails.Score
cautils.ReportV2ToV1(opaSessionObj)
// score := calculatePostureScore(opaSessionObj.PostureReport)
failedResources := []string{}
warningResources := []string{}
allResources := []string{}
frameworkNames := []string{}
frameworkScores := []float32{}
for _, frameworkReport := range opaSessionObj.PostureReport.FrameworkReports {
frameworkNames = append(frameworkNames, frameworkReport.Name)
frameworkScores = append(frameworkScores, frameworkReport.Score)
failedResources = reporthandling.GetUniqueResourcesIDs(append(failedResources, frameworkReport.ListResourcesIDs().GetFailedResources()...))
warningResources = reporthandling.GetUniqueResourcesIDs(append(warningResources, frameworkReport.ListResourcesIDs().GetWarningResources()...))
allResources = reporthandling.GetUniqueResourcesIDs(append(allResources, frameworkReport.ListResourcesIDs().GetAllResources()...))
prettyPrinter.summarySetup(frameworkReport, opaSessionObj.AllResources)
}
prettyPrinter.frameworkSummary = ResultSummary{
RiskScore: overallRiskScore,
TotalResources: len(allResources),
TotalFailed: len(failedResources),
TotalWarning: len(warningResources),
}
prettyPrinter.printResults()
prettyPrinter.printSummaryTable(frameworkNames, frameworkScores)
}
func (prettyPrinter *PrettyPrinter) SetWriter(outputFile string) {
prettyPrinter.writer = printer.GetWriter(outputFile)
}
func (prettyPrinter *PrettyPrinter) Score(score float32) {
}
func (prettyPrinter *PrettyPrinter) summarySetup(fr reporthandling.FrameworkReport, allResources map[string]workloadinterface.IMetadata) {
for _, cr := range fr.ControlReports {
// if len(cr.RuleReports) == 0 {
// continue
// }
workloadsSummary := listResultSummary(cr.RuleReports, allResources)
var passedWorkloads map[string][]WorkloadSummary
if prettyPrinter.verboseMode {
passedWorkloads = groupByNamespaceOrKind(workloadsSummary, workloadSummaryPassed)
}
//controlSummary
prettyPrinter.summary[cr.Name] = ResultSummary{
ID: cr.ControlID,
RiskScore: cr.Score,
TotalResources: cr.GetNumberOfResources(),
TotalFailed: cr.GetNumberOfFailedResources(),
TotalWarning: cr.GetNumberOfWarningResources(),
FailedWorkloads: groupByNamespaceOrKind(workloadsSummary, workloadSummaryFailed),
ExcludedWorkloads: groupByNamespaceOrKind(workloadsSummary, workloadSummaryExclude),
PassedWorkloads: passedWorkloads,
Description: cr.Description,
Remediation: cr.Remediation,
ListInputKinds: cr.ListControlsInputKinds(),
}
}
prettyPrinter.sortedControlNames = prettyPrinter.getSortedControlsNames()
}
func (prettyPrinter *PrettyPrinter) printResults() {
for i := 0; i < len(prettyPrinter.sortedControlNames); i++ {
controlSummary := prettyPrinter.summary[prettyPrinter.sortedControlNames[i]]
prettyPrinter.printTitle(prettyPrinter.sortedControlNames[i], &controlSummary)
prettyPrinter.printResources(&controlSummary)
if prettyPrinter.summary[prettyPrinter.sortedControlNames[i]].TotalResources > 0 {
prettyPrinter.printSummary(prettyPrinter.sortedControlNames[i], &controlSummary)
}
}
}
func (prettyPrinter *PrettyPrinter) printSummary(controlName string, controlSummary *ResultSummary) {
cautils.SimpleDisplay(prettyPrinter.writer, "Summary - ")
cautils.SuccessDisplay(prettyPrinter.writer, "Passed:%v ", controlSummary.TotalResources-controlSummary.TotalFailed-controlSummary.TotalWarning)
cautils.WarningDisplay(prettyPrinter.writer, "Excluded:%v ", controlSummary.TotalWarning)
cautils.FailureDisplay(prettyPrinter.writer, "Failed:%v ", controlSummary.TotalFailed)
cautils.InfoDisplay(prettyPrinter.writer, "Total:%v\n", controlSummary.TotalResources)
if controlSummary.TotalFailed > 0 {
cautils.DescriptionDisplay(prettyPrinter.writer, "Remediation: %v\n", controlSummary.Remediation)
}
cautils.DescriptionDisplay(prettyPrinter.writer, "\n")
}
func (prettyPrinter *PrettyPrinter) printTitle(controlName string, controlSummary *ResultSummary) {
cautils.InfoDisplay(prettyPrinter.writer, "[control: %s - %s] ", controlName, getControlURL(controlSummary.ID))
if controlSummary.TotalResources == 0 {
cautils.InfoDisplay(prettyPrinter.writer, "skipped %v\n", emoji.ConfusedFace)
} else if controlSummary.TotalFailed != 0 {
cautils.FailureDisplay(prettyPrinter.writer, "failed %v\n", emoji.SadButRelievedFace)
} else if controlSummary.TotalWarning != 0 {
cautils.WarningDisplay(prettyPrinter.writer, "excluded %v\n", emoji.NeutralFace)
} else {
cautils.SuccessDisplay(prettyPrinter.writer, "passed %v\n", emoji.ThumbsUp)
}
cautils.DescriptionDisplay(prettyPrinter.writer, "Description: %s\n", controlSummary.Description)
}
func (prettyPrinter *PrettyPrinter) printResources(controlSummary *ResultSummary) {
if len(controlSummary.FailedWorkloads) > 0 {
cautils.FailureDisplay(prettyPrinter.writer, "Failed:\n")
prettyPrinter.printGroupedResources(controlSummary.FailedWorkloads)
}
if len(controlSummary.ExcludedWorkloads) > 0 {
cautils.WarningDisplay(prettyPrinter.writer, "Excluded:\n")
prettyPrinter.printGroupedResources(controlSummary.ExcludedWorkloads)
}
if len(controlSummary.PassedWorkloads) > 0 {
cautils.SuccessDisplay(prettyPrinter.writer, "Passed:\n")
prettyPrinter.printGroupedResources(controlSummary.PassedWorkloads)
}
}
func (prettyPrinter *PrettyPrinter) printGroupedResources(workloads map[string][]WorkloadSummary) {
indent := INDENT
for title, rsc := range workloads {
prettyPrinter.printGroupedResource(indent, title, rsc)
}
}
func (prettyPrinter *PrettyPrinter) printGroupedResource(indent string, title string, rsc []WorkloadSummary) {
preIndent := indent
if title != "" {
cautils.SimpleDisplay(prettyPrinter.writer, "%s%s\n", indent, title)
indent += indent
}
for r := range rsc {
relatedObjectsStr := generateRelatedObjectsStr(rsc[r])
cautils.SimpleDisplay(prettyPrinter.writer, fmt.Sprintf("%s%s - %s %s\n", indent, rsc[r].resource.GetKind(), rsc[r].resource.GetName(), relatedObjectsStr))
}
indent = preIndent
}
func generateRelatedObjectsStr(workload WorkloadSummary) string {
relatedStr := ""
if workload.resource.GetObjectType() == workloadinterface.TypeWorkloadObject {
relatedObjects := objectsenvelopes.NewRegoResponseVectorObject(workload.resource.GetObject()).GetRelatedObjects()
for i, related := range relatedObjects {
if ns := related.GetNamespace(); i == 0 && ns != "" {
relatedStr += fmt.Sprintf("Namespace - %s, ", ns)
}
relatedStr += fmt.Sprintf("%s - %s, ", related.GetKind(), related.GetName())
}
}
if relatedStr != "" {
relatedStr = fmt.Sprintf(" [%s]", relatedStr[:len(relatedStr)-2])
}
return relatedStr
}
func generateRow(control string, cs ResultSummary) []string {
row := []string{control}
row = append(row, cs.ToSlice()...)
if cs.TotalResources != 0 {
row = append(row, fmt.Sprintf("%d", int(cs.RiskScore))+"%")
} else {
row = append(row, "skipped")
}
return row
}
func generateHeader() []string {
return []string{"Control Name", "Failed Resources", "Excluded Resources", "All Resources", "% risk-score"}
}
func generateFooter(prettyPrinter *PrettyPrinter) []string {
// Control name | # failed resources | all resources | % success
row := []string{}
row = append(row, "Resource Summary") //fmt.Sprintf(""%d", numControlers"))
row = append(row, fmt.Sprintf("%d", prettyPrinter.frameworkSummary.TotalFailed))
row = append(row, fmt.Sprintf("%d", prettyPrinter.frameworkSummary.TotalWarning))
row = append(row, fmt.Sprintf("%d", prettyPrinter.frameworkSummary.TotalResources))
row = append(row, fmt.Sprintf("%.2f%s", prettyPrinter.frameworkSummary.RiskScore, "%"))
return row
}
func (prettyPrinter *PrettyPrinter) printSummaryTable(frameworksNames []string, frameworkScores []float32) {
// For control scan framework will be nil
prettyPrinter.printFramework(frameworksNames, frameworkScores)
summaryTable := tablewriter.NewWriter(prettyPrinter.writer)
summaryTable.SetAutoWrapText(false)
summaryTable.SetHeader(generateHeader())
summaryTable.SetHeaderLine(true)
alignments := []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER}
summaryTable.SetColumnAlignment(alignments)
for i := 0; i < len(prettyPrinter.sortedControlNames); i++ {
controlSummary := prettyPrinter.summary[prettyPrinter.sortedControlNames[i]]
summaryTable.Append(generateRow(prettyPrinter.sortedControlNames[i], controlSummary))
}
summaryTable.SetFooter(generateFooter(prettyPrinter))
// summaryTable.SetFooter(generateFooter())
summaryTable.Render()
}
func (prettyPrinter *PrettyPrinter) printFramework(frameworksNames []string, frameworkScores []float32) {
if len(frameworksNames) == 1 {
if frameworksNames[0] != "" {
cautils.InfoTextDisplay(prettyPrinter.writer, fmt.Sprintf("FRAMEWORK %s\n", frameworksNames[0]))
}
} else if len(frameworksNames) > 1 {
p := "FRAMEWORKS: "
for i := 0; i < len(frameworksNames)-1; i++ {
p += fmt.Sprintf("%s (risk: %.2f), ", frameworksNames[i], frameworkScores[i])
}
p += fmt.Sprintf("%s (risk: %.2f)\n", frameworksNames[len(frameworksNames)-1], frameworkScores[len(frameworkScores)-1])
cautils.InfoTextDisplay(prettyPrinter.writer, p)
}
}
func (prettyPrinter *PrettyPrinter) getSortedControlsNames() []string {
controlNames := make([]string, 0, len(prettyPrinter.summary))
for k := range prettyPrinter.summary {
controlNames = append(controlNames, k)
}
sort.Strings(controlNames)
return controlNames
}
func getControlURL(controlID string) string {
return fmt.Sprintf("https://hub.armo.cloud/docs/%s", strings.ToLower(controlID))
}

View File

@@ -1,3 +0,0 @@
package v1
var INDENT = " "

View File

@@ -1,11 +0,0 @@
package v1
import (
"github.com/armosec/kubescape/cautils"
)
type SilentPrinter struct {
}
func (silentPrinter *SilentPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
}

View File

@@ -1,54 +0,0 @@
package v1
import (
"fmt"
"github.com/armosec/k8s-interface/workloadinterface"
"github.com/armosec/opa-utils/reporthandling"
)
type Summary map[string]ResultSummary
func NewSummary() Summary {
return make(map[string]ResultSummary)
}
type ResultSummary struct {
ID string
RiskScore float32
TotalResources int
TotalFailed int
TotalWarning int
Description string
Remediation string
Framework []string
ListInputKinds []string
FailedWorkloads map[string][]WorkloadSummary // <namespace>:[<WorkloadSummary>]
ExcludedWorkloads map[string][]WorkloadSummary // <namespace>:[<WorkloadSummary>]
PassedWorkloads map[string][]WorkloadSummary // <namespace>:[<WorkloadSummary>]
}
type WorkloadSummary struct {
resource workloadinterface.IMetadata
status string
}
func (controlSummary *ResultSummary) ToSlice() []string {
s := []string{}
s = append(s, fmt.Sprintf("%d", controlSummary.TotalFailed))
s = append(s, fmt.Sprintf("%d", controlSummary.TotalWarning))
s = append(s, fmt.Sprintf("%d", controlSummary.TotalResources))
return s
}
func workloadSummaryFailed(workloadSummary *WorkloadSummary) bool {
return workloadSummary.status == reporthandling.StatusFailed
}
func workloadSummaryExclude(workloadSummary *WorkloadSummary) bool {
return workloadSummary.status == reporthandling.StatusWarning
}
func workloadSummaryPassed(workloadSummary *WorkloadSummary) bool {
return workloadSummary.status == reporthandling.StatusPassed
}

View File

@@ -1,85 +0,0 @@
package v1
import (
"github.com/armosec/k8s-interface/k8sinterface"
"github.com/armosec/k8s-interface/workloadinterface"
"github.com/armosec/opa-utils/objectsenvelopes"
"github.com/armosec/opa-utils/reporthandling"
)
// Group workloads by namespace - return {"namespace": <[]WorkloadSummary>}
func groupByNamespaceOrKind(resources []WorkloadSummary, status func(workloadSummary *WorkloadSummary) bool) map[string][]WorkloadSummary {
mapResources := make(map[string][]WorkloadSummary)
for i := range resources {
if !status(&resources[i]) {
continue
}
t := resources[i].resource.GetObjectType()
if t == objectsenvelopes.TypeRegoResponseVectorObject && !isKindToBeGrouped(resources[i].resource.GetKind()) {
t = workloadinterface.TypeWorkloadObject
}
switch t { // TODO - find a better way to defind the groups
case workloadinterface.TypeWorkloadObject:
ns := ""
if resources[i].resource.GetNamespace() != "" {
ns = "Namespace " + resources[i].resource.GetNamespace()
}
if r, ok := mapResources[ns]; ok {
r = append(r, resources[i])
mapResources[ns] = r
} else {
mapResources[ns] = []WorkloadSummary{resources[i]}
}
case objectsenvelopes.TypeRegoResponseVectorObject:
group := resources[i].resource.GetKind() + "s"
if r, ok := mapResources[group]; ok {
r = append(r, resources[i])
mapResources[group] = r
} else {
mapResources[group] = []WorkloadSummary{resources[i]}
}
default:
group, _ := k8sinterface.SplitApiVersion(resources[i].resource.GetApiVersion())
if r, ok := mapResources[group]; ok {
r = append(r, resources[i])
mapResources[group] = r
} else {
mapResources[group] = []WorkloadSummary{resources[i]}
}
}
}
return mapResources
}
func isKindToBeGrouped(kind string) bool {
if kind == "Group" || kind == "User" {
return true
}
return false
}
func listResultSummary(ruleReports []reporthandling.RuleReport, allResources map[string]workloadinterface.IMetadata) []WorkloadSummary {
workloadsSummary := []WorkloadSummary{}
for c := range ruleReports {
resourcesIDs := ruleReports[c].ListResourcesIDs()
workloadsSummary = append(workloadsSummary, newListWorkloadsSummary(allResources, resourcesIDs.GetFailedResources(), reporthandling.StatusFailed)...)
workloadsSummary = append(workloadsSummary, newListWorkloadsSummary(allResources, resourcesIDs.GetWarningResources(), reporthandling.StatusWarning)...)
workloadsSummary = append(workloadsSummary, newListWorkloadsSummary(allResources, resourcesIDs.GetPassedResources(), reporthandling.StatusPassed)...)
}
return workloadsSummary
}
func newListWorkloadsSummary(allResources map[string]workloadinterface.IMetadata, resourcesIDs []string, status string) []WorkloadSummary {
workloadsSummary := []WorkloadSummary{}
for _, i := range resourcesIDs {
if r, ok := allResources[i]; ok {
workloadsSummary = append(workloadsSummary, WorkloadSummary{
resource: r,
status: status,
})
}
}
return workloadsSummary
}

View File

@@ -5,18 +5,22 @@ import (
"fmt"
"os"
"github.com/armosec/armoapi-go/armotypes"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/logger"
"github.com/armosec/kubescape/cautils/logger/helpers"
"github.com/armosec/kubescape/resultshandling/printer"
reporthandlingv2 "github.com/armosec/opa-utils/reporthandling/v2"
)
type JunitPrinter struct {
writer *os.File
writer *os.File
verbose bool
}
func NewJunitPrinter() *JunitPrinter {
return &JunitPrinter{}
func NewJunitPrinter(verbose bool) *JunitPrinter {
return &JunitPrinter{
verbose: verbose,
}
}
func (junitPrinter *JunitPrinter) SetWriter(outputFile string) {
@@ -32,97 +36,89 @@ func (junitPrinter *JunitPrinter) FinalizeData(opaSessionObj *cautils.OPASession
}
func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
junitResult, err := convertPostureReportToJunitResult(opaSessionObj.Report)
junitResult, err := junitPrinter.convertPostureReportToJunitResult(opaSessionObj)
if err != nil {
logger.L().Fatal("failed to convert posture report object")
logger.L().Fatal("failed to build xml result object", helpers.Error(err))
}
postureReportStr, err := xml.Marshal(junitResult)
if err != nil {
logger.L().Fatal("failed to convert posture report object")
logger.L().Fatal("failed to Marshal xml result object", helpers.Error(err))
}
junitPrinter.writer.Write(postureReportStr)
}
type JUnitTestSuites struct {
XMLName xml.Name `xml:"testsuites"`
Suites []JUnitTestSuite `xml:"testsuite"`
}
// JUnitTestSuite is a single JUnit test suite which may contain many
// testcases.
type JUnitTestSuite struct {
XMLName xml.Name `xml:"testsuite"`
Tests int `xml:"tests,attr"`
Time string `xml:"time,attr"`
Name string `xml:"name,attr"`
Resources int `xml:"resources,attr"`
Excluded int `xml:"excluded,attr"`
Failed int `xml:"filed,attr"`
Properties []JUnitProperty `xml:"properties>property,omitempty"`
TestCases []JUnitTestCase `xml:"testcase"`
XMLName xml.Name `xml:"testsuite"`
Suites []JUnitTestCase `xml:"testsuites"`
RiskScore float32 `xml:"riskScore,attr"` // test risk score
Time string `xml:"time,attr"` // scanning time
Controls int `xml:"tests,attr"` // number of controls
}
// JUnitTestCase is a single test case with its result.
type JUnitTestCase struct {
XMLName xml.Name `xml:"testcase"`
Classname string `xml:"classname,attr"`
Name string `xml:"name,attr"`
Time string `xml:"time,attr"`
Resources int `xml:"resources,attr"`
Excluded int `xml:"excluded,attr"`
Failed int `xml:"filed,attr"`
SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"`
Failure *JUnitFailure `xml:"failure,omitempty"`
type JUnitTestCase struct { // Control
XMLName xml.Name `xml:"testcase"`
RiskScore float32 `xml:"riskScore,attr"`
Status string `xml:"status,attr"`
Name string `xml:"name,attr"`
AllResources int `xml:"allResources,attr"`
Excluded int `xml:"excludedResources,attr"`
Failed int `xml:"filedResources,attr"`
Resources []JUnitResource `xml:"resources"`
}
// JUnitSkipMessage contains the reason why a testcase was skipped.
type JUnitSkipMessage struct {
Message string `xml:"message,attr"`
type JUnitResource struct { // Single resource
Name string `xml:"name,attr"`
Namespace string `xml:"namespace,attr"`
Kind string `xml:"kind,attr"`
ApiVersion string `xml:"apiVersion,attr"`
FailedPaths []armotypes.PosturePaths `xml:"jsonPaths"`
}
// JUnitProperty represents a key/value pair used to define properties.
type JUnitProperty struct {
Name string `xml:"name,attr"`
Value string `xml:"value,attr"`
}
// JUnitFailure contains data related to a failed test.
type JUnitFailure struct {
Message string `xml:"message,attr"`
Type string `xml:"type,attr"`
Contents string `xml:",chardata"`
}
func convertPostureReportToJunitResult(postureResult *reporthandlingv2.PostureReport) (*JUnitTestSuites, error) {
juResult := JUnitTestSuites{XMLName: xml.Name{Local: "Kubescape scan results"}}
for _, framework := range postureResult.ListFrameworks().All() {
suite := JUnitTestSuite{
Name: framework.GetName(),
Resources: framework.NumberOfResources().All(),
Excluded: framework.NumberOfResources().Excluded(),
Failed: framework.NumberOfResources().Failed(),
}
for _, controlReports := range postureResult.ListControls().All() {
suite.Tests = suite.Tests + 1
testCase := JUnitTestCase{}
testCase.Name = controlReports.GetName()
testCase.Classname = "Kubescape"
testCase.Time = postureResult.ReportGenerationTime.String()
// if 0 < len(controlReports.RuleReports[0].RuleResponses) {
// testCase.Resources = controlReports.NumberOfResources().All()
// testCase.Excluded = controlReports.NumberOfResources().Excluded()
// testCase.Failed = controlReports.NumberOfResources().Failed()
// failure := JUnitFailure{}
// failure.Message = fmt.Sprintf("%d resources failed", testCase.Failed)
// for _, ruleResponses := range controlReports.RuleReports[0].RuleResponses {
// failure.Contents = fmt.Sprintf("%s\n%s", failure.Contents, ruleResponses.AlertMessage)
// }
// testCase.Failure = &failure
// }
suite.TestCases = append(suite.TestCases, testCase)
}
juResult.Suites = append(juResult.Suites, suite)
func (junitPrinter *JunitPrinter) convertPostureReportToJunitResult(results *cautils.OPASessionObj) (*JUnitTestSuites, error) {
juResult := JUnitTestSuites{
XMLName: xml.Name{
Local: "Kubescape scan results",
},
RiskScore: results.Report.SummaryDetails.Score,
Time: results.Report.GetTimestamp().String(),
Controls: len(results.Report.ListControls().All()),
}
// controls
for _, controlReports := range results.Report.ListControls().All() {
testCase := JUnitTestCase{}
testCase.Name = controlReports.GetName()
testCase.Status = string(controlReports.GetStatus().Status())
// resources
testCase.AllResources = controlReports.NumberOfResources().All()
testCase.Excluded = controlReports.NumberOfResources().Excluded()
testCase.Failed = controlReports.NumberOfResources().Failed()
var jUnitResources []JUnitResource
for _, resourceID := range controlReports.ListResourcesIDs().All() {
if !junitPrinter.verbose {
continue
}
jUnitResource := JUnitResource{}
if resource, ok := results.AllResources[resourceID]; ok {
jUnitResource.Name = resource.GetName()
jUnitResource.Namespace = resource.GetNamespace()
jUnitResource.Kind = resource.GetKind()
jUnitResource.ApiVersion = resource.GetApiVersion()
}
if result, ok := results.ResourcesResult[resourceID]; ok {
rules := result.ListRulesOfControl("", controlReports.GetName())
for _, rule := range rules {
jUnitResource.FailedPaths = append(jUnitResource.FailedPaths, rule.Paths...)
}
}
jUnitResources = append(jUnitResources, jUnitResource)
}
testCase.Resources = jUnitResources
juResult.Suites = append(juResult.Suites, testCase)
}
return &juResult, nil
}

View File

@@ -1,41 +0,0 @@
package resourcemapping
import (
"encoding/json"
"fmt"
"os"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/logger"
"github.com/armosec/kubescape/resultshandling/printer"
)
type JsonPrinter struct {
writer *os.File
}
func NewJsonPrinter() *JsonPrinter {
return &JsonPrinter{}
}
func (jsonPrinter *JsonPrinter) SetWriter(outputFile string) {
jsonPrinter.writer = printer.GetWriter(outputFile)
}
func (jsonPrinter *JsonPrinter) Score(score float32) {
fmt.Fprintf(os.Stderr, "\nOverall risk-score (0- Excellent, 100- All failed): %d\n", int(score))
}
func (jsonPrinter *JsonPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
postureReportStr, err := json.Marshal(opaSessionObj.Report)
if err != nil {
logger.L().Fatal("failed to convert posture report object")
}
jsonPrinter.writer.Write(postureReportStr)
}
func (jsonPrinter *JsonPrinter) FinalizeData(opaSessionObj *cautils.OPASessionObj) {
// finalizeReport(opaSessionObj)
}

View File

@@ -59,7 +59,7 @@ func NewPrinter(printFormat, outputVersion string, verboseMode bool) printer.IPr
case printer.JsonFormat:
return printerv1.NewJsonPrinter()
case printer.JunitResultFormat:
return printerv1.NewJunitPrinter()
return printerv2.NewJunitPrinter(verboseMode)
case printer.PrometheusFormat:
return printerv1.NewPrometheusPrinter(verboseMode)
case printer.PdfFormat: