Compare commits

..

25 Commits

Author SHA1 Message Date
David Wertenteil
63968b564b update k8s-interface pkg 2022-05-18 14:36:55 +03:00
David Wertenteil
e237c48186 merged 2022-05-18 14:24:53 +03:00
David Wertenteil
622b121535 adding scan request log 2022-05-18 13:22:33 +03:00
Bezbran
20774d4a40 Merge pull request #510 from Daniel-GrunbergerCA/master
Set number of worker nodes based on scheduable nodes (based on taints) & set status to 'skipped' when there are no image vulns
2022-05-18 09:40:51 +03:00
DanielGrunbergerCA
7bb6bb85ec go mod 2022-05-18 09:35:58 +03:00
DanielGrunbergerCA
da908a84bc update k8s-iface for http handler 2022-05-18 09:35:07 +03:00
DanielGrunbergerCA
b515e259c0 Merge remote-tracking branch 'upstream/dev' 2022-05-18 09:33:38 +03:00
DanielGrunbergerCA
facd551518 update k8s-interface version 2022-05-18 09:33:13 +03:00
David Wertenteil
0fc569d9d9 fixed import 2022-05-18 00:35:45 +03:00
David Wertenteil
da27a27ad5 adding status rest api 2022-05-18 00:34:15 +03:00
DanielGrunbergerCA
5d4a20f622 fix test 2022-05-17 16:01:03 +03:00
DanielGrunbergerCA
70b15a373b unit test for isEmptyImgVulns 2022-05-17 15:32:49 +03:00
DanielGrunbergerCA
01353f81b3 unit test for isMaterNodeTaints 2022-05-16 17:29:29 +03:00
DanielGrunbergerCA
22f10b6581 go mod 2022-05-16 17:02:55 +03:00
DanielGrunbergerCA
785178ffb1 show skipped for scan without imgvuln 2022-05-16 16:55:37 +03:00
DanielGrunbergerCA
f9b5c58402 pull worker nodes based on taints 2022-05-16 16:36:00 +03:00
David Wertenteil
8ed6d63ce5 Merge pull request #509 from Daniel-GrunbergerCA/fix-eks
Fix eks and support http for all endpoints
2022-05-16 15:22:49 +03:00
DanielGrunbergerCA
990a7c2052 update go mod 2022-05-16 14:28:09 +03:00
DanielGrunbergerCA
09b0c09472 support http and https for all endpoints 2022-05-16 14:13:04 +03:00
DanielGrunbergerCA
f83c38b58e update k8s-interface 2022-05-16 11:51:55 +03:00
DanielGrunbergerCA
51e600797a Merge remote-tracking branch 'upstream/dev' into fix-eks 2022-05-16 11:43:40 +03:00
DanielGrunbergerCA
6382edeb6e Merge remote-tracking branch 'upstream/dev' 2022-05-10 09:05:18 +03:00
DanielGrunbergerCA
7103c7d32c fix url 2022-05-04 11:11:29 +03:00
DanielGrunbergerCA
b4e1663cd1 make parse func 2022-05-03 16:24:19 +03:00
DanielGrunbergerCA
60ec6e8294 support env with http 2022-05-03 12:58:33 +03:00
33 changed files with 967 additions and 198 deletions

View File

