Compare commits

...

2 Commits

Author SHA1 Message Date
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
20 changed files with 261 additions and 155 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

@@ -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

@@ -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

@@ -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

@@ -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

2
go.mod
View File

@@ -6,7 +6,7 @@ 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/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

5
go.sum
View File

@@ -113,7 +113,6 @@ 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=
@@ -123,8 +122,8 @@ github.com/armosec/k8s-interface v0.0.37/go.mod h1:vHxGWqD/uh6+GQb9Sqv7OGMs+Rvc2
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/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
)

View File

@@ -123,8 +123,9 @@ github.com/armosec/k8s-interface v0.0.37/go.mod h1:vHxGWqD/uh6+GQb9Sqv7OGMs+Rvc2
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/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 +463,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))
@@ -75,11 +131,8 @@ func (handler *HTTPHandler) Scan(w http.ResponseWriter, r *http.Request) {
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 +146,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 +171,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 +183,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 +215,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 +267,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 +276,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)