diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d86c8590..247f1e2b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -51,7 +51,7 @@ jobs: ArmoERServer: report.armo.cloud ArmoWebsite: portal.armo.cloud CGO_ENABLED: 0 - run: python3 --version && python3 build.py + run: cd cmd && python3 --version && python3 build.py - name: Smoke Testing env: diff --git a/.github/workflows/build_dev.yaml b/.github/workflows/build_dev.yaml index 658dc4d3..51526340 100644 --- a/.github/workflows/build_dev.yaml +++ b/.github/workflows/build_dev.yaml @@ -35,7 +35,7 @@ jobs: ArmoERServer: report.armo.cloud ArmoWebsite: portal.armo.cloud CGO_ENABLED: 0 - run: python3 --version && python3 build.py + run: cd cmd && python3 --version && python3 build.py - name: Smoke Testing env: diff --git a/.github/workflows/master_pr_checks.yaml b/.github/workflows/master_pr_checks.yaml index 0458eb49..1cc5d873 100644 --- a/.github/workflows/master_pr_checks.yaml +++ b/.github/workflows/master_pr_checks.yaml @@ -19,8 +19,14 @@ jobs: with: go-version: 1.17 - - name: Test - run: go test -v ./... + - name: Test cmd pkg + run: cd cmd && go test -v ./... + + - name: Test core pkg + run: cd core && go test -v ./... + + - name: Test httphandler pkg + run: cd httphandler && go test -v ./... - name: Build env: @@ -30,7 +36,7 @@ jobs: ArmoERServer: report.armo.cloud ArmoWebsite: portal.armo.cloud CGO_ENABLED: 0 - run: python3 --version && python3 build.py + run: cd cmd && python3 --version && python3 build.py - name: Smoke Testing env: diff --git a/build/Dockerfile b/build/Dockerfile index 600bcb5e..bb00a3f0 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -17,13 +17,14 @@ RUN pip3 install --no-cache --upgrade pip setuptools WORKDIR /work ADD . . +WORKDIR /work/httphandler RUN python build.py RUN ls -ltr build/ubuntu-latest FROM alpine -COPY --from=builder /work/build/ubuntu-latest/kubescape /usr/bin/kubescape +COPY --from=builder /work/httphandler/build/ubuntu-latest/kubescape /usr/bin/kubescape # # Download the frameworks. Use the "--use-default" flag when running kubescape # RUN kubescape download framework nsa && kubescape download framework mitre diff --git a/build.py b/cmd/build.py similarity index 98% rename from build.py rename to cmd/build.py index 249e5644..9194802a 100644 --- a/build.py +++ b/cmd/build.py @@ -18,7 +18,7 @@ def checkStatus(status, msg): def getBuildDir(): currentPlatform = platform.system() - buildDir = "build/" + buildDir = "../build/" if currentPlatform == "Windows": buildDir += "windows-latest" elif currentPlatform == "Linux": buildDir += "ubuntu-latest" @@ -70,7 +70,7 @@ def main(): ldflags += " -X {}={}".format(WEBSITE_CONST, ArmoWebsite) if ArmoAuthServer: ldflags += " -X {}={}".format(AUTH_SERVER_CONST, ArmoAuthServer) - + build_command = ["go", "build", "-o", ks_file, "-ldflags" ,ldflags] print("Building kubescape and saving here: {}".format(ks_file)) diff --git a/core/cautils/versioncheck.go b/core/cautils/versioncheck.go index 02e58414..14113401 100644 --- a/core/cautils/versioncheck.go +++ b/core/cautils/versioncheck.go @@ -13,7 +13,8 @@ import ( "golang.org/x/mod/semver" ) -const SKIP_VERSION_CHECK = "KUBESCAPE_SKIP_UPDATE_CHECK" +const SKIP_VERSION_CHECK_DEPRECATED = "KUBESCAPE_SKIP_UPDATE_CHECK" +const SKIP_VERSION_CHECK = "KS_SKIP_UPDATE_CHECK" var BuildNumber string @@ -29,6 +30,8 @@ func NewIVersionCheckHandler() IVersionCheckHandler { } if v, ok := os.LookupEnv(SKIP_VERSION_CHECK); ok && pkgutils.StringToBool(v) { return NewVersionCheckHandlerMock() + } else if v, ok := os.LookupEnv(SKIP_VERSION_CHECK_DEPRECATED); ok && pkgutils.StringToBool(v) { + return NewVersionCheckHandlerMock() } return NewVersionCheckHandler() } diff --git a/httphandler/README.md b/httphandler/README.md index 995cd07b..82c74707 100644 --- a/httphandler/README.md +++ b/httphandler/README.md @@ -5,7 +5,7 @@ Running `kubescape` will start up a webserver on port `8080` which will serve the following paths: * POST `/v1/scan` - Trigger a kubescape scan. The server will return an ID and will execute the scanning asynchronously -* * `synchronously`: scan synchronously (return results and not ID). Use only in small clusters are with an increased timeout +* * `wait`: scan synchronously (return results and not ID). Use only in small clusters are with an increased timeout * GET `/v1/results` - Request kubescape scan results * * query `id=` -> ID returned when triggering the scan action. If empty will return latest results * * query `remove` -> Remove results from storage after reading the results @@ -20,15 +20,15 @@ Running `kubescape` will start up a webserver on port `8080` which will serve th POST /v1/results body: -```json +``` { - "format": "", // results format [default: json] (same as 'kubescape scan --format') - "excludedNamespaces": null, // list of namespaces to exclude (same as 'kubescape scan --excluded-namespaces') - "includeNamespaces": null, // list of namespaces to include (same as 'kubescape scan --include-namespaces') - "submit": false, // submit results to Kubescape cloud (same as 'kubescape scan --submit') - "hostScanner": false, // deploy kubescape K8s host-scanner DaemonSet in the scanned cluster (same as 'kubescape scan --enable-host-scan') - "keepLocal": false, // do not submit results to Kubescape cloud (same as 'kubescape scan --keep-local') - "account": "" // account ID (same as 'kubescape scan --account') + "format": , // 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') + "submit": , // submit results to Kubescape cloud (same as 'kubescape scan --submit') + "hostScanner": , // deploy kubescape K8s host-scanner DaemonSet in the scanned cluster (same as 'kubescape scan --enable-host-scan') + "keepLocal": , // do not submit results to Kubescape cloud (same as 'kubescape scan --keep-local') + "account": // account ID (same as 'kubescape scan --account') } ``` @@ -37,11 +37,10 @@ e.g.: ```bash curl --header "Content-Type: application/json" \ --request POST \ - --data '{"account":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","hostScanner":true, "submit":true}' \ + --data '{"hostScanner":true, "submit":true}' \ http://127.0.0.1:8080/v1/scan ``` -## Installation into kubernetes +## Examples -The [yaml](ks-prometheus-support.yaml) file will deploy one instance of kubescape (with all relevant dependencies) to run on your cluster - -**NOTE** Make sure the configurations suit your cluster (e.g. `serviceType`, namespace, etc.) \ No newline at end of file +* [Prometheus](examples/prometheus/README.md) +* [Microservice](examples/microservice/README.md) diff --git a/httphandler/build.py b/httphandler/build.py new file mode 100644 index 00000000..7e333399 --- /dev/null +++ b/httphandler/build.py @@ -0,0 +1,92 @@ +import os +import sys +import hashlib +import platform +import subprocess + +BASE_GETTER_CONST = "github.com/armosec/kubescape/core/cautils/getter" +BE_SERVER_CONST = BASE_GETTER_CONST + ".ArmoBEURL" +ER_SERVER_CONST = BASE_GETTER_CONST + ".ArmoERURL" +WEBSITE_CONST = BASE_GETTER_CONST + ".ArmoFEURL" +AUTH_SERVER_CONST = BASE_GETTER_CONST + ".armoAUTHURL" + +def checkStatus(status, msg): + if status != 0: + sys.stderr.write(msg) + exit(status) + + +def getBuildDir(): + currentPlatform = platform.system() + buildDir = "build/" + + if currentPlatform == "Windows": return os.path.join(buildDir, "windows-latest") + if currentPlatform == "Linux": return os.path.join(buildDir, "ubuntu-latest") + if currentPlatform == "Darwin": return os.path.join(buildDir, "macos-latest") + raise OSError("Platform %s is not supported!" % (currentPlatform)) + +def getPackageName(): + packageName = "kubescape" + # if platform.system() == "Windows": packageName += ".exe" + + return packageName + + +def main(): + print("Building Kubescape") + + # print environment variables + # print(os.environ) + + # Set some variables + packageName = getPackageName() + buildUrl = "github.com/armosec/kubescape/core/cautils.BuildNumber" + releaseVersion = os.getenv("RELEASE") + ArmoBEServer = os.getenv("ArmoBEServer") + ArmoERServer = os.getenv("ArmoERServer") + ArmoWebsite = os.getenv("ArmoWebsite") + ArmoAuthServer = os.getenv("ArmoAuthServer") + + # Create build directory + buildDir = getBuildDir() + + ks_file = os.path.join(buildDir, packageName) + hash_file = ks_file + ".sha256" + + if not os.path.isdir(buildDir): + os.makedirs(buildDir) + + # Build kubescape + ldflags = "-w -s" + if releaseVersion: + ldflags += " -X {}={}".format(buildUrl, releaseVersion) + if ArmoBEServer: + ldflags += " -X {}={}".format(BE_SERVER_CONST, ArmoBEServer) + if ArmoERServer: + ldflags += " -X {}={}".format(ER_SERVER_CONST, ArmoERServer) + if ArmoWebsite: + ldflags += " -X {}={}".format(WEBSITE_CONST, ArmoWebsite) + if ArmoAuthServer: + ldflags += " -X {}={}".format(AUTH_SERVER_CONST, ArmoAuthServer) + + build_command = ["go", "build", "-o", ks_file, "-ldflags" ,ldflags] + + print("Building kubescape and saving here: {}".format(ks_file)) + print("Build command: {}".format(" ".join(build_command))) + + status = subprocess.call(build_command) + checkStatus(status, "Failed to build kubescape") + + sha256 = hashlib.sha256() + with open(ks_file, "rb") as kube: + sha256.update(kube.read()) + with open(hash_file, "w") as kube_sha: + hash = sha256.hexdigest() + print("kubescape hash: {}, file: {}".format(hash, hash_file)) + kube_sha.write(sha256.hexdigest()) + + print("Build Done") + + +if __name__ == "__main__": + main() diff --git a/httphandler/examples/microservice/README.md b/httphandler/examples/microservice/README.md new file mode 100644 index 00000000..c6b7185d --- /dev/null +++ b/httphandler/examples/microservice/README.md @@ -0,0 +1,20 @@ +# Kubescape as a microservice + +1. Deploy kubescape microservice + ```bash + kubectl apply -f https://raw.githubusercontent.com/armosec/kubescape/master/httphandler/examples/prometheus/kubescape.yaml + ``` + > **NOTE** Make sure the configurations suit your cluster (e.g. `serviceType`, namespace, etc.) + +2. Trigger scan + ```bash + curl --header "Content-Type: application/json" \ + --request POST \ + --data '{"account":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","hostScanner":true, "submit":true}' \ + http://127.0.0.1:8080/v1/scan + ``` + +3. Get results + ```bash + curl --request GET http://127.0.0.1:8080/v1/results -o results.json + ``` \ No newline at end of file diff --git a/httphandler/examples/prometheus/README.md b/httphandler/examples/prometheus/README.md new file mode 100644 index 00000000..34d75c29 --- /dev/null +++ b/httphandler/examples/prometheus/README.md @@ -0,0 +1,19 @@ +# Prometheus Kubescape Integration + +1. Deploy kubescape + ```bash + kubectl apply -f https://raw.githubusercontent.com/armosec/kubescape/master/httphandler/examples/prometheus/kubescape.yaml + ``` + > **NOTE** Make sure the configurations suit your cluster (e.g. `serviceType`, etc.) + +2. Deploy kube-prometheus-stack + ```bash + helm repo add prometheus-community https://prometheus-community.github.io/helm-charts + helm repo update + helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false,prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false + ``` +3. Deploy pod monitor + ```bash + kubectl apply -f https://raw.githubusercontent.com/armosec/kubescape/master/httphandler/examples/prometheus/kubescape.yaml + ``` + diff --git a/httphandler/ks-prometheus-support.yaml b/httphandler/examples/prometheus/ks-deployment.yaml similarity index 75% rename from httphandler/ks-prometheus-support.yaml rename to httphandler/examples/prometheus/ks-deployment.yaml index 4f0488d9..2a664225 100644 --- a/httphandler/ks-prometheus-support.yaml +++ b/httphandler/examples/prometheus/ks-deployment.yaml @@ -51,6 +51,7 @@ spec: type: NodePort ports: - port: 8080 + name: http targetPort: 8080 protocol: TCP selector: @@ -76,26 +77,30 @@ spec: serviceAccountName: kubescape-discovery containers: - name: kubescape - # livenessProbe: - # httpGet: - # path: /livez - # port: 8080 - # initialDelaySeconds: 3 - # periodSeconds: 3 - # readinessProbe: - # httpGet: - # path: /readyz - # port: 8080 - # initialDelaySeconds: 3 - # periodSeconds: 3 - image: quay.io/armosec/kubescape:prometheus.v1 + livenessProbe: + httpGet: + path: /livez + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /readyz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + image: quay.io/armosec/kubescape:prometheus.v2 env: - - name: KS_RUN_PROMETHEUS_SERVER - value: "true" - name: KS_DEFAULT_CONFIGMAP_NAMESPACE value: "ks-scanner" + - name: "KS_SKIP_UPDATE_CHECK" # do not check latest version + value: "true" + - name: KS_ENABLE_HOST_SCANNER # enable host scanner -> https://hub.armo.cloud/docs/host-sensor + value: "true" ports: - containerPort: 8080 + name: http + protocol: TCP command: - kubescape resources: diff --git a/httphandler/examples/prometheus/podmonitor.yaml b/httphandler/examples/prometheus/podmonitor.yaml new file mode 100644 index 00000000..1fe3f487 --- /dev/null +++ b/httphandler/examples/prometheus/podmonitor.yaml @@ -0,0 +1,15 @@ +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: kubescape + namespace: ks-scanner + labels: + app: kubescape +spec: + selector: + matchLabels: + app: kubescape + podMetricsEndpoints: + - port: http + interval: 120s + scrapeTimeout: 120s diff --git a/httphandler/handlerequests/v1/prometheus.go b/httphandler/handlerequests/v1/prometheus.go index 6de638dc..8ad68488 100644 --- a/httphandler/handlerequests/v1/prometheus.go +++ b/httphandler/handlerequests/v1/prometheus.go @@ -50,10 +50,11 @@ func (handler *HTTPHandler) Metrics(w http.ResponseWriter, r *http.Request) { func getPrometheusDefaultScanCommand(scanID string) *cautils.ScanInfo { scanInfo := cautils.ScanInfo{} scanInfo.FrameworkScan = true - scanInfo.ScanAll = true // scan all frameworks - scanInfo.ReportID = scanID // scan ID - scanInfo.HostSensorEnabled.Set(os.Getenv("KS_ENABLE_HOST_SENSOR")) // enable host scanner - scanInfo.FailThreshold = 100 // Do not fail scanning - // scanInfo.Format = "prometheus" // results format + scanInfo.ScanAll = true // scan all frameworks + scanInfo.ReportID = scanID // scan ID + scanInfo.HostSensorEnabled.Set(os.Getenv("KS_ENABLE_HOST_SCANNER")) // enable host scanner + scanInfo.FailThreshold = 100 // Do not fail scanning + scanInfo.Format = "prometheus" // results format + scanInfo.Local = true // Do not publish results to Kubescape SaaS return &scanInfo } diff --git a/httphandler/handlerequests/v1/requestshandler.go b/httphandler/handlerequests/v1/requestshandler.go index 4aa2a47d..425c5292 100644 --- a/httphandler/handlerequests/v1/requestshandler.go +++ b/httphandler/handlerequests/v1/requestshandler.go @@ -128,7 +128,6 @@ func (handler *HTTPHandler) Results(w http.ResponseWriter, r *http.Request) { if scanID = r.URL.Query().Get("scanID"); scanID == "" { scanID = handler.state.getLatestID() } - logger.L().Info("requesting results", helpers.String("ID", scanID)) if handler.state.isBusy() { // if requested ID is still scanning if scanID == handler.state.getID() { @@ -141,6 +140,8 @@ 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)) + if r.URL.Query().Has("remove") { defer removeResultsFile(scanID) } @@ -152,6 +153,8 @@ func (handler *HTTPHandler) Results(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } case http.MethodDelete: + logger.L().Info("deleting results", helpers.String("ID", scanID)) + if r.URL.Query().Has("all") { removeResultDirs() } else { diff --git a/httphandler/handlerequests/v1/requestshandlerutils.go b/httphandler/handlerequests/v1/requestshandlerutils.go index 49cb248a..736a876e 100644 --- a/httphandler/handlerequests/v1/requestshandlerutils.go +++ b/httphandler/handlerequests/v1/requestshandlerutils.go @@ -11,6 +11,7 @@ import ( func scan(scanRequest *PostScanRequest, scanID string) ([]byte, error) { scanInfo := getScanCommand(scanRequest, scanID) + ks := core.NewKubescape() result, err := ks.Scan(scanInfo) if err != nil { @@ -34,7 +35,7 @@ func readResultsFile(fileID string) ([]byte, error) { if fileName := searchFile(fileID); fileName != "" { return os.ReadFile(fileName) } - return nil, fmt.Errorf("file not found") + return nil, fmt.Errorf("file %s not found", fileID) } func removeResultDirs() { @@ -59,7 +60,7 @@ func searchFile(fileID string) string { func findFile(targetDir string, fileName string) (string, error) { - matches, err := filepath.Glob(targetDir + fileName) + matches, err := filepath.Glob(filepath.Join(targetDir, fileName)) if err != nil { return "", err }