@@ -12,7 +12,6 @@ import (
"github.com/armosec/kubescape/v2/core/cautils/logger"
"github.com/armosec/kubescape/v2/core/cautils/logger/helpers"
"github.com/armosec/kubescape/v2/core/meta"
"github.com/armosec/opa-utils/reporthandling"
"github.com/enescakir/emoji"
"github.com/spf13/cobra"
)
@@ -59,7 +58,7 @@ func getControlCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comman
RunE: func(cmd *cobra.Command, args []string) error {
// flagValidationControl(scanInfo)
scanInfo.PolicyIdentifier = []reporthandling.PolicyIdentifier{}
scanInfo.PolicyIdentifier = []cautils.PolicyIdentifier{}
if len(args) == 0 {
scanInfo.ScanAll = true

View File

@@ -91,7 +91,7 @@ func GetScanCommand(ks meta.IKubescape) *cobra.Command {
scanCmd.PersistentFlags().MarkHidden("silent") // this flag should be deprecated since we added the --logger support
// scanCmd.PersistentFlags().MarkHidden("format-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 valuable data from cluster nodes for certain controls. Yaml file: https://raw.githubusercontent.com/armosec/kubescape/master/hostsensorutils/hostsensor.yaml")
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 valuable data from cluster nodes for certain controls. Yaml file: https://github.com/armosec/kubescape/blob/master/core/pkg/hostsensorutils/hostsensor.yaml")
hostF.NoOptDefVal = "true"
hostF.DefValue = "false, for no TTY in stdin"

View File

@@ -9,12 +9,23 @@ import (
"strings"
)
func parseHost(urlObj *url.URL) {
if strings.Contains(urlObj.Host, "http://") {
urlObj.Scheme = "http"
urlObj.Host = strings.Replace(urlObj.Host, "http://", "", 1)
} else {
urlObj.Scheme = "https"
urlObj.Host = strings.Replace(urlObj.Host, "https://", "", 1)
}
}
var NativeFrameworks = []string{"nsa", "mitre", "armobest", "devopsbest"}
func (armoAPI *ArmoAPI) getFrameworkURL(frameworkName string) string {
u := url.URL{}
u.Scheme = "https"
u.Host = armoAPI.apiURL
u.Host = armoAPI.GetAPIURL()
parseHost(&u)
u.Path = "api/v1/armoFrameworks"
q := u.Query()
q.Add("customerGUID", armoAPI.getCustomerGUIDFallBack())
@@ -31,8 +42,9 @@ func (armoAPI *ArmoAPI) getFrameworkURL(frameworkName string) string {
func (armoAPI *ArmoAPI) getListFrameworkURL() string {
u := url.URL{}
u.Scheme = "https"
u.Host = armoAPI.apiURL
u.Host = armoAPI.GetAPIURL()
parseHost(&u)
u.Path = "api/v1/armoFrameworks"
q := u.Query()
q.Add("customerGUID", armoAPI.getCustomerGUIDFallBack())
@@ -42,8 +54,8 @@ func (armoAPI *ArmoAPI) getListFrameworkURL() string {
}
func (armoAPI *ArmoAPI) getExceptionsURL(clusterName string) string {
u := url.URL{}
u.Scheme = "https"
u.Host = armoAPI.apiURL
u.Host = armoAPI.GetAPIURL()
parseHost(&u)
u.Path = "api/v1/armoPostureExceptions"
q := u.Query()
@@ -58,8 +70,8 @@ func (armoAPI *ArmoAPI) getExceptionsURL(clusterName string) string {
func (armoAPI *ArmoAPI) exceptionsURL(exceptionsPolicyName string) string {
u := url.URL{}
u.Scheme = "https"
u.Host = armoAPI.apiURL
u.Host = armoAPI.GetAPIURL()
parseHost(&u)
u.Path = "api/v1/postureExceptionPolicy"
q := u.Query()
@@ -81,8 +93,8 @@ func (armoAPI *ArmoAPI) getAccountConfigDefault(clusterName string) string {
func (armoAPI *ArmoAPI) getAccountConfig(clusterName string) string {
u := url.URL{}
u.Scheme = "https"
u.Host = armoAPI.apiURL
u.Host = armoAPI.GetAPIURL()
parseHost(&u)
u.Path = "api/v1/armoCustomerConfiguration"
q := u.Query()
@@ -97,8 +109,8 @@ func (armoAPI *ArmoAPI) getAccountConfig(clusterName string) string {
func (armoAPI *ArmoAPI) getAccountURL() string {
u := url.URL{}
u.Scheme = "https"
u.Host = armoAPI.apiURL
u.Host = armoAPI.GetAPIURL()
parseHost(&u)
u.Path = "api/v1/createTenant"
return u.String()
}
@@ -106,7 +118,6 @@ func (armoAPI *ArmoAPI) getAccountURL() string {
func (armoAPI *ArmoAPI) getApiToken() string {
u := url.URL{}
u.Scheme = "https"
u.Host = armoAPI.authURL
u.Path = "frontegg/identity/resources/auth/v1/api-token"
return u.String()
}
@@ -114,7 +125,6 @@ func (armoAPI *ArmoAPI) getApiToken() string {
func (armoAPI *ArmoAPI) getOpenidCustomers() string {
u := url.URL{}
u.Scheme = "https"
u.Host = armoAPI.apiURL
u.Path = "api/v1/openid_customers"
return u.String()
}

View File

@@ -8,6 +8,7 @@ import (
"path/filepath"
"strings"
"github.com/armosec/armoapi-go/armotypes"
apisv1 "github.com/armosec/opa-utils/httpserver/apis/v1"
giturl "github.com/armosec/go-git-url"
@@ -77,33 +78,39 @@ const (
ControlViewType ViewTypes = "control"
)
type PolicyIdentifier struct {
Name string // policy name e.g. nsa,mitre,c-0012
Kind apisv1.NotificationPolicyKind // policy kind e.g. Framework,Control,Rule
Designators armotypes.PortalDesignator
}
type ScanInfo struct {
Getters // TODO - remove from object
PolicyIdentifier []reporthandling.PolicyIdentifier // TODO - remove from object
UseExceptions string // Load file with exceptions configuration
ControlsInputs string // Load file with inputs for controls
UseFrom []string // Load framework from local file (instead of download). Use when running offline
UseDefault bool // Load framework from cached file (instead of download). Use when running offline
UseArtifactsFrom string // Load artifacts from local path. Use when running offline
VerboseMode bool // Display all of the input resources and not only failed resources
View string // Display all of the input resources and not only failed resources
Format string // Format results (table, json, junit ...)
Output string // Store results in an output file, Output file name
FormatVersion string // Output object can be differnet between versions, this is for testing and backward compatibility
ExcludedNamespaces string // used for host scanner namespace
IncludeNamespaces string // DEPRECATED?
InputPatterns []string // Yaml files input patterns
Silent bool // Silent mode - Do not print progress logs
FailThreshold float32 // Failure score threshold
Submit bool // Submit results to Armo BE
ScanID string // Report id of the current scan
HostSensorEnabled BoolPtrFlag // Deploy ARMO K8s host scanner to collect data from certain controls
HostSensorYamlPath string // Path to hostsensor file
Local bool // Do not submit results
Account string // account ID
KubeContext string // context name
FrameworkScan bool // false if scanning control
ScanAll bool // true if scan all frameworks
Getters // TODO - remove from object
PolicyIdentifier []PolicyIdentifier // TODO - remove from object
UseExceptions string // Load file with exceptions configuration
ControlsInputs string // Load file with inputs for controls
UseFrom []string // Load framework from local file (instead of download). Use when running offline
UseDefault bool // Load framework from cached file (instead of download). Use when running offline
UseArtifactsFrom string // Load artifacts from local path. Use when running offline
VerboseMode bool // Display all of the input resources and not only failed resources
View string // Display all of the input resources and not only failed resources
Format string // Format results (table, json, junit ...)
Output string // Store results in an output file, Output file name
FormatVersion string // Output object can be differnet between versions, this is for testing and backward compatibility
ExcludedNamespaces string // used for host scanner namespace
IncludeNamespaces string //
InputPatterns []string // Yaml files input patterns
Silent bool // Silent mode - Do not print progress logs
FailThreshold float32 // Failure score threshold
Submit bool // Submit results to Armo BE
ScanID string // Report id of the current scan
HostSensorEnabled BoolPtrFlag // Deploy ARMO K8s host scanner to collect data from certain controls
HostSensorYamlPath string // Path to hostsensor file
Local bool // Do not submit results
Account string // account ID
KubeContext string // context name
FrameworkScan bool // false if scanning control
ScanAll bool // true if scan all frameworks
}
type Getters struct {
@@ -193,8 +200,8 @@ func (scanInfo *ScanInfo) GetScanningEnvironment() string {
func (scanInfo *ScanInfo) SetPolicyIdentifiers(policies []string, kind apisv1.NotificationPolicyKind) {
for _, policy := range policies {
if !scanInfo.contains(policy) {
newPolicy := reporthandling.PolicyIdentifier{}
newPolicy.Kind = reporthandling.NotificationPolicyKind(kind) // reporthandling.KindFramework
newPolicy := PolicyIdentifier{}
newPolicy.Kind = kind
newPolicy.Name = policy
scanInfo.PolicyIdentifier = append(scanInfo.PolicyIdentifier, newPolicy)
}

View File

@@ -13,7 +13,6 @@ import (
"github.com/armosec/kubescape/v2/core/pkg/resultshandling/reporter"
reporterv2 "github.com/armosec/kubescape/v2/core/pkg/resultshandling/reporter/v2"
"github.com/armosec/opa-utils/reporthandling"
"github.com/armosec/rbac-utils/rbacscanner"
)
@@ -105,7 +104,7 @@ func getFieldSelector(scanInfo *cautils.ScanInfo) resourcehandler.IFieldSelector
return &resourcehandler.EmptySelector{}
}
func policyIdentifierNames(pi []reporthandling.PolicyIdentifier) string {
func policyIdentifierNames(pi []cautils.PolicyIdentifier) string {
policiesNames := ""
for i := range pi {
policiesNames += pi[i].Name

View File

@@ -5,7 +5,6 @@ import (
apisv1 "github.com/armosec/opa-utils/httpserver/apis/v1"
"github.com/armosec/armoapi-go/armotypes"
"github.com/armosec/k8s-interface/k8sinterface"
"github.com/armosec/kubescape/v2/core/cautils"
@@ -20,7 +19,6 @@ import (
"github.com/armosec/kubescape/v2/core/pkg/resultshandling/printer"
"github.com/armosec/kubescape/v2/core/pkg/resultshandling/reporter"
"github.com/armosec/opa-utils/reporthandling"
"github.com/armosec/opa-utils/resources"
)
@@ -146,7 +144,7 @@ func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsH
// ===================== policies & resources =====================
policyHandler := policyhandler.NewPolicyHandler(interfaces.resourceHandler)
scanData, err := collectResources(policyHandler, scanInfo)
scanData, err := policyHandler.CollectResources(scanInfo.PolicyIdentifier, scanInfo)
if err != nil {
return resultsHandling, err
}
@@ -169,28 +167,6 @@ func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsH
return resultsHandling, nil
}
// TODO - remove function
func collectResources(policyHandler *policyhandler.PolicyHandler, scanInfo *cautils.ScanInfo) (*cautils.OPASessionObj, error) {
policyNotification := &reporthandling.PolicyNotification{
Rules: scanInfo.PolicyIdentifier,
KubescapeNotification: reporthandling.KubescapeNotification{
Designators: armotypes.PortalDesignator{},
NotificationType: reporthandling.TypeExecPostureScan,
},
}
switch policyNotification.KubescapeNotification.NotificationType {
case reporthandling.TypeExecPostureScan:
collectedResources, err := policyHandler.CollectResources(policyNotification, scanInfo)
if err != nil {
return nil, err
}
return collectedResources, nil
default:
return nil, fmt.Errorf("notification type '%s' Unknown", policyNotification.KubescapeNotification.NotificationType)
}
}
// func askUserForHostSensor() bool {
// return false

View File

@@ -14,7 +14,7 @@ var (
LinuxKernelVariables = "LinuxKernelVariables"
KubeletCommandLine = "KubeletCommandLine"
MapResourceToApiGroup = map[string]string{
MapHostSensorResourceToApiGroup = map[string]string{
KubeletConfiguration: "hostdata.kubescape.cloud/v1beta0",
OsReleaseFile: "hostdata.kubescape.cloud/v1beta0",
KubeletCommandLine: "hostdata.kubescape.cloud/v1beta0",
@@ -26,7 +26,7 @@ var (
)
func addInfoToMap(resource string, infoMap map[string]apis.StatusInfo, err error) {
group, version := k8sinterface.SplitApiVersion(MapResourceToApiGroup[resource])
group, version := k8sinterface.SplitApiVersion(MapHostSensorResourceToApiGroup[resource])
r := k8sinterface.JoinResourceTriplets(group, version, resource)
infoMap[r] = apis.StatusInfo{
InnerStatus: apis.StatusSkipped,

View File

@@ -3,9 +3,9 @@ package policyhandler
import (
"fmt"
"github.com/armosec/armoapi-go/armotypes"
"github.com/armosec/kubescape/v2/core/cautils"
"github.com/armosec/kubescape/v2/core/pkg/resourcehandler"
"github.com/armosec/opa-utils/reporthandling"
)
// PolicyHandler -
@@ -22,7 +22,7 @@ func NewPolicyHandler(resourceHandler resourcehandler.IResourceHandler) *PolicyH
}
}
func (policyHandler *PolicyHandler) CollectResources(notification *reporthandling.PolicyNotification, scanInfo *cautils.ScanInfo) (*cautils.OPASessionObj, error) {
func (policyHandler *PolicyHandler) CollectResources(policyIdentifier []cautils.PolicyIdentifier, scanInfo *cautils.ScanInfo) (*cautils.OPASessionObj, error) {
opaSessionObj := cautils.NewOPASessionObj(nil, nil, scanInfo)
// validate notification
@@ -30,11 +30,11 @@ func (policyHandler *PolicyHandler) CollectResources(notification *reporthandlin
policyHandler.getters = &scanInfo.Getters
// get policies
if err := policyHandler.getPolicies(notification, opaSessionObj); err != nil {
if err := policyHandler.getPolicies(policyIdentifier, opaSessionObj); err != nil {
return opaSessionObj, err
}
err := policyHandler.getResources(notification, opaSessionObj, scanInfo)
err := policyHandler.getResources(policyIdentifier, opaSessionObj, scanInfo)
if err != nil {
return opaSessionObj, err
}
@@ -46,10 +46,10 @@ func (policyHandler *PolicyHandler) CollectResources(notification *reporthandlin
return opaSessionObj, nil
}
func (policyHandler *PolicyHandler) getResources(notification *reporthandling.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *cautils.ScanInfo) error {
func (policyHandler *PolicyHandler) getResources(policyIdentifier []cautils.PolicyIdentifier, opaSessionObj *cautils.OPASessionObj, scanInfo *cautils.ScanInfo) error {
opaSessionObj.Report.ClusterAPIServerInfo = policyHandler.resourceHandler.GetClusterAPIServerInfo()
resourcesMap, allResources, armoResources, err := policyHandler.resourceHandler.GetResources(opaSessionObj, &notification.Designators)
resourcesMap, allResources, armoResources, err := policyHandler.resourceHandler.GetResources(opaSessionObj, &policyIdentifier[0].Designators)
if err != nil {
return err
}
@@ -60,3 +60,10 @@ func (policyHandler *PolicyHandler) getResources(notification *reporthandling.Po
return nil
}
func getDesignator(policyIdentifier []cautils.PolicyIdentifier) *armotypes.PortalDesignator {
if len(policyIdentifier) > 0 {
return &policyIdentifier[0].Designators
}
return &armotypes.PortalDesignator{}
}

View File

@@ -4,6 +4,8 @@ import (
"fmt"
"strings"
apisv1 "github.com/armosec/opa-utils/httpserver/apis/v1"
"github.com/armosec/kubescape/v2/core/cautils"
"github.com/armosec/kubescape/v2/core/cautils/getter"
"github.com/armosec/kubescape/v2/core/cautils/logger"
@@ -11,18 +13,18 @@ import (
"github.com/armosec/opa-utils/reporthandling"
)
func (policyHandler *PolicyHandler) getPolicies(notification *reporthandling.PolicyNotification, policiesAndResources *cautils.OPASessionObj) error {
func (policyHandler *PolicyHandler) getPolicies(policyIdentifier []cautils.PolicyIdentifier, policiesAndResources *cautils.OPASessionObj) error {
logger.L().Info("Downloading/Loading policy definitions")
cautils.StartSpinner()
defer cautils.StopSpinner()
policies, err := policyHandler.getScanPolicies(notification)
policies, err := policyHandler.getScanPolicies(policyIdentifier)
if err != nil {
return err
}
if len(policies) == 0 {
return fmt.Errorf("failed to download policies: '%s'. Make sure the policy exist and you spelled it correctly. For more information, please feel free to contact ARMO team", strings.Join(policyIdentifierToSlice(notification.Rules), ", "))
return fmt.Errorf("failed to download policies: '%s'. Make sure the policy exist and you spelled it correctly. For more information, please feel free to contact ARMO team", strings.Join(policyIdentifierToSlice(policyIdentifier), ", "))
}
policiesAndResources.Policies = policies
@@ -44,12 +46,12 @@ func (policyHandler *PolicyHandler) getPolicies(notification *reporthandling.Pol
return nil
}
func (policyHandler *PolicyHandler) getScanPolicies(notification *reporthandling.PolicyNotification) ([]reporthandling.Framework, error) {
func (policyHandler *PolicyHandler) getScanPolicies(policyIdentifier []cautils.PolicyIdentifier) ([]reporthandling.Framework, error) {
frameworks := []reporthandling.Framework{}
switch getScanKind(notification) {
case reporthandling.KindFramework: // Download frameworks
for _, rule := range notification.Rules {
switch getScanKind(policyIdentifier) {
case apisv1.KindFramework: // Download frameworks
for _, rule := range policyIdentifier {
receivedFramework, err := policyHandler.getters.PolicyGetter.GetFramework(rule.Name)
if err != nil {
return frameworks, policyDownloadError(err)
@@ -63,11 +65,11 @@ func (policyHandler *PolicyHandler) getScanPolicies(notification *reporthandling
}
}
}
case reporthandling.KindControl: // Download controls
case apisv1.KindControl: // Download controls
f := reporthandling.Framework{}
var receivedControl *reporthandling.Control
var err error
for _, rule := range notification.Rules {
for _, rule := range policyIdentifier {
receivedControl, err = policyHandler.getters.PolicyGetter.GetControl(rule.Name)
if err != nil {
return frameworks, policyDownloadError(err)
@@ -89,7 +91,7 @@ func (policyHandler *PolicyHandler) getScanPolicies(notification *reporthandling
return frameworks, nil
}
func policyIdentifierToSlice(rules []reporthandling.PolicyIdentifier) []string {
func policyIdentifierToSlice(rules []cautils.PolicyIdentifier) []string {
s := []string{}
for i := range rules {
s = append(s, fmt.Sprintf("%s: %s", rules[i].Kind, rules[i].Name))

View File

@@ -4,12 +4,14 @@ import (
"fmt"
"strings"
"github.com/armosec/opa-utils/reporthandling"
apisv1 "github.com/armosec/opa-utils/httpserver/apis/v1"
"github.com/armosec/kubescape/v2/core/cautils"
)
func getScanKind(notification *reporthandling.PolicyNotification) reporthandling.NotificationPolicyKind {
if len(notification.Rules) > 0 {
return notification.Rules[0].Kind
func getScanKind(policyIdentifier []cautils.PolicyIdentifier) apisv1.NotificationPolicyKind {
if len(policyIdentifier) > 0 {
return policyIdentifier[0].Kind
}
return "unknown"
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/armosec/armoapi-go/armotypes"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8slabels "k8s.io/apimachinery/pkg/labels"
@@ -86,6 +87,9 @@ func (k8sHandler *K8sResourceHandler) GetResources(sessionObj *cautils.OPASessio
if err := k8sHandler.registryAdaptors.collectImagesVulnerabilities(k8sResourcesMap, allResources, armoResourceMap); err != nil {
logger.L().Warning("failed to collect image vulnerabilities", helpers.Error(err))
}
if isEmptyImgVulns(*armoResourceMap) {
cautils.SetInfoMapForResources("image scanning not configured. For more information: https://hub.armo.cloud/docs/cluster-vulnerability-scanning", imgVulnResources, sessionObj.InfoMap)
}
}
hostResources := cautils.MapHostResources(armoResourceMap)
@@ -103,7 +107,7 @@ func (k8sHandler *K8sResourceHandler) GetResources(sessionObj *cautils.OPASessio
sessionObj.InfoMap = infoMap
}
} else {
cautils.SetInfoMapForResources("enable-host-scan flag not used", hostResources, sessionObj.InfoMap)
cautils.SetInfoMapForResources("enable-host-scan flag not used. For more information: https://hub.armo.cloud/docs/host-sensor", hostResources, sessionObj.InfoMap)
}
}
@@ -287,17 +291,31 @@ func getCloudProviderDescription(allResources map[string]workloadinterface.IMeta
}
func (k8sHandler *K8sResourceHandler) pullWorkerNodesNumber() (int, error) {
// labels used for control plane
listOptions := metav1.ListOptions{
LabelSelector: "!node-role.kubernetes.io/control-plane,!node-role.kubernetes.io/master",
nodesList, err := k8sHandler.k8s.KubernetesClient.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
scheduableNodes := v1.NodeList{}
if nodesList != nil {
for _, node := range nodesList.Items {
if len(node.Spec.Taints) == 0 {
scheduableNodes.Items = append(scheduableNodes.Items, node)
} else {
if !isMasterNodeTaints(node.Spec.Taints) {
scheduableNodes.Items = append(scheduableNodes.Items, node)
}
}
}
}
nodesList, err := k8sHandler.k8s.KubernetesClient.CoreV1().Nodes().List(context.TODO(), listOptions)
if err != nil {
return 0, err
}
nodesNumber := 0
if nodesList != nil {
nodesNumber = len(nodesList.Items)
}
return nodesNumber, nil
return len(scheduableNodes.Items), nil
}
// NoSchedule taint with empty value is usually applied to controlplane
func isMasterNodeTaints(taints []v1.Taint) bool {
for _, taint := range taints {
if taint.Effect == v1.TaintEffectNoSchedule && taint.Value == "" {
return true
}
}
return false
}

View File

@@ -0,0 +1,532 @@
package resourcehandler
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
)
func TestIsMasterNodeTaints(t *testing.T) {
noTaintNode := `
{
"apiVersion": "v1",
"kind": "Node",
"metadata": {
"annotations": {
"kubeadm.alpha.kubernetes.io/cri-socket": "/var/run/dockershim.sock",
"node.alpha.kubernetes.io/ttl": "0",
"volumes.kubernetes.io/controller-managed-attach-detach": "true"
},
"creationTimestamp": "2022-05-16T10:52:32Z",
"labels": {
"beta.kubernetes.io/arch": "amd64",
"beta.kubernetes.io/os": "linux",
"kubernetes.io/arch": "amd64",
"kubernetes.io/hostname": "danielg-minikube",
"kubernetes.io/os": "linux",
"minikube.k8s.io/commit": "3e64b11ed75e56e4898ea85f96b2e4af0301f43d",
"minikube.k8s.io/name": "danielg-minikube",
"minikube.k8s.io/updated_at": "2022_05_16T13_52_35_0700",
"minikube.k8s.io/version": "v1.25.1",
"node-role.kubernetes.io/control-plane": "",
"node-role.kubernetes.io/master": "",
"node.kubernetes.io/exclude-from-external-load-balancers": ""
},
"name": "danielg-minikube",
"resourceVersion": "9432",
"uid": "fc4afcb6-4ca4-4038-ba54-5e16065a614a"
},
"spec": {
"podCIDR": "10.244.0.0/24",
"podCIDRs": [
"10.244.0.0/24"
]
},
"status": {
"addresses": [
{
"address": "192.168.49.2",
"type": "InternalIP"
},
{
"address": "danielg-minikube",
"type": "Hostname"
}
],
"allocatable": {
"cpu": "4",
"ephemeral-storage": "94850516Ki",
"hugepages-2Mi": "0",
"memory": "10432976Ki",
"pods": "110"
},
"capacity": {
"cpu": "4",
"ephemeral-storage": "94850516Ki",
"hugepages-2Mi": "0",
"memory": "10432976Ki",
"pods": "110"
},
"conditions": [
{
"lastHeartbeatTime": "2022-05-16T14:14:31Z",
"lastTransitionTime": "2022-05-16T10:52:29Z",
"message": "kubelet has sufficient memory available",
"reason": "KubeletHasSufficientMemory",
"status": "False",
"type": "MemoryPressure"
},
{
"lastHeartbeatTime": "2022-05-16T14:14:31Z",
"lastTransitionTime": "2022-05-16T10:52:29Z",
"message": "kubelet has no disk pressure",
"reason": "KubeletHasNoDiskPressure",
"status": "False",
"type": "DiskPressure"
},
{
"lastHeartbeatTime": "2022-05-16T14:14:31Z",
"lastTransitionTime": "2022-05-16T10:52:29Z",
"message": "kubelet has sufficient PID available",
"reason": "KubeletHasSufficientPID",
"status": "False",
"type": "PIDPressure"
},
{
"lastHeartbeatTime": "2022-05-16T14:14:31Z",
"lastTransitionTime": "2022-05-16T10:52:45Z",
"message": "kubelet is posting ready status",
"reason": "KubeletReady",
"status": "True",
"type": "Ready"
}
],
"daemonEndpoints": {
"kubeletEndpoint": {
"Port": 10250
}
},
"images": [
{
"names": [
"requarks/wiki@sha256:dd83fff15e77843ff934b25c28c865ac000edf7653e5d11adad1dd51df87439d"
],
"sizeBytes": 441083858
},
{
"names": [
"mariadb@sha256:821d0411208eaa88f9e1f0daccd1d534f88d19baf724eb9a2777cbedb10b6c66"
],
"sizeBytes": 400782682
},
{
"names": [
"k8s.gcr.io/etcd@sha256:64b9ea357325d5db9f8a723dcf503b5a449177b17ac87d69481e126bb724c263",
"k8s.gcr.io/etcd:3.5.1-0"
],
"sizeBytes": 292558922
},
{
"names": [
"kubernetesui/dashboard@sha256:ec27f462cf1946220f5a9ace416a84a57c18f98c777876a8054405d1428cc92e",
"kubernetesui/dashboard:v2.3.1"
],
"sizeBytes": 220033604
},
{
"names": [
"k8s.gcr.io/kube-apiserver@sha256:f54681a71cce62cbc1b13ebb3dbf1d880f849112789811f98b6aebd2caa2f255",
"k8s.gcr.io/kube-apiserver:v1.23.1"
],
"sizeBytes": 135162256
},
{
"names": [
"k8s.gcr.io/kube-controller-manager@sha256:a7ed87380108a2d811f0d392a3fe87546c85bc366e0d1e024dfa74eb14468604",
"k8s.gcr.io/kube-controller-manager:v1.23.1"
],
"sizeBytes": 124971684
},
{
"names": [
"k8s.gcr.io/kube-proxy@sha256:e40f3a28721588affcf187f3f246d1e078157dabe274003eaa2957a83f7170c8",
"k8s.gcr.io/kube-proxy:v1.23.1"
],
"sizeBytes": 112327826
},
{
"names": [
"quay.io/armosec/kubescape@sha256:6196f766be50d94b45d903a911f5ee95ac99bc392a1324c3e063bec41efd98ba",
"quay.io/armosec/kubescape:v2.0.153"
],
"sizeBytes": 110345054
},
{
"names": [
"nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d"
],
"sizeBytes": 109129446
},
{
"names": [
"quay.io/armosec/action-trigger@sha256:b93707d10ff86aac8dfa42ad37192d6bcf9aceeb4321b21756e438389c26e07c",
"quay.io/armosec/action-trigger:v0.0.5"
],
"sizeBytes": 65127067
},
{
"names": [
"quay.io/armosec/images-vulnerabilities-scan@sha256:a5f9ddc04a7fdce6d52ef85a21f0de567d8e04d418c2bc5bf5d72b151c997625",
"quay.io/armosec/images-vulnerabilities-scan:v0.0.7"
],
"sizeBytes": 61446712
},
{
"names": [
"quay.io/armosec/images-vulnerabilities-scan@sha256:2f879858da89f6542e3223fb18d6d793810cc2ad6e398b66776475e4218b6af5",
"quay.io/armosec/images-vulnerabilities-scan:v0.0.8"
],
"sizeBytes": 61446528
},
{
"names": [
"quay.io/armosec/cluster-collector@sha256:2c4f733d09f7f4090ace04585230bdfacbbc29a3ade38a2e1233d2c0f730d9b6",
"quay.io/armosec/cluster-collector:v0.0.9"
],
"sizeBytes": 53699576
},
{
"names": [
"k8s.gcr.io/kube-scheduler@sha256:8be4eb1593cf9ff2d91b44596633b7815a3753696031a1eb4273d1b39427fa8c",
"k8s.gcr.io/kube-scheduler:v1.23.1"
],
"sizeBytes": 53488305
},
{
"names": [
"k8s.gcr.io/coredns/coredns@sha256:5b6ec0d6de9baaf3e92d0f66cd96a25b9edbce8716f5f15dcd1a616b3abd590e",
"k8s.gcr.io/coredns/coredns:v1.8.6"
],
"sizeBytes": 46829283
},
{
"names": [
"kubernetesui/metrics-scraper@sha256:36d5b3f60e1a144cc5ada820910535074bdf5cf73fb70d1ff1681537eef4e172",
"kubernetesui/metrics-scraper:v1.0.7"
],
"sizeBytes": 34446077
},
{
"names": [
"gcr.io/k8s-minikube/storage-provisioner@sha256:18eb69d1418e854ad5a19e399310e52808a8321e4c441c1dddad8977a0d7a944",
"gcr.io/k8s-minikube/storage-provisioner:v5"
],
"sizeBytes": 31465472
},
{
"names": [
"quay.io/armosec/notification-server@sha256:b6e9b296cd53bd3b2b42c516d8ab43db998acff1124a57aff8d66b3dd7881979",
"quay.io/armosec/notification-server:v0.0.3"
],
"sizeBytes": 20209940
},
{
"names": [
"quay.io/armosec/kube-host-sensor@sha256:82139d2561039726be060df2878ef023c59df7c536fbd7f6d766af5a99569fee",
"quay.io/armosec/kube-host-sensor:latest"
],
"sizeBytes": 11796788
},
{
"names": [
"k8s.gcr.io/pause@sha256:3d380ca8864549e74af4b29c10f9cb0956236dfb01c40ca076fb6c37253234db",
"k8s.gcr.io/pause:3.6"
],
"sizeBytes": 682696
}
],
"nodeInfo": {
"architecture": "amd64",
"bootID": "828cbe73-120b-43cf-aae0-9e2d15b8c873",
"containerRuntimeVersion": "docker://20.10.12",
"kernelVersion": "5.13.0-40-generic",
"kubeProxyVersion": "v1.23.1",
"kubeletVersion": "v1.23.1",
"machineID": "8de776e053e140d6a14c2d2def3d6bb8",
"operatingSystem": "linux",
"osImage": "Ubuntu 20.04.2 LTS",
"systemUUID": "da12dc19-10bf-4033-a440-2d9aa33d6fe3"
}
}
}
`
var l v1.Node
_ = json.Unmarshal([]byte(noTaintNode), &l)
assert.False(t, isMasterNodeTaints(l.Spec.Taints))
taintNode :=
`
{
"apiVersion": "v1",
"kind": "Node",
"metadata": {
"annotations": {
"kubeadm.alpha.kubernetes.io/cri-socket": "/var/run/dockershim.sock",
"node.alpha.kubernetes.io/ttl": "0",
"volumes.kubernetes.io/controller-managed-attach-detach": "true"
},
"creationTimestamp": "2022-05-16T10:52:32Z",
"labels": {
"beta.kubernetes.io/arch": "amd64",
"beta.kubernetes.io/os": "linux",
"kubernetes.io/arch": "amd64",
"kubernetes.io/hostname": "danielg-minikube",
"kubernetes.io/os": "linux",
"minikube.k8s.io/commit": "3e64b11ed75e56e4898ea85f96b2e4af0301f43d",
"minikube.k8s.io/name": "danielg-minikube",
"minikube.k8s.io/updated_at": "2022_05_16T13_52_35_0700",
"minikube.k8s.io/version": "v1.25.1",
"node-role.kubernetes.io/control-plane": "",
"node-role.kubernetes.io/master": "",
"node.kubernetes.io/exclude-from-external-load-balancers": ""
},
"name": "danielg-minikube",
"resourceVersion": "9871",
"uid": "fc4afcb6-4ca4-4038-ba54-5e16065a614a"
},
"spec": {
"podCIDR": "10.244.0.0/24",
"podCIDRs": [
"10.244.0.0/24"
],
"taints": [
{
"effect": "NoSchedule",
"key": "key1",
"value": ""
}
]
},
"status": {
"addresses": [
{
"address": "192.168.49.2",
"type": "InternalIP"
},
{
"address": "danielg-minikube",
"type": "Hostname"
}
],
"allocatable": {
"cpu": "4",
"ephemeral-storage": "94850516Ki",
"hugepages-2Mi": "0",
"memory": "10432976Ki",
"pods": "110"
},
"capacity": {
"cpu": "4",
"ephemeral-storage": "94850516Ki",
"hugepages-2Mi": "0",
"memory": "10432976Ki",
"pods": "110"
},
"conditions": [
{
"lastHeartbeatTime": "2022-05-16T14:24:45Z",
"lastTransitionTime": "2022-05-16T10:52:29Z",
"message": "kubelet has sufficient memory available",
"reason": "KubeletHasSufficientMemory",
"status": "False",
"type": "MemoryPressure"
},
{
"lastHeartbeatTime": "2022-05-16T14:24:45Z",
"lastTransitionTime": "2022-05-16T10:52:29Z",
"message": "kubelet has no disk pressure",
"reason": "KubeletHasNoDiskPressure",
"status": "False",
"type": "DiskPressure"
},
{
"lastHeartbeatTime": "2022-05-16T14:24:45Z",
"lastTransitionTime": "2022-05-16T10:52:29Z",
"message": "kubelet has sufficient PID available",
"reason": "KubeletHasSufficientPID",
"status": "False",
"type": "PIDPressure"
},
{
"lastHeartbeatTime": "2022-05-16T14:24:45Z",
"lastTransitionTime": "2022-05-16T10:52:45Z",
"message": "kubelet is posting ready status",
"reason": "KubeletReady",
"status": "True",
"type": "Ready"
}
],
"daemonEndpoints": {
"kubeletEndpoint": {
"Port": 10250
}
},
"images": [
{
"names": [
"requarks/wiki@sha256:dd83fff15e77843ff934b25c28c865ac000edf7653e5d11adad1dd51df87439d"
],
"sizeBytes": 441083858
},
{
"names": [
"mariadb@sha256:821d0411208eaa88f9e1f0daccd1d534f88d19baf724eb9a2777cbedb10b6c66"
],
"sizeBytes": 400782682
},
{
"names": [
"k8s.gcr.io/etcd@sha256:64b9ea357325d5db9f8a723dcf503b5a449177b17ac87d69481e126bb724c263",
"k8s.gcr.io/etcd:3.5.1-0"
],
"sizeBytes": 292558922
},
{
"names": [
"kubernetesui/dashboard@sha256:ec27f462cf1946220f5a9ace416a84a57c18f98c777876a8054405d1428cc92e",
"kubernetesui/dashboard:v2.3.1"
],
"sizeBytes": 220033604
},
{
"names": [
"k8s.gcr.io/kube-apiserver@sha256:f54681a71cce62cbc1b13ebb3dbf1d880f849112789811f98b6aebd2caa2f255",
"k8s.gcr.io/kube-apiserver:v1.23.1"
],
"sizeBytes": 135162256
},
{
"names": [
"k8s.gcr.io/kube-controller-manager@sha256:a7ed87380108a2d811f0d392a3fe87546c85bc366e0d1e024dfa74eb14468604",
"k8s.gcr.io/kube-controller-manager:v1.23.1"
],
"sizeBytes": 124971684
},
{
"names": [
"k8s.gcr.io/kube-proxy@sha256:e40f3a28721588affcf187f3f246d1e078157dabe274003eaa2957a83f7170c8",
"k8s.gcr.io/kube-proxy:v1.23.1"
],
"sizeBytes": 112327826
},
{
"names": [
"quay.io/armosec/kubescape@sha256:6196f766be50d94b45d903a911f5ee95ac99bc392a1324c3e063bec41efd98ba",
"quay.io/armosec/kubescape:v2.0.153"
],
"sizeBytes": 110345054
},
{
"names": [
"nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d"
],
"sizeBytes": 109129446
},
{
"names": [
"quay.io/armosec/action-trigger@sha256:b93707d10ff86aac8dfa42ad37192d6bcf9aceeb4321b21756e438389c26e07c",
"quay.io/armosec/action-trigger:v0.0.5"
],
"sizeBytes": 65127067
},
{
"names": [
"quay.io/armosec/images-vulnerabilities-scan@sha256:a5f9ddc04a7fdce6d52ef85a21f0de567d8e04d418c2bc5bf5d72b151c997625",
"quay.io/armosec/images-vulnerabilities-scan:v0.0.7"
],
"sizeBytes": 61446712
},
{
"names": [
"quay.io/armosec/images-vulnerabilities-scan@sha256:2f879858da89f6542e3223fb18d6d793810cc2ad6e398b66776475e4218b6af5",
"quay.io/armosec/images-vulnerabilities-scan:v0.0.8"
],
"sizeBytes": 61446528
},
{
"names": [
"quay.io/armosec/cluster-collector@sha256:2c4f733d09f7f4090ace04585230bdfacbbc29a3ade38a2e1233d2c0f730d9b6",
"quay.io/armosec/cluster-collector:v0.0.9"
],
"sizeBytes": 53699576
},
{
"names": [
"k8s.gcr.io/kube-scheduler@sha256:8be4eb1593cf9ff2d91b44596633b7815a3753696031a1eb4273d1b39427fa8c",
"k8s.gcr.io/kube-scheduler:v1.23.1"
],
"sizeBytes": 53488305
},
{
"names": [
"k8s.gcr.io/coredns/coredns@sha256:5b6ec0d6de9baaf3e92d0f66cd96a25b9edbce8716f5f15dcd1a616b3abd590e",
"k8s.gcr.io/coredns/coredns:v1.8.6"
],
"sizeBytes": 46829283
},
{
"names": [
"kubernetesui/metrics-scraper@sha256:36d5b3f60e1a144cc5ada820910535074bdf5cf73fb70d1ff1681537eef4e172",
"kubernetesui/metrics-scraper:v1.0.7"
],
"sizeBytes": 34446077
},
{
"names": [
"gcr.io/k8s-minikube/storage-provisioner@sha256:18eb69d1418e854ad5a19e399310e52808a8321e4c441c1dddad8977a0d7a944",
"gcr.io/k8s-minikube/storage-provisioner:v5"
],
"sizeBytes": 31465472
},
{
"names": [
"quay.io/armosec/notification-server@sha256:b6e9b296cd53bd3b2b42c516d8ab43db998acff1124a57aff8d66b3dd7881979",
"quay.io/armosec/notification-server:v0.0.3"
],
"sizeBytes": 20209940
},
{
"names": [
"quay.io/armosec/kube-host-sensor@sha256:82139d2561039726be060df2878ef023c59df7c536fbd7f6d766af5a99569fee",
"quay.io/armosec/kube-host-sensor:latest"
],
"sizeBytes": 11796788
},
{
"names": [
"k8s.gcr.io/pause@sha256:3d380ca8864549e74af4b29c10f9cb0956236dfb01c40ca076fb6c37253234db",
"k8s.gcr.io/pause:3.6"
],
"sizeBytes": 682696
}
],
"nodeInfo": {
"architecture": "amd64",
"bootID": "828cbe73-120b-43cf-aae0-9e2d15b8c873",
"containerRuntimeVersion": "docker://20.10.12",
"kernelVersion": "5.13.0-40-generic",
"kubeProxyVersion": "v1.23.1",
"kubeletVersion": "v1.23.1",
"machineID": "8de776e053e140d6a14c2d2def3d6bb8",
"operatingSystem": "linux",
"osImage": "Ubuntu 20.04.2 LTS",
"systemUUID": "da12dc19-10bf-4033-a440-2d9aa33d6fe3"
}
}
}
`
_ = json.Unmarshal([]byte(taintNode), &l)
assert.True(t, isMasterNodeTaints(l.Spec.Taints))
}

View File

@@ -4,7 +4,6 @@ import (
"strings"
"github.com/armosec/kubescape/v2/core/cautils"
"github.com/armosec/kubescape/v2/core/pkg/hostsensorutils"
"github.com/armosec/opa-utils/reporthandling"
"k8s.io/utils/strings/slices"
@@ -12,12 +11,43 @@ import (
)
var (
ClusterDescribe = "ClusterDescribe"
ClusterDescribe = "ClusterDescribe"
KubeletConfiguration = "KubeletConfiguration"
OsReleaseFile = "OsReleaseFile"
KernelVersion = "KernelVersion"
LinuxSecurityHardeningStatus = "LinuxSecurityHardeningStatus"
OpenPortsList = "OpenPortsList"
LinuxKernelVariables = "LinuxKernelVariables"
KubeletCommandLine = "KubeletCommandLine"
ImageVulnerabilities = "ImageVulnerabilities"
MapResourceToApiGroup = map[string]string{
KubeletConfiguration: "hostdata.kubescape.cloud/v1beta0",
OsReleaseFile: "hostdata.kubescape.cloud/v1beta0",
KubeletCommandLine: "hostdata.kubescape.cloud/v1beta0",
KernelVersion: "hostdata.kubescape.cloud/v1beta0",
LinuxSecurityHardeningStatus: "hostdata.kubescape.cloud/v1beta0",
OpenPortsList: "hostdata.kubescape.cloud/v1beta0",
LinuxKernelVariables: "hostdata.kubescape.cloud/v1beta0",
}
MapResourceToApiGroupVuln = map[string][]string{
ImageVulnerabilities: {"armo.vuln.images/v1", "image.vulnscan.com/v1"}}
MapResourceToApiGroupCloud = map[string][]string{
ClusterDescribe: {"container.googleapis.com/v1", "eks.amazonaws.com/v1", "management.azure.com/v1"}}
)
func isEmptyImgVulns(armoResourcesMap cautils.ArmoResources) bool {
imgVulnResources := cautils.MapImageVulnResources(&armoResourcesMap)
for _, resource := range imgVulnResources {
if val, ok := armoResourcesMap[resource]; ok {
if len(val) > 0 {
return false
}
}
}
return true
}
func setK8sResourceMap(frameworks []reporthandling.Framework) *cautils.K8SResources {
k8sResources := make(cautils.K8SResources)
complexMap := setComplexK8sResourceMap(frameworks)
@@ -80,10 +110,16 @@ func setComplexArmoResourceMap(frameworks []reporthandling.Framework, resourceTo
}
func mapArmoResourceToApiGroup(resource string) []string {
if val, ok := hostsensorutils.MapResourceToApiGroup[resource]; ok {
if val, ok := MapResourceToApiGroup[resource]; ok {
return []string{val}
}
return MapResourceToApiGroupCloud[resource]
if val, ok := MapResourceToApiGroupCloud[resource]; ok {
return val
}
if val, ok := MapResourceToApiGroupVuln[resource]; ok {
return val
}
return []string{}
}
func insertControls(resource string, resourceToControl map[string][]string, control reporthandling.Control) {

View File

@@ -2,7 +2,9 @@ package resourcehandler
import (
"github.com/armosec/k8s-interface/k8sinterface"
"github.com/armosec/kubescape/v2/core/cautils"
"github.com/armosec/opa-utils/reporthandling"
"github.com/stretchr/testify/assert"
"testing"
)
@@ -24,6 +26,18 @@ func TestSetResourceMap(t *testing.T) {
}
}
func TestSsEmptyImgVulns(t *testing.T) {
armoResourcesMap := make(cautils.ArmoResources, 0)
armoResourcesMap["container.googleapis.com/v1"] = []string{"fsdfds"}
assert.Equal(t, true, isEmptyImgVulns(armoResourcesMap))
armoResourcesMap["armo.vuln.images/v1/ImageVulnerabilities"] = []string{"dada"}
assert.Equal(t, false, isEmptyImgVulns(armoResourcesMap))
armoResourcesMap["armo.vuln.images/v1/ImageVulnerabilities"] = []string{}
armoResourcesMap["bla"] = []string{"blu"}
assert.Equal(t, true, isEmptyImgVulns(armoResourcesMap))
}
func TestInsertK8sResources(t *testing.T) {
// insertK8sResources

View File

@@ -28,7 +28,7 @@ func (jsonPrinter *JsonPrinter) Score(score float32) {
}
func (jsonPrinter *JsonPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
r, err := json.Marshal(DataToJson(opaSessionObj))
r, err := json.Marshal(FinalizeResults(opaSessionObj))
if err != nil {
logger.L().Fatal("failed to Marshal posture report object")
}

View File

@@ -12,7 +12,7 @@ import (
)
// finalizeV2Report finalize the results objects by copying data from map to lists
func DataToJson(data *cautils.OPASessionObj) *reporthandlingv2.PostureReport {
func FinalizeResults(data *cautils.OPASessionObj) *reporthandlingv2.PostureReport {
report := reporthandlingv2.PostureReport{
SummaryDetails: data.Report.SummaryDetails,
ClusterAPIServerInfo: data.Report.ClusterAPIServerInfo,

View File

@@ -12,6 +12,7 @@ import (
"github.com/armosec/kubescape/v2/core/cautils/getter"
"github.com/armosec/kubescape/v2/core/cautils/logger"
"github.com/armosec/kubescape/v2/core/cautils/logger/helpers"
v2 "github.com/armosec/kubescape/v2/core/pkg/resultshandling/reporter/v2"
"github.com/armosec/opa-utils/reporthandling"
"github.com/google/uuid"
)
@@ -140,8 +141,8 @@ func (report *ReportEventReceiver) generateMessage() {
message := "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 registering here:"
u := url.URL{}
u.Scheme = "https"
u.Host = getter.GetArmoAPIConnector().GetFrontendURL()
v2.ParseHost(&u)
if report.customerAdminEMail != "" {
logger.L().Debug("", helpers.String("account ID", report.customerGUID))

View File

@@ -5,15 +5,16 @@ import (
"github.com/armosec/k8s-interface/workloadinterface"
"github.com/armosec/kubescape/v2/core/cautils/getter"
v2 "github.com/armosec/kubescape/v2/core/pkg/resultshandling/reporter/v2"
"github.com/armosec/opa-utils/reporthandling"
"github.com/google/uuid"
)
func (report *ReportEventReceiver) initEventReceiverURL() {
urlObj := url.URL{}
urlObj.Scheme = "https"
urlObj.Host = getter.GetArmoAPIConnector().GetReportReceiverURL()
v2.ParseHost(&urlObj)
urlObj.Path = "/k8s/postureReport"
q := urlObj.Query()
q.Add("customerGUID", uuid.MustParse(report.customerGUID).String())

View File

@@ -89,9 +89,8 @@ func (report *ReportEventReceiver) prepareReport(opaSessionObj *cautils.OPASessi
func (report *ReportEventReceiver) GetURL() string {
u := url.URL{}
u.Scheme = "https"
u.Host = getter.GetArmoAPIConnector().GetFrontendURL()
ParseHost(&u)
q := u.Query()
if report.customerAdminEMail != "" || report.token == "" { // data has been submitted

View File

@@ -11,11 +11,9 @@ import (
func (report *ReportEventReceiver) initEventReceiverURL() {
urlObj := url.URL{}
urlObj.Scheme = "https"
urlObj.Host = getter.GetArmoAPIConnector().GetReportReceiverURL()
ParseHost(&urlObj)
urlObj.Path = "/k8s/v2/postureReport"
q := urlObj.Query()
q.Add("customerGUID", uuid.MustParse(report.customerGUID).String())
q.Add("clusterName", report.clusterName)

View File

@@ -1,6 +1,7 @@
package v2
import (
"net/url"
"strings"
)
@@ -21,3 +22,13 @@ func maskID(id string) string {
return strings.TrimSuffix(str, sep)
}
func ParseHost(urlObj *url.URL) {
if strings.Contains(urlObj.Host, "http://") {
urlObj.Scheme = "http"
urlObj.Host = strings.Replace(urlObj.Host, "http://", "", 1)
} else {
urlObj.Scheme = "https"
urlObj.Host = strings.Replace(urlObj.Host, "https://", "", 1)
}
}

View File

@@ -0,0 +1,38 @@
package v2
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseHost(t *testing.T) {
urlObj := url.URL{}
urlObj.Host = "http://localhost:7555"
ParseHost(&urlObj)
assert.Equal(t, "http", urlObj.Scheme)
assert.Equal(t, "localhost:7555", urlObj.Host)
urlObj.Host = "https://localhost:7555"
ParseHost(&urlObj)
assert.Equal(t, "https", urlObj.Scheme)
assert.Equal(t, "localhost:7555", urlObj.Host)
urlObj.Host = "http://portal-dev.armo.cloud"
ParseHost(&urlObj)
assert.Equal(t, "http", urlObj.Scheme)
assert.Equal(t, "portal-dev.armo.cloud", urlObj.Host)
urlObj.Host = "https://portal-dev.armo.cloud"
ParseHost(&urlObj)
assert.Equal(t, "https", urlObj.Scheme)
assert.Equal(t, "portal-dev.armo.cloud", urlObj.Host)
urlObj.Host = "portal-dev.armo.cloud"
ParseHost(&urlObj)
assert.Equal(t, "https", urlObj.Scheme)
assert.Equal(t, "portal-dev.armo.cloud", urlObj.Host)
}

View File

@@ -10,6 +10,7 @@ import (
printerv1 "github.com/armosec/kubescape/v2/core/pkg/resultshandling/printer/v1"
printerv2 "github.com/armosec/kubescape/v2/core/pkg/resultshandling/printer/v2"
"github.com/armosec/kubescape/v2/core/pkg/resultshandling/reporter"
reporthandlingv2 "github.com/armosec/opa-utils/reporthandling/v2"
)
type ResultsHandler struct {
@@ -52,7 +53,12 @@ func (resultsHandler *ResultsHandler) GetReporter() reporter.IReport {
// ToJson return results in json format
func (resultsHandler *ResultsHandler) ToJson() ([]byte, error) {
return json.Marshal(printerv2.DataToJson(resultsHandler.scanData))
return json.Marshal(printerv2.FinalizeResults(resultsHandler.scanData))
}
// GetResults return results
func (resultsHandler *ResultsHandler) GetResults() *reporthandlingv2.PostureReport {
return printerv2.FinalizeResults(resultsHandler.scanData)
}
// HandleResults handle the scan results according to the pre defind interfaces

4
go.mod
View File

@@ -5,8 +5,8 @@ go 1.17
require (
github.com/armosec/armoapi-go v0.0.73
github.com/armosec/go-git-url v0.0.4
github.com/armosec/k8s-interface v0.0.70
github.com/armosec/opa-utils v0.0.137
github.com/armosec/k8s-interface v0.0.74
github.com/armosec/opa-utils v0.0.139
github.com/armosec/rbac-utils v0.0.14
github.com/armosec/utils-go v0.0.5
github.com/armosec/utils-k8s-go v0.0.6

8
go.sum
View File

@@ -113,18 +113,18 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armosec/armoapi-go v0.0.2/go.mod h1:vIK17yoKbJRQyZXWWLe3AqfqCRITxW8qmSkApyq5xFs=
github.com/armosec/armoapi-go v0.0.23/go.mod h1:iaVVGyc23QGGzAdv4n+szGQg3Rbpixn9yQTU3qWRpaw=
github.com/armosec/armoapi-go v0.0.67/go.mod h1:/9SQAgtLbYkfFneRRm/zkIn3zz+4Y2xv6N3vtFcyF8s=
github.com/armosec/armoapi-go v0.0.73 h1:LMf+eCkkf+W9NVvOzHKFgVUEpBMvh27M7//UQP3aiO8=
github.com/armosec/armoapi-go v0.0.73/go.mod h1:/9SQAgtLbYkfFneRRm/zkIn3zz+4Y2xv6N3vtFcyF8s=
github.com/armosec/go-git-url v0.0.4 h1:emG9Yfl53rHpuX41fXLD92ehzhRoNSSnGT6Pr7ogWMY=
github.com/armosec/go-git-url v0.0.4/go.mod h1:PJqdEyJyFxTQvawBcyOM0Ies6+uezire5gpwfr1XX5M=
github.com/armosec/k8s-interface v0.0.8/go.mod h1:xxS+V5QT3gVQTwZyAMMDrYLWGrfKOpiJ7Jfhfa0w9sM=
github.com/armosec/k8s-interface v0.0.37/go.mod h1:vHxGWqD/uh6+GQb9Sqv7OGMs+Rvc2dsFVc0XtgRh1ZU=
github.com/armosec/k8s-interface v0.0.70 h1:NU3UIaNl7H3hsRecwggiaQbZXTwXtOKg3GOBjq6/XJw=
github.com/armosec/k8s-interface v0.0.70/go.mod h1:8NX4xWXh8mwW7QyZdZea1czNdM2azCK9BbUNmiZYXW0=
github.com/armosec/k8s-interface v0.0.74 h1:qZ1bkQv9JLfpwzxNcTytpYQpHKTKZ5rIhMze8SU1cPI=
github.com/armosec/k8s-interface v0.0.74/go.mod h1:8NX4xWXh8mwW7QyZdZea1czNdM2azCK9BbUNmiZYXW0=
github.com/armosec/opa-utils v0.0.64/go.mod h1:6tQP8UDq2EvEfSqh8vrUdr/9QVSCG4sJfju1SXQOn4c=
github.com/armosec/opa-utils v0.0.137 h1:KAkxWYnnTef8ofixJ198Zs4Xs7MOh32+yMUyFY7I8DA=
github.com/armosec/opa-utils v0.0.137/go.mod h1:mCFQzz4E227f7V2jQVQ9XCivkNNK3UWCTaZ0HE5rBWk=
github.com/armosec/opa-utils v0.0.139 h1:JPxgPXVJUUIujtIoZk6TejE8PkZhX2pYnpj+E8PhcfA=
github.com/armosec/opa-utils v0.0.139/go.mod h1:VnRVJgDDPFAprGDcibTtKHf9wgkoyTU8wmX2BxEIwok=
github.com/armosec/rbac-utils v0.0.1/go.mod h1:pQ8CBiij8kSKV7aeZm9FMvtZN28VgA7LZcYyTWimq40=
github.com/armosec/rbac-utils v0.0.14 h1:CKYKcgqJEXWF2Hen/B1pVGtS3nDAG1wp9dDv6oNtq90=
github.com/armosec/rbac-utils v0.0.14/go.mod h1:Ex/IdGWhGv9HZq6Hs8N/ApzCKSIvpNe/ETqDfnuyah0=

View File

@@ -4,50 +4,85 @@
Running `kubescape` will start up a webserver on port `8080` which will serve the following paths:
### Trigger scan
* POST `/v1/scan` - Trigger a kubescape scan. The server will return an ID and will execute the scanning asynchronously
* * `wait`: scan synchronously (return results and not ID). Use only in small clusters are with an increased timeout
* * `keep`: Do not delete results from local storage after returning
### Get results
* GET `/v1/results` - Request kubescape scan results
* * query `id=<string>` -> ID returned when triggering the scan action. If empty will return latest results
* * query `keep` -> Do not delete results from local storage after returning
### Check scanning progress status
Check the scanning status - is the scanning in progress or done. This is meant for a waiting mechanize since the API does not return the entire results object when the scanning is done
* GET `/v1/status` - Request kubescape scan status
* * query `id=<string>` -> Check status of a specific scan. If empty will check if any scan is in progress
### Delete cached results
* DELETE `/v1/results` - Delete kubescape scan results from storage. If empty will delete latest results
* * query `id=<string>`: Delete ID of specific results
* * query `all`: Delete all cached results
### Prometheus support API
* GET/POST `/v1/metrics` - will trigger cluster scan. will respond with prometheus metrics once they have been scanned. This will respond 503 if the scan failed.
* `/livez` - will respond 200 is server is alive
* `/readyz` - will respond 200 if server can receive requests
## Trigger Kubescape scan
POST /v1/results
POST /v1/scan
body:
```
{
"format": <str>, // results format [default: json] (same as 'kubescape scan --format')
"excludedNamespaces": [<str>], // list of namespaces to exclude (same as 'kubescape scan --excluded-namespaces')
"includeNamespaces": [<str>], // list of namespaces to include (same as 'kubescape scan --include-namespaces')
"useCachedArtifacts"`: <bool>, // use the cached artifacts instead of downloading (offline support)
"submit": <bool>, // submit results to Kubescape cloud (same as 'kubescape scan --submit')
"hostScanner": <bool>, // deploy kubescape K8s host-scanner DaemonSet in the scanned cluster (same as 'kubescape scan --enable-host-scan')
"keepLocal": <bool>, // do not submit results to Kubescape cloud (same as 'kubescape scan --keep-local')
"account": <str>, // account ID (same as 'kubescape scan --account')
"targetType": <str>, // framework/control
"targetNames": [<str>] // names. e.g. when targetType==framework, targetNames=["nsa", "mitre"]
"format": <str>, // results format [default: json] (same as 'kubescape scan --format')
"excludedNamespaces": [<str>], // list of namespaces to exclude (same as 'kubescape scan --excluded-namespaces')
"includeNamespaces": [<str>], // list of namespaces to include (same as 'kubescape scan --include-namespaces')
"useCachedArtifacts"`: <bool>, // use the cached artifacts instead of downloading (offline support)
"submit": <bool>, // submit results to Kubescape cloud (same as 'kubescape scan --submit')
"hostScanner": <bool>, // deploy kubescape K8s host-scanner DaemonSet in the scanned cluster (same as 'kubescape scan --enable-host-scan')
"keepLocal": <bool>, // do not submit results to Kubescape cloud (same as 'kubescape scan --keep-local')
"account": <str>, // account ID (same as 'kubescape scan --account')
"targetType": <str>, // framework/control
"targetNames": [<str>] // names. e.g. when targetType==framework, targetNames=["nsa", "mitre"]
}
```
Response body:
```
{
"id": <str>, // scan ID
"type": <responseType:str>, // response object type
"response": <object:interface> // response payload as list of bytes
}
```
Response body types:
* "v1results" - v1 results object
* "id" - id string
* "error" - error object
## API Examples
#### Default scan
1. Trigger kubescape scan
```bash
curl --header "Content-Type: application/json" --request POST --data '{"hostScanner":true}' http://127.0.0.1:8080/v1/scan -o scan_id
curl --header "Content-Type: application/json" --request POST --data '{"hostScanner":true, "submit": true}' http://127.0.0.1:8080/v1/scan
```
2. Get kubescape scan results
```bash
curl --request GET http://127.0.0.1:8080/v1/results?id=$(cat scan_id)
curl --request GET http://127.0.0.1:8080/v1/results -o response.json
```
#### Trigger scan and wait for scan to end
```bash
curl --header "Content-Type: application/json" --request POST --data '{"hostScanner":true, "submit": true}' http://127.0.0.1:8080/v1/scan?wait -o scan_results.json
```
#### Scan single namespace with a specific framework
```bash
curl --header "Content-Type: application/json" \
@@ -55,6 +90,7 @@ curl --header "Content-Type: application/json" \
--data '{"hostScanner":true, "submit":true, "includeNamespaces": ["ks-scanner"], "targetType": "framework", "targetNames": ["nsa"] }' \
http://127.0.0.1:8080/v1/scan
```
## Examples
* [Prometheus](examples/prometheus/README.md)

View File

@@ -6,10 +6,11 @@ replace github.com/armosec/kubescape/v2 => ../
require (
github.com/armosec/kubescape/v2 v2.0.0-00010101000000-000000000000
github.com/armosec/opa-utils v0.0.137
github.com/armosec/opa-utils v0.0.139
github.com/armosec/utils-go v0.0.5
github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0
github.com/gorilla/schema v1.2.0
github.com/stretchr/testify v1.7.1
)
@@ -30,7 +31,7 @@ require (
github.com/OneOfOne/xxhash v1.2.8 // indirect
github.com/armosec/armoapi-go v0.0.73 // indirect
github.com/armosec/go-git-url v0.0.4 // indirect
github.com/armosec/k8s-interface v0.0.70 // indirect
github.com/armosec/k8s-interface v0.0.75 // indirect
github.com/armosec/rbac-utils v0.0.14 // indirect
github.com/armosec/utils-k8s-go v0.0.6 // indirect
github.com/aws/aws-sdk-go v1.41.11 // indirect

View File

@@ -120,11 +120,13 @@ github.com/armosec/go-git-url v0.0.4 h1:emG9Yfl53rHpuX41fXLD92ehzhRoNSSnGT6Pr7og
github.com/armosec/go-git-url v0.0.4/go.mod h1:PJqdEyJyFxTQvawBcyOM0Ies6+uezire5gpwfr1XX5M=
github.com/armosec/k8s-interface v0.0.8/go.mod h1:xxS+V5QT3gVQTwZyAMMDrYLWGrfKOpiJ7Jfhfa0w9sM=
github.com/armosec/k8s-interface v0.0.37/go.mod h1:vHxGWqD/uh6+GQb9Sqv7OGMs+Rvc2dsFVc0XtgRh1ZU=
github.com/armosec/k8s-interface v0.0.70 h1:NU3UIaNl7H3hsRecwggiaQbZXTwXtOKg3GOBjq6/XJw=
github.com/armosec/k8s-interface v0.0.70/go.mod h1:8NX4xWXh8mwW7QyZdZea1czNdM2azCK9BbUNmiZYXW0=
github.com/armosec/k8s-interface v0.0.75 h1:pfheXWGcE6vUlo4TOkwXQ8iGo8Dw/UCXefD3Bx4l0Qs=
github.com/armosec/k8s-interface v0.0.75/go.mod h1:8NX4xWXh8mwW7QyZdZea1czNdM2azCK9BbUNmiZYXW0=
github.com/armosec/opa-utils v0.0.64/go.mod h1:6tQP8UDq2EvEfSqh8vrUdr/9QVSCG4sJfju1SXQOn4c=
github.com/armosec/opa-utils v0.0.137 h1:KAkxWYnnTef8ofixJ198Zs4Xs7MOh32+yMUyFY7I8DA=
github.com/armosec/opa-utils v0.0.137/go.mod h1:mCFQzz4E227f7V2jQVQ9XCivkNNK3UWCTaZ0HE5rBWk=
github.com/armosec/opa-utils v0.0.139 h1:JPxgPXVJUUIujtIoZk6TejE8PkZhX2pYnpj+E8PhcfA=
github.com/armosec/opa-utils v0.0.139/go.mod h1:VnRVJgDDPFAprGDcibTtKHf9wgkoyTU8wmX2BxEIwok=
github.com/armosec/rbac-utils v0.0.1/go.mod h1:pQ8CBiij8kSKV7aeZm9FMvtZN28VgA7LZcYyTWimq40=
github.com/armosec/rbac-utils v0.0.14 h1:CKYKcgqJEXWF2Hen/B1pVGtS3nDAG1wp9dDv6oNtq90=
github.com/armosec/rbac-utils v0.0.14/go.mod h1:Ex/IdGWhGv9HZq6Hs8N/ApzCKSIvpNe/ETqDfnuyah0=
@@ -462,6 +464,8 @@ github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97Dwqy
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=

View File

@@ -8,7 +8,6 @@ import (
"github.com/armosec/kubescape/v2/core/cautils"
"github.com/armosec/kubescape/v2/core/cautils/getter"
"github.com/armosec/opa-utils/reporthandling"
)
func ToScanInfo(scanRequest *utilsmetav1.PostScanRequest) *cautils.ScanInfo {
@@ -61,7 +60,7 @@ func ToScanInfo(scanRequest *utilsmetav1.PostScanRequest) *cautils.ScanInfo {
func setTargetInScanInfo(scanRequest *utilsmetav1.PostScanRequest, scanInfo *cautils.ScanInfo) {
if scanRequest.TargetType != "" && len(scanRequest.TargetNames) > 0 {
if strings.EqualFold(string(scanRequest.TargetType), string(reporthandling.KindFramework)) {
if strings.EqualFold(string(scanRequest.TargetType), string(apisv1.KindFramework)) {
scanRequest.TargetType = apisv1.KindFramework
scanInfo.FrameworkScan = true
scanInfo.ScanAll = false
@@ -69,7 +68,7 @@ func setTargetInScanInfo(scanRequest *utilsmetav1.PostScanRequest, scanInfo *cau
scanRequest.TargetNames = []string{}
scanInfo.ScanAll = true
}
} else if strings.EqualFold(string(scanRequest.TargetType), string(reporthandling.KindControl)) {
} else if strings.EqualFold(string(scanRequest.TargetType), string(apisv1.KindControl)) {
scanRequest.TargetType = apisv1.KindControl
scanInfo.ScanAll = false
} else {

View File

@@ -6,7 +6,6 @@ import (
"github.com/armosec/kubescape/v2/core/cautils"
apisv1 "github.com/armosec/opa-utils/httpserver/apis/v1"
utilsmetav1 "github.com/armosec/opa-utils/httpserver/meta/v1"
"github.com/armosec/opa-utils/reporthandling"
"github.com/stretchr/testify/assert"
)
@@ -34,9 +33,9 @@ func TestToScanInfo(t *testing.T) {
assert.False(t, s.ScanAll)
assert.True(t, s.FrameworkScan)
assert.Equal(t, "nsa", s.PolicyIdentifier[0].Name)
assert.Equal(t, reporthandling.KindFramework, s.PolicyIdentifier[0].Kind)
assert.Equal(t, apisv1.KindFramework, s.PolicyIdentifier[0].Kind)
assert.Equal(t, "mitre", s.PolicyIdentifier[1].Name)
assert.Equal(t, reporthandling.KindFramework, s.PolicyIdentifier[1].Kind)
assert.Equal(t, apisv1.KindFramework, s.PolicyIdentifier[1].Kind)
}
{
req := &utilsmetav1.PostScanRequest{
@@ -51,7 +50,7 @@ func TestToScanInfo(t *testing.T) {
assert.Equal(t, "", s.ExcludedNamespaces)
assert.Equal(t, 1, len(s.PolicyIdentifier))
assert.Equal(t, "c-0001", s.PolicyIdentifier[0].Name)
assert.Equal(t, reporthandling.KindControl, s.PolicyIdentifier[0].Kind)
assert.Equal(t, apisv1.KindControl, s.PolicyIdentifier[0].Kind)
}
{
req := &utilsmetav1.PostScanRequest{}

View File

@@ -9,6 +9,7 @@ import (
utilsapisv1 "github.com/armosec/opa-utils/httpserver/apis/v1"
utilsmetav1 "github.com/armosec/opa-utils/httpserver/meta/v1"
"github.com/gorilla/schema"
"github.com/armosec/kubescape/v2/core/cautils/logger"
"github.com/armosec/kubescape/v2/core/cautils/logger/helpers"
@@ -18,6 +19,21 @@ import (
var OutputDir = "./results"
var FailedOutputDir = "./failed"
type ScanQueryParams struct {
ReturnResults bool `schema:"wait"` // wait for scanning to complete (synchronized request)
KeepResults bool `schema:"keep"` // do not delete results after returning (relevant only for synchronized requests)
}
type ResultsQueryParams struct {
ScanID string `schema:"id"`
KeepResults bool `schema:"keep"` // do not delete results after returning (default will delete results)
AllResults bool `schema:"all"` // delete all results
}
type StatusQueryParams struct {
ScanID string `schema:"id"`
}
type HTTPHandler struct {
state *serverState
}
@@ -28,28 +44,68 @@ func NewHTTPHandler() *HTTPHandler {
}
}
func (handler *HTTPHandler) Scan(w http.ResponseWriter, r *http.Request) {
// ============================================== STATUS ========================================================
// Status API
func (handler *HTTPHandler) Status(w http.ResponseWriter, r *http.Request) {
defer handler.recover(w)
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
response := utilsmetav1.Response{}
w.Header().Set("Content-Type", "application/json")
statusQueryParams := &StatusQueryParams{}
if err := schema.NewDecoder().Decode(statusQueryParams, r.URL.Query()); err != nil {
handler.writeError(w, fmt.Errorf("failed to parse query params, reason: %s", err.Error()))
return
}
if !handler.state.isBusy() {
response.Type = utilsapisv1.NotBusyScanResponseType
w.Write(responseToBytes(&response))
return
}
currentScanID := handler.state.getID()
if statusQueryParams.ScanID != "" && currentScanID != statusQueryParams.ScanID {
response.Type = utilsapisv1.NotBusyScanResponseType
w.Write(responseToBytes(&response))
return
}
response.Response = currentScanID
response.ID = currentScanID
response.Type = utilsapisv1.BusyScanResponseType
w.Write(responseToBytes(&response))
}
// ============================================== SCAN ========================================================
// Scan API - TODO: break down to functions
func (handler *HTTPHandler) Scan(w http.ResponseWriter, r *http.Request) {
defer handler.recover(w)
defer r.Body.Close()
switch r.Method {
case http.MethodGet: // return request template
json.NewEncoder(w).Encode(utilsmetav1.PostScanRequest{})
w.WriteHeader(http.StatusOK)
return
case http.MethodPost:
default:
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
response := utilsmetav1.Response{}
w.Header().Set("Content-Type", "application/json")
scanQueryParams := &ScanQueryParams{}
if err := schema.NewDecoder().Decode(scanQueryParams, r.URL.Query()); err != nil {
handler.writeError(w, fmt.Errorf("failed to parse query params, reason: %s", err.Error()))
return
}
if handler.state.isBusy() {
// TODO - Add to queue
w.WriteHeader(http.StatusOK)
response.Response = []byte(handler.state.getID())
response.Response = handler.state.getID()
response.ID = handler.state.getID()
response.Type = utilsapisv1.IDScanResponseType
w.Write(responseToBytes(&response))
@@ -69,17 +125,17 @@ func (handler *HTTPHandler) Scan(w http.ResponseWriter, r *http.Request) {
handler.writeError(w, fmt.Errorf("failed to read request body, reason: %s", err.Error()))
return
}
logger.L().Info("REST API received scan request", helpers.String("body", string(readBuffer)))
scanRequest := utilsmetav1.PostScanRequest{}
if err := json.Unmarshal(readBuffer, &scanRequest); err != nil {
handler.writeError(w, fmt.Errorf("failed to parse request payload, reason: %s", err.Error()))
return
}
returnResults := r.URL.Query().Has("wait")
keepResults := r.URL.Query().Has("keep")
var wg sync.WaitGroup
if returnResults {
if scanQueryParams.ReturnResults {
wg.Add(1)
} else {
wg.Add(0)
@@ -93,20 +149,20 @@ func (handler *HTTPHandler) Scan(w http.ResponseWriter, r *http.Request) {
results, err := scan(&scanRequest, scanID)
if err != nil {
logger.L().Error("scanning failed", helpers.String("ID", scanID), helpers.Error(err))
if returnResults {
if scanQueryParams.ReturnResults {
response.Type = utilsapisv1.ErrorScanResponseType
response.Response = []byte(err.Error())
response.Response = err.Error()
statusCode = http.StatusInternalServerError
}
} else {
logger.L().Success("done scanning", helpers.String("ID", scanID))
if returnResults {
if scanQueryParams.ReturnResults {
response.Type = utilsapisv1.ResultsV1ScanResponseType
response.Response = results
wg.Done()
}
}
if !keepResults {
if scanQueryParams.ReturnResults && !scanQueryParams.KeepResults {
logger.L().Debug("deleting results", helpers.String("ID", scanID))
removeResultsFile(scanID)
}
@@ -118,6 +174,10 @@ func (handler *HTTPHandler) Scan(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(statusCode)
w.Write(responseToBytes(&response))
}
// ============================================== RESULTS ========================================================
// Results API - TODO: break down to functions
func (handler *HTTPHandler) Results(w http.ResponseWriter, r *http.Request) {
response := utilsmetav1.Response{}
w.Header().Set("Content-Type", "application/json")
@@ -126,25 +186,31 @@ func (handler *HTTPHandler) Results(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var scanID string
if scanID = r.URL.Query().Get("id"); scanID == "" {
scanID = handler.state.getLatestID()
resultsQueryParams := &ResultsQueryParams{}
if err := schema.NewDecoder().Decode(resultsQueryParams, r.URL.Query()); err != nil {
handler.writeError(w, fmt.Errorf("failed to parse query params, reason: %s", err.Error()))
return
}
if scanID == "" { // if no scan found
if resultsQueryParams.ScanID == "" {
resultsQueryParams.ScanID = handler.state.getLatestID()
}
if resultsQueryParams.ScanID == "" { // if no scan found
logger.L().Info("empty scan ID")
w.WriteHeader(http.StatusBadRequest) // Should we return ok?
response.Response = []byte("latest scan not found. trigger again")
response.Response = "latest scan not found. trigger again"
response.Type = utilsapisv1.ErrorScanResponseType
w.Write(responseToBytes(&response))
return
}
response.ID = scanID
response.ID = resultsQueryParams.ScanID
if handler.state.isBusy() { // if requested ID is still scanning
if scanID == handler.state.getID() {
logger.L().Info("scan in process", helpers.String("ID", scanID))
if resultsQueryParams.ScanID == handler.state.getID() {
logger.L().Info("scan in process", helpers.String("ID", resultsQueryParams.ScanID))
w.WriteHeader(http.StatusOK)
response.Response = []byte("scanning in progress")
response.Response = "scanning in progress"
w.Write(responseToBytes(&response))
return
}
@@ -152,29 +218,31 @@ func (handler *HTTPHandler) Results(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
logger.L().Info("requesting results", helpers.String("ID", scanID))
logger.L().Info("requesting results", helpers.String("ID", resultsQueryParams.ScanID))
if !r.URL.Query().Has("keep") {
logger.L().Info("deleting results", helpers.String("ID", scanID))
defer removeResultsFile(scanID)
}
if res, err := readResultsFile(scanID); err != nil {
logger.L().Info("scan result not found", helpers.String("ID", scanID))
if res, err := readResultsFile(resultsQueryParams.ScanID); err != nil {
logger.L().Info("scan result not found", helpers.String("ID", resultsQueryParams.ScanID))
w.WriteHeader(http.StatusNoContent)
response.Response = []byte(err.Error())
response.Response = err.Error()
} else {
logger.L().Info("scan result found", helpers.String("ID", scanID))
logger.L().Info("scan result found", helpers.String("ID", resultsQueryParams.ScanID))
w.WriteHeader(http.StatusOK)
response.Response = res
if !resultsQueryParams.KeepResults {
logger.L().Info("deleting results", helpers.String("ID", resultsQueryParams.ScanID))
defer removeResultsFile(resultsQueryParams.ScanID)
}
}
w.Write(responseToBytes(&response))
case http.MethodDelete:
logger.L().Info("deleting results", helpers.String("ID", scanID))
logger.L().Info("deleting results", helpers.String("ID", resultsQueryParams.ScanID))
if r.URL.Query().Has("all") {
if resultsQueryParams.AllResults {
removeResultDirs()
} else {
removeResultsFile(scanID)
removeResultsFile(resultsQueryParams.ScanID)
}
w.WriteHeader(http.StatusOK)
default:
@@ -202,7 +270,7 @@ func (handler *HTTPHandler) recover(w http.ResponseWriter) {
handler.state.setNotBusy()
logger.L().Error("recover", helpers.Error(fmt.Errorf("%v", err)))
w.WriteHeader(http.StatusInternalServerError)
response.Response = []byte(fmt.Sprintf("%v", err))
response.Response = fmt.Sprintf("%v", err)
response.Type = utilsapisv1.ErrorScanResponseType
w.Write(responseToBytes(&response))
}
@@ -211,7 +279,7 @@ func (handler *HTTPHandler) recover(w http.ResponseWriter) {
func (handler *HTTPHandler) writeError(w http.ResponseWriter, err error) {
response := utilsmetav1.Response{}
w.WriteHeader(http.StatusBadRequest)
response.Response = []byte(err.Error())
response.Response = err.Error()
response.Type = utilsapisv1.ErrorScanResponseType
w.Write(responseToBytes(&response))
handler.state.setNotBusy()

View File

@@ -1,6 +1,7 @@
package v1
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
@@ -10,30 +11,33 @@ import (
"github.com/armosec/kubescape/v2/core/cautils/getter"
"github.com/armosec/kubescape/v2/core/core"
utilsmetav1 "github.com/armosec/opa-utils/httpserver/meta/v1"
reporthandlingv2 "github.com/armosec/opa-utils/reporthandling/v2"
"github.com/armosec/utils-go/boolutils"
)
func scan(scanRequest *utilsmetav1.PostScanRequest, scanID string) ([]byte, error) {
func scan(scanRequest *utilsmetav1.PostScanRequest, scanID string) (*reporthandlingv2.PostureReport, error) {
scanInfo := getScanCommand(scanRequest, scanID)
ks := core.NewKubescape()
result, err := ks.Scan(scanInfo)
if err != nil {
return []byte{}, writeScanErrorToFile(err, scanID)
return nil, writeScanErrorToFile(err, scanID)
}
if err := result.HandleResults(); err != nil {
return nil, err
}
b, err := result.ToJson()
if err != nil {
err = fmt.Errorf("failed to parse scan results to json, reason: %s", err.Error())
}
return b, err
return result.GetResults(), nil
}
func readResultsFile(fileID string) ([]byte, error) {
func readResultsFile(fileID string) (*reporthandlingv2.PostureReport, error) {
if fileName := searchFile(fileID); fileName != "" {
return os.ReadFile(fileName)
f, err := os.ReadFile(fileName)
if err != nil {
return nil, err
}
postureReport := &reporthandlingv2.PostureReport{}
err = json.Unmarshal(f, postureReport)
return postureReport, err
}
return nil, fmt.Errorf("file %s not found", fileID)
}

View File

@@ -15,6 +15,7 @@ import (
const (
scanPath = "/v1/scan"
statusPath = "/v1/status"
resultsPath = "/v1/results"
prometheusMmeticsPath = "/v1/metrics"
livePath = "/livez"
@@ -44,6 +45,7 @@ func SetupHTTPListener() error {
rtr.HandleFunc(prometheusMmeticsPath, httpHandler.Metrics)
rtr.HandleFunc(scanPath, httpHandler.Scan)
rtr.HandleFunc(statusPath, httpHandler.Status)
rtr.HandleFunc(resultsPath, httpHandler.Results)
rtr.HandleFunc(livePath, httpHandler.Live)
rtr.HandleFunc(readyPath, httpHandler.Ready)