From 7c2c87001b9f7a75d8af22d7af836071ef80e645 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Fri, 12 Mar 2021 08:29:10 +0530 Subject: [PATCH 01/38] add support for k8s validating webhooks --- .gitignore | 5 +- ...upport-Admission-Controller-Webhooks.patch | 13107 ++++++++++++++++ Makefile | 2 +- build/Dockerfile | 7 +- deploy/docker-compose.yml | 1 + .../admission-controller-webhooks-usage.md | 108 + go.mod | 3 + go.sum | 1 + pkg/cli/server.go | 12 +- pkg/config/config-reader.go | 5 + pkg/config/types.go | 7 + pkg/http-server/assets/icons.svg | 1 + pkg/http-server/assets/jsonTree.css | 107 + pkg/http-server/assets/jsonTree.js | 819 + pkg/http-server/assets/moment.js | 5670 +++++++ pkg/http-server/assets/webhook-scan-logs.css | 33 + pkg/http-server/assets/webhook-scan-logs.js | 44 + pkg/http-server/constants.go | 3 + pkg/http-server/handler.go | 9 +- pkg/http-server/handler_test.go | 6 +- pkg/http-server/health_test.go | 2 +- .../k8s_testdata/config-deny-category.toml | 5 + .../k8s_testdata/config-deny-high.toml | 5 + .../config-deny-non-existing-category.toml | 8 + .../k8s_testdata/config-medium-severity.toml | 2 + .../k8s_testdata/config-specific-rule.toml | 5 + pkg/http-server/k8s_testdata/empty.json | 0 .../k8s_testdata/empty_object.json | 7 + pkg/http-server/k8s_testdata/invalid.json | 1 + .../k8s_testdata/risky_testconfig.json | 27 + pkg/http-server/k8s_testdata/testconfig.json | 27 + .../kubernetes_pod/AC-K8-CA-PO-H-0165.json | 21 + .../kubernetes_pod/AC-K8-DS-PO-M-0176.json | 14 + .../kubernetes_pod/AC-K8-DS-PO-M-0177.json | 14 + .../kubernetes_pod/AC-K8-IA-PO-H-0106.json | 14 + .../kubernetes_pod/AC-K8-IA-PO-H-0137.json | 14 + .../kubernetes_pod/AC-K8-IA-PO-H-0138.json | 14 + .../kubernetes_pod/AC-K8-IA-PO-H-0168.json | 21 + .../kubernetes_pod/AC-K8-IA-PO-M-0105.json | 14 + .../kubernetes_pod/AC-K8-IA-PO-M-0135.json | 14 + .../kubernetes_pod/AC-K8-IA-PO-M-0139.json | 14 + .../kubernetes_pod/AC-K8-IA-PO-M-0140.json | 21 + .../kubernetes_pod/AC-K8-IA-PO-M-0141.json | 14 + .../kubernetes_pod/AC-K8-IA-PO-M-0143.json | 22 + .../kubernetes_pod/AC-K8-IA-PO-M-0162.json | 16 + .../kubernetes_pod/AC-K8-IA-PS-M-0112.json | 15 + .../kubernetes_pod/AC-K8-NS-PO-H-0117.json | 14 + .../kubernetes_pod/AC-K8-NS-PO-H-0170.json | 14 + .../kubernetes_pod/AC-K8-NS-PO-M-0122.json | 14 + .../kubernetes_pod/AC-K8-NS-PO-M-0133.json | 14 + .../kubernetes_pod/AC-K8-NS-PO-M-0163.json | 16 + .../kubernetes_pod/AC-K8-NS-PO-M-0164.json | 16 + .../kubernetes_pod/AC-K8-NS-PO-M-0171.json | 17 + .../kubernetes_pod/AC-K8-NS-PO-M-0182.json | 14 + .../kubernetes_pod/AC-K8-OE-PK-M-0034.json | 19 + .../kubernetes_pod/AC-K8-OE-PK-M-0155.json | 21 + .../kubernetes_pod/AC-K8-OE-PK-M-0156.json | 21 + .../kubernetes_pod/AC-K8-OE-PK-M-0157.json | 21 + .../kubernetes_pod/AC-K8-OE-PK-M-0158.json | 21 + .../kubernetes_pod/AC-K8-OE-PO-L-0129.json | 16 + .../kubernetes_pod/AC-K8-OE-PO-L-0130.json | 16 + .../kubernetes_pod/AC-K8-OE-PO-L-0134.json | 14 + .../kubernetes_pod/AC-K8-OE-PO-M-0166.json | 14 + .../kubernetes_pod/allowedHostPath.rego | 107 + .../kubernetes_pod/allowedProcMount.rego | 126 + .../kubernetes_pod/allowedVolumes.rego | 58 + .../accurics.kubernetes.IAM.73.json | 16 + .../accurics.kubernetes.IAM.74.json | 16 + .../accurics.kubernetes.IAM.75.json | 16 + .../accurics.kubernetes.IAM.76.json | 16 + .../accurics.kubernetes.IAM.77.json | 16 + .../accurics.kubernetes.IAM.78.json | 16 + .../accurics.kubernetes.IAM.79.json | 16 + .../accurics.kubernetes.IAM.80.json | 16 + .../accurics.kubernetes.IAM.81.json | 16 + .../accurics.kubernetes.IAM.82.json | 16 + .../accurics.kubernetes.IAM.83.json | 16 + .../accurics.kubernetes.IAM.84.json | 16 + .../accurics.kubernetes.IAM.85.json | 16 + .../accurics.kubernetes.IAM.86.json | 16 + .../accurics.kubernetes.IAM.87.json | 16 + .../accurics.kubernetes.IAM.88.json | 16 + .../containerHasAllowedCapabilities.rego | 119 + .../kubernetes_pod/appArmorProfile.rego | 108 + .../kubernetes_pod/autoMountTokenEnabled.rego | 33 + .../kubernetes_pod/capSysAdminUsed.rego | 69 + .../kubernetes_pod/capabilityUsed.rego | 74 + .../kubernetes_pod/commandCheck.rego | 19 + .../accurics.kubernetes.IAM.105.json | 16 + .../accurics.kubernetes.IAM.106.json | 16 + .../accurics.kubernetes.IAM.108.json | 16 + .../accurics.kubernetes.IAM.109.json | 16 + .../accurics.kubernetes.IAM.110.json | 16 + .../accurics.kubernetes.IAM.111.json | 16 + .../accurics.kubernetes.IAM.112.json | 16 + .../accurics.kubernetes.IAM.113.json | 16 + .../accurics.kubernetes.IAM.114.json | 16 + .../accurics.kubernetes.IAM.115.json | 16 + .../accurics.kubernetes.IAM.116.json | 16 + .../accurics.kubernetes.IAM.117.json | 16 + .../accurics.kubernetes.IAM.118.json | 16 + .../accurics.kubernetes.IAM.119.json | 16 + .../accurics.kubernetes.IAM.120.json | 16 + .../containerResourcesNotDefined.rego | 111 + .../accurics.kubernetes.EKM.57.json | 16 + .../accurics.kubernetes.EKM.58.json | 16 + .../accurics.kubernetes.EKM.59.json | 16 + .../accurics.kubernetes.EKM.60.json | 16 + .../accurics.kubernetes.EKM.61.json | 16 + .../accurics.kubernetes.EKM.62.json | 16 + .../accurics.kubernetes.EKM.63.json | 16 + .../accurics.kubernetes.EKM.64.json | 16 + .../accurics.kubernetes.EKM.65.json | 16 + .../accurics.kubernetes.EKM.66.json | 16 + .../accurics.kubernetes.EKM.67.json | 16 + .../accurics.kubernetes.EKM.68.json | 16 + .../accurics.kubernetes.EKM.69.json | 16 + .../accurics.kubernetes.EKM.70.json | 16 + .../accurics.kubernetes.EKM.71.json | 16 + .../accurics.kubernetes.EKM.72.json | 16 + .../containerUsesSecretsInEnvironmentVar.rego | 115 + .../kubernetes_pod/containersAsHighUID.rego | 102 + .../kubernetes_pod/disallowedSysCalls.rego | 51 + .../AC-K8-DS-PO-M-0143.json | 14 + .../disallowed_volumes/disAllowedVolumes.rego | 52 + .../kubernetes_pod/dockerSockCheck.rego | 35 + .../kubernetes_pod/imageWithLatestTag.rego | 196 + .../kubernetes_pod/imageWithoutDigest.rego | 105 + .../kubernetes_pod/kubeDashboardEnabled.rego | 6 + .../kubernetes_pod/otherNamespace.rego | 20 + .../priviledgedContainersEnabled.rego | 11 + .../kubernetes_pod/probeCheck.rego | 68 + .../kubernetes_pod/secCompProfile.rego | 153 + .../kubernetes_pod/secretsAsEnvVariables.rego | 75 + .../kubernetes_pod/securityContextCheck.rego | 76 + .../kubernetes_pod/securityContextUsed.rego | 103 + .../kubernetes_pod/specBoolCheck.rego | 36 + .../kubernetes_pod/tillerDeployed.rego | 35 + pkg/http-server/routes.go | 10 +- pkg/http-server/routes_test.go | 2 +- pkg/http-server/server.go | 3 +- pkg/http-server/start.go | 27 +- pkg/http-server/templates/index.html | 34 + pkg/http-server/templates/show.html | 40 + pkg/http-server/webhook-deny-rule-matcher.go | 35 + .../webhook-deny-rule-matcher_test.go | 98 + pkg/http-server/webhook-scan-logger.go | 196 + pkg/http-server/webhook-scan-logger_test.go | 78 + pkg/http-server/webhook-scan-logs.go | 242 + pkg/http-server/webhook-scan.go | 297 + pkg/http-server/webhook-scan_test.go | 244 + pkg/initialize/run.go | 2 +- pkg/runtime/executor_test.go | 3 - .../scan-skip-rules-low-severity.toml | 2 +- 154 files changed, 24681 insertions(+), 24 deletions(-) create mode 100644 0001-K8S-Support-Admission-Controller-Webhooks.patch create mode 100644 docs/getting-started/admission-controller-webhooks-usage.md create mode 100644 pkg/http-server/assets/icons.svg create mode 100644 pkg/http-server/assets/jsonTree.css create mode 100644 pkg/http-server/assets/jsonTree.js create mode 100644 pkg/http-server/assets/moment.js create mode 100644 pkg/http-server/assets/webhook-scan-logs.css create mode 100644 pkg/http-server/assets/webhook-scan-logs.js create mode 100644 pkg/http-server/k8s_testdata/config-deny-category.toml create mode 100644 pkg/http-server/k8s_testdata/config-deny-high.toml create mode 100644 pkg/http-server/k8s_testdata/config-deny-non-existing-category.toml create mode 100644 pkg/http-server/k8s_testdata/config-medium-severity.toml create mode 100644 pkg/http-server/k8s_testdata/config-specific-rule.toml create mode 100644 pkg/http-server/k8s_testdata/empty.json create mode 100644 pkg/http-server/k8s_testdata/empty_object.json create mode 100644 pkg/http-server/k8s_testdata/invalid.json create mode 100644 pkg/http-server/k8s_testdata/risky_testconfig.json create mode 100644 pkg/http-server/k8s_testdata/testconfig.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego create mode 100644 pkg/http-server/templates/index.html create mode 100644 pkg/http-server/templates/show.html create mode 100644 pkg/http-server/webhook-deny-rule-matcher.go create mode 100644 pkg/http-server/webhook-deny-rule-matcher_test.go create mode 100644 pkg/http-server/webhook-scan-logger.go create mode 100644 pkg/http-server/webhook-scan-logger_test.go create mode 100644 pkg/http-server/webhook-scan-logs.go create mode 100644 pkg/http-server/webhook-scan.go create mode 100644 pkg/http-server/webhook-scan_test.go diff --git a/.gitignore b/.gitignore index fc0803b00..5dc0e2f3b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ docs/_build/ #vscode .vscode/ +#GoLand +.idea + /updatedFiles # Go binar @@ -30,4 +33,4 @@ docs/_build/ .DS_Store -vendor/ \ No newline at end of file +vendor/ diff --git a/0001-K8S-Support-Admission-Controller-Webhooks.patch b/0001-K8S-Support-Admission-Controller-Webhooks.patch new file mode 100644 index 000000000..fe3a1e5aa --- /dev/null +++ b/0001-K8S-Support-Admission-Controller-Webhooks.patch @@ -0,0 +1,13107 @@ +From c64cc0c7d05a0fff5c2dc112ba7104f01788f87d Mon Sep 17 00:00:00 2001 +From: Shay Ya'ari +Date: Tue, 2 Mar 2021 11:01:05 +0200 +Subject: [PATCH] K8S Support Admission Controller Webhooks + +Use K8s Admission Review Objects + +Comments +--- + .gitignore | 5 +- + build/Dockerfile | 7 +- + deploy/docker-compose.yml | 1 + + .../admission-controller-webhooks-usage.md | 108 + + go.mod | 3 + + go.sum | 8 +- + pkg/cli/server.go | 12 +- + pkg/config/config-reader.go | 5 + + pkg/config/types.go | 9 +- + pkg/http-server/assets/icons.svg | 1 + + pkg/http-server/assets/jsonTree.css | 107 + + pkg/http-server/assets/jsonTree.js | 819 +++ + pkg/http-server/assets/moment.js | 5670 +++++++++++++++++ + pkg/http-server/assets/webhook-scan-logs.css | 33 + + pkg/http-server/assets/webhook-scan-logs.js | 44 + + pkg/http-server/constants.go | 3 + + pkg/http-server/handler.go | 7 +- + pkg/http-server/handler_test.go | 6 +- + pkg/http-server/health_test.go | 2 +- + .../k8s_testdata/config-deny-category.toml | 5 + + .../k8s_testdata/config-deny-high.toml | 5 + + .../config-deny-non-existing-category.toml | 8 + + .../k8s_testdata/config-medium-severity.toml | 2 + + .../k8s_testdata/config-specific-rule.toml | 5 + + pkg/http-server/k8s_testdata/empty.json | 0 + .../k8s_testdata/empty_object.json | 7 + + pkg/http-server/k8s_testdata/invalid.json | 1 + + .../k8s_testdata/risky_testconfig.json | 27 + + pkg/http-server/k8s_testdata/testconfig.json | 27 + + .../kubernetes_pod/AC-K8-CA-PO-H-0165.json | 21 + + .../kubernetes_pod/AC-K8-DS-PO-M-0176.json | 14 + + .../kubernetes_pod/AC-K8-DS-PO-M-0177.json | 14 + + .../kubernetes_pod/AC-K8-IA-PO-H-0106.json | 14 + + .../kubernetes_pod/AC-K8-IA-PO-H-0137.json | 14 + + .../kubernetes_pod/AC-K8-IA-PO-H-0138.json | 14 + + .../kubernetes_pod/AC-K8-IA-PO-H-0168.json | 21 + + .../kubernetes_pod/AC-K8-IA-PO-M-0105.json | 14 + + .../kubernetes_pod/AC-K8-IA-PO-M-0135.json | 14 + + .../kubernetes_pod/AC-K8-IA-PO-M-0139.json | 14 + + .../kubernetes_pod/AC-K8-IA-PO-M-0140.json | 21 + + .../kubernetes_pod/AC-K8-IA-PO-M-0141.json | 14 + + .../kubernetes_pod/AC-K8-IA-PO-M-0143.json | 22 + + .../kubernetes_pod/AC-K8-IA-PO-M-0162.json | 16 + + .../kubernetes_pod/AC-K8-IA-PS-M-0112.json | 15 + + .../kubernetes_pod/AC-K8-NS-PO-H-0117.json | 14 + + .../kubernetes_pod/AC-K8-NS-PO-H-0170.json | 14 + + .../kubernetes_pod/AC-K8-NS-PO-M-0122.json | 14 + + .../kubernetes_pod/AC-K8-NS-PO-M-0133.json | 14 + + .../kubernetes_pod/AC-K8-NS-PO-M-0163.json | 16 + + .../kubernetes_pod/AC-K8-NS-PO-M-0164.json | 16 + + .../kubernetes_pod/AC-K8-NS-PO-M-0171.json | 17 + + .../kubernetes_pod/AC-K8-NS-PO-M-0182.json | 14 + + .../kubernetes_pod/AC-K8-OE-PK-M-0034.json | 19 + + .../kubernetes_pod/AC-K8-OE-PK-M-0155.json | 21 + + .../kubernetes_pod/AC-K8-OE-PK-M-0156.json | 21 + + .../kubernetes_pod/AC-K8-OE-PK-M-0157.json | 21 + + .../kubernetes_pod/AC-K8-OE-PK-M-0158.json | 21 + + .../kubernetes_pod/AC-K8-OE-PO-L-0129.json | 16 + + .../kubernetes_pod/AC-K8-OE-PO-L-0130.json | 16 + + .../kubernetes_pod/AC-K8-OE-PO-L-0134.json | 14 + + .../kubernetes_pod/AC-K8-OE-PO-M-0166.json | 14 + + .../kubernetes_pod/allowedHostPath.rego | 107 + + .../kubernetes_pod/allowedProcMount.rego | 126 + + .../kubernetes_pod/allowedVolumes.rego | 58 + + .../accurics.kubernetes.IAM.73.json | 16 + + .../accurics.kubernetes.IAM.74.json | 16 + + .../accurics.kubernetes.IAM.75.json | 16 + + .../accurics.kubernetes.IAM.76.json | 16 + + .../accurics.kubernetes.IAM.77.json | 16 + + .../accurics.kubernetes.IAM.78.json | 16 + + .../accurics.kubernetes.IAM.79.json | 16 + + .../accurics.kubernetes.IAM.80.json | 16 + + .../accurics.kubernetes.IAM.81.json | 16 + + .../accurics.kubernetes.IAM.82.json | 16 + + .../accurics.kubernetes.IAM.83.json | 16 + + .../accurics.kubernetes.IAM.84.json | 16 + + .../accurics.kubernetes.IAM.85.json | 16 + + .../accurics.kubernetes.IAM.86.json | 16 + + .../accurics.kubernetes.IAM.87.json | 16 + + .../accurics.kubernetes.IAM.88.json | 16 + + .../containerHasAllowedCapabilities.rego | 119 + + .../kubernetes_pod/appArmorProfile.rego | 108 + + .../kubernetes_pod/autoMountTokenEnabled.rego | 33 + + .../kubernetes_pod/capSysAdminUsed.rego | 69 + + .../kubernetes_pod/capabilityUsed.rego | 74 + + .../kubernetes_pod/commandCheck.rego | 19 + + .../accurics.kubernetes.IAM.105.json | 16 + + .../accurics.kubernetes.IAM.106.json | 16 + + .../accurics.kubernetes.IAM.108.json | 16 + + .../accurics.kubernetes.IAM.109.json | 16 + + .../accurics.kubernetes.IAM.110.json | 16 + + .../accurics.kubernetes.IAM.111.json | 16 + + .../accurics.kubernetes.IAM.112.json | 16 + + .../accurics.kubernetes.IAM.113.json | 16 + + .../accurics.kubernetes.IAM.114.json | 16 + + .../accurics.kubernetes.IAM.115.json | 16 + + .../accurics.kubernetes.IAM.116.json | 16 + + .../accurics.kubernetes.IAM.117.json | 16 + + .../accurics.kubernetes.IAM.118.json | 16 + + .../accurics.kubernetes.IAM.119.json | 16 + + .../accurics.kubernetes.IAM.120.json | 16 + + .../containerResourcesNotDefined.rego | 111 + + .../accurics.kubernetes.EKM.57.json | 16 + + .../accurics.kubernetes.EKM.58.json | 16 + + .../accurics.kubernetes.EKM.59.json | 16 + + .../accurics.kubernetes.EKM.60.json | 16 + + .../accurics.kubernetes.EKM.61.json | 16 + + .../accurics.kubernetes.EKM.62.json | 16 + + .../accurics.kubernetes.EKM.63.json | 16 + + .../accurics.kubernetes.EKM.64.json | 16 + + .../accurics.kubernetes.EKM.65.json | 16 + + .../accurics.kubernetes.EKM.66.json | 16 + + .../accurics.kubernetes.EKM.67.json | 16 + + .../accurics.kubernetes.EKM.68.json | 16 + + .../accurics.kubernetes.EKM.69.json | 16 + + .../accurics.kubernetes.EKM.70.json | 16 + + .../accurics.kubernetes.EKM.71.json | 16 + + .../accurics.kubernetes.EKM.72.json | 16 + + .../containerUsesSecretsInEnvironmentVar.rego | 115 + + .../kubernetes_pod/containersAsHighUID.rego | 102 + + .../kubernetes_pod/disallowedSysCalls.rego | 51 + + .../AC-K8-DS-PO-M-0143.json | 14 + + .../disallowed_volumes/disAllowedVolumes.rego | 52 + + .../kubernetes_pod/dockerSockCheck.rego | 35 + + .../kubernetes_pod/imageWithLatestTag.rego | 196 + + .../kubernetes_pod/imageWithoutDigest.rego | 105 + + .../kubernetes_pod/kubeDashboardEnabled.rego | 6 + + .../kubernetes_pod/otherNamespace.rego | 20 + + .../priviledgedContainersEnabled.rego | 11 + + .../kubernetes_pod/probeCheck.rego | 68 + + .../kubernetes_pod/secCompProfile.rego | 153 + + .../kubernetes_pod/secretsAsEnvVariables.rego | 75 + + .../kubernetes_pod/securityContextCheck.rego | 76 + + .../kubernetes_pod/securityContextUsed.rego | 103 + + .../kubernetes_pod/specBoolCheck.rego | 36 + + .../kubernetes_pod/tillerDeployed.rego | 35 + + pkg/http-server/routes.go | 10 +- + pkg/http-server/routes_test.go | 2 +- + pkg/http-server/server.go | 3 +- + pkg/http-server/start.go | 31 +- + pkg/http-server/templates/index.html | 34 + + pkg/http-server/templates/show.html | 40 + + pkg/http-server/webhook-deny-rule-matcher.go | 35 + + .../webhook-deny-rule-matcher_test.go | 98 + + pkg/http-server/webhook-scan-logger.go | 197 + + pkg/http-server/webhook-scan-logger_test.go | 77 + + pkg/http-server/webhook-scan-logs.go | 242 + + pkg/http-server/webhook-scan.go | 297 + + pkg/http-server/webhook-scan_test.go | 244 + + pkg/initialize/run.go | 2 +- + pkg/runtime/executor_test.go | 3 - + .../scan-skip-rules-low-severity.toml | 2 +- + 152 files changed, 11577 insertions(+), 30 deletions(-) + create mode 100644 docs/getting-started/admission-controller-webhooks-usage.md + create mode 100644 pkg/http-server/assets/icons.svg + create mode 100644 pkg/http-server/assets/jsonTree.css + create mode 100644 pkg/http-server/assets/jsonTree.js + create mode 100644 pkg/http-server/assets/moment.js + create mode 100644 pkg/http-server/assets/webhook-scan-logs.css + create mode 100644 pkg/http-server/assets/webhook-scan-logs.js + create mode 100644 pkg/http-server/k8s_testdata/config-deny-category.toml + create mode 100644 pkg/http-server/k8s_testdata/config-deny-high.toml + create mode 100644 pkg/http-server/k8s_testdata/config-deny-non-existing-category.toml + create mode 100644 pkg/http-server/k8s_testdata/config-medium-severity.toml + create mode 100644 pkg/http-server/k8s_testdata/config-specific-rule.toml + create mode 100644 pkg/http-server/k8s_testdata/empty.json + create mode 100644 pkg/http-server/k8s_testdata/empty_object.json + create mode 100644 pkg/http-server/k8s_testdata/invalid.json + create mode 100644 pkg/http-server/k8s_testdata/risky_testconfig.json + create mode 100644 pkg/http-server/k8s_testdata/testconfig.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json + create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json + create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json + create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego + create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json + create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego + create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego + create mode 100644 pkg/http-server/templates/index.html + create mode 100644 pkg/http-server/templates/show.html + create mode 100644 pkg/http-server/webhook-deny-rule-matcher.go + create mode 100644 pkg/http-server/webhook-deny-rule-matcher_test.go + create mode 100644 pkg/http-server/webhook-scan-logger.go + create mode 100644 pkg/http-server/webhook-scan-logger_test.go + create mode 100644 pkg/http-server/webhook-scan-logs.go + create mode 100644 pkg/http-server/webhook-scan.go + create mode 100644 pkg/http-server/webhook-scan_test.go + +diff --git a/.gitignore b/.gitignore +index fc0803b..5dc0e2f 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -23,6 +23,9 @@ docs/_build/ + #vscode + .vscode/ + ++#GoLand ++.idea ++ + /updatedFiles + + # Go binar +@@ -30,4 +33,4 @@ docs/_build/ + + .DS_Store + +-vendor/ +\ No newline at end of file ++vendor/ +diff --git a/build/Dockerfile b/build/Dockerfile +index 376c117..7e06ea8 100644 +--- a/build/Dockerfile ++++ b/build/Dockerfile +@@ -3,13 +3,14 @@ FROM golang:alpine AS builder + + ARG GOOS_VAL=linux + ARG GOARCH_VAL=amd64 +-ARG CGO_ENABLED_VAL=0 ++ARG CGO_ENABLED_VAL=1 + + WORKDIR $GOPATH/src/terrascan + + # download go dependencies + COPY go.mod go.sum ./ + RUN go mod download ++RUN apk add -U build-base + + # copy terrascan source + COPY . . +@@ -32,6 +33,10 @@ USER terrascan + # copy terrascan binary from build + COPY --from=builder /go/bin/terrascan /go/bin/terrascan + ++# Copy webhooks UI templates & assets ++COPY ./pkg/http-server/templates /go/terrascan ++COPY ./pkg/http-server/assets /go/terrascan/assets ++ + EXPOSE 9010 + + ENTRYPOINT ["/go/bin/terrascan"] +diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml +index 9210f5c..6a430a5 100644 +--- a/deploy/docker-compose.yml ++++ b/deploy/docker-compose.yml +@@ -4,3 +4,4 @@ services: + image: accurics/terrascan:${TAG:-latest} + ports: + - 9010:9010 ++ - 443:9443 +diff --git a/docs/getting-started/admission-controller-webhooks-usage.md b/docs/getting-started/admission-controller-webhooks-usage.md +new file mode 100644 +index 0000000..5db33da +--- /dev/null ++++ b/docs/getting-started/admission-controller-webhooks-usage.md +@@ -0,0 +1,108 @@ ++# Using Terrascan as a Kubernetes Admission Controller ++ ++## Overview ++Terrascan can be integrated with K8s [admissions webhooks](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/). ++It can be used as one of the validating webhooks to be used and scan new configurations. ++ ++In this guide, we'll demonstrate how Terrascan can be configured to: ++* Scan configuration changes policies when an object is being created or updated ++* Allow / reject the request in case a violation is detected ++ ++ ++## Installation Guide ++ ++### Create an instance ++Your Terrascan instance has the following requirements for being able to scan K8s configurations. ++ ++1. Be accessible via HTTPS. Make sure your cloud firewall is configured to allow this. ++1. Have a valid SSL certificate for the served domain name. To do that, choose one of our suggested methods: ++ 1. Use a subdomain of your choosing (e.g dev-terrascan-k8s.accurics.com) and create a valid certificate for this subdomain through your SSL certificate provider. [Let's Encrypt](https://letsencrypt.org/) is a free, simple to use certificate authority you can use. ++ 1. Use a reverse-proxy to serve SSL requests; for example, use Cloudflare Flexible to get a certificate by a trusted-CA to your [self-signed certificate](https://www.digitalocean.com/community/tutorials/openssl-essentials-working-with-ssl-certificates-private-keys-and-csrs). ++ 1. Generate a self-signed certificate and have your K8s cluster trust it. To add a trusted CA to ca-pemstore, as demonstrated in [paraspatidar's blog post](https://medium.com/@paraspatidar/add-ssl-tls-certificate-or-pem-file-to-kubernetes-pod-s-trusted-root-ca-store-7bed5cd683d). ++1. Use the Terrascan docker as demonstrated in this document, or run it from the sources. ++ ++### Run Terrascan webhook service ++Run Terrascan docker image in your server using the following command: ++ ```bash ++ sudo docker run -p 443:9443 -v :/data -u root -e K8S_WEBHOOK_API_KEY=> accurics/terrascan server --cert-path /data/cert.pem --key-path /data/key.pem ++ ``` ++`` is a key used for authentication between your K8s environment and the Terrascan server. Generate your preferred key and use it here. ++ ++`` is a directory path in your server where both the certificate and the private key .pem files are stored. ++In addition, this directory is used to write save the webhook logs. (An SQLite file) ++ ++You can specify a config file that specifies which policies to use in the scan and which violations should lead to rejection. ++ ++A config file example: ```my_terrscan_config.toml``` ++ ```bash ++[severity] ++level = "medium" ++[rules] ++ skip-rules = [ ++ "accurics.kubernetes.IAM.107" ++ ] ++ ++[k8s-deny-rules] ++ denied-categories = [ ++ "Network Ports Security" ++ ] ++ denied-severity = "high" ++ ``` ++ ++You can specify the following configurations: ++* **scan-rules** - one or more rules to scan ++* **skip-rules** - one or more rules to skip while scanning ++* **severity** - the minimal level of severity of the policies to be scanned ++ ++ ++* **k8s-deny-rules** - specify the rules that should cause a rejection of the admission request ++ * **denied-categories** - one or more policy categories that are not allowed in the detected violations ++ * **denied-severity** - the minimal level of severity that should cause a rejection ++ ++In order to use a configuration file, add it as a command line argument: ++ ++``` -c /data/my_terrscan_config.toml``` ++ ++ ++### Configure K8s to send webhooks ++Configure a new ```ValidatingWebhookConfiguration``` in your Kubernetes environment and specify your Terrascan server endpoint. ++ ++Example: ++ ```bash ++ cat </v1/k8s/webhooks//scan ++ sideEffects: None ++ admissionReviewVersions: ["v1"] ++ EOF ++ ``` ++ ++* You can modify the `rules` that trigger the webhook according to your preferences. ++* Update the ```clientConfig``` URL with your terrascan server address and the API key you generated before. ++ ++ ++### Test your settings ++Try to run a new pod / service. For example: ++``` Bash ++ kubectl run mynginx --image=nginx ++``` ++ ++Go to ```https:///k8s/webhooks//logs``` and verify your request is logged. +diff --git a/go.mod b/go.mod +index c76b2a7..2d86ab4 100644 +--- a/go.mod ++++ b/go.mod +@@ -19,6 +19,7 @@ require ( + github.com/hashicorp/terraform v0.14.4 + github.com/iancoleman/strcase v0.1.3 + github.com/mattn/go-isatty v0.0.12 ++ github.com/mattn/go-sqlite3 v1.14.6 + github.com/mitchellh/go-homedir v1.1.0 + github.com/onsi/ginkgo v1.12.1 + github.com/onsi/gomega v1.10.5 +@@ -35,5 +36,7 @@ require ( + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 + helm.sh/helm/v3 v3.4.0 + honnef.co/go/tools v0.1.1 // indirect ++ k8s.io/api v0.19.2 ++ k8s.io/apimachinery v0.19.2 + sigs.k8s.io/kustomize/api v0.7.2 + ) +diff --git a/go.sum b/go.sum +index c589491..6b0fbb0 100644 +--- a/go.sum ++++ b/go.sum +@@ -700,7 +700,10 @@ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp + github.com/mattn/go-shellwords v1.0.4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= + github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= + github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= ++github.com/mattn/go-sqlite3 v1.12.0 h1:u/x3mp++qUxvYfulZ4HKOvVO0JWhk7HtE8lWhbGz/Do= + github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= ++github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= ++github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= + github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= + github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +@@ -1317,7 +1320,6 @@ golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWc + golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= + golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= + golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +-golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= + golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= + golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= + golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +@@ -1325,8 +1327,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc + golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= + golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= + golang.org/x/tools v0.0.0-20201028111035-eafbe7b904eb/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +-golang.org/x/tools v0.0.0-20210115202250-e0d201561e39 h1:BTs2GMGSMWpgtCpv1CE7vkJTv7XcHdcLLnAMu7UbgTY= +-golang.org/x/tools v0.0.0-20210115202250-e0d201561e39/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= + golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= + golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +@@ -1485,8 +1485,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh + honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= + honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= + honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +-honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c= +-honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM= + honnef.co/go/tools v0.1.1 h1:EVDuO03OCZwpV2t/tLLxPmPiomagMoBOgfPt0FM+4IY= + honnef.co/go/tools v0.1.1/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= + k8s.io/api v0.19.0 h1:XyrFIJqTYZJ2DU7FBE/bSPz7b1HvbVBuBf07oeo6eTc= +diff --git a/pkg/cli/server.go b/pkg/cli/server.go +index edb4627..ea6040e 100644 +--- a/pkg/cli/server.go ++++ b/pkg/cli/server.go +@@ -21,6 +21,14 @@ import ( + "github.com/spf13/cobra" + ) + ++var ( ++ // CertFile Certificate file path, required in order to enable secure HTTP server ++ CertFile string ++ ++ // PrivateKeyFile Private key file path, required in order to enable secure HTTP server ++ PrivateKeyFile string ++) ++ + var serverCmd = &cobra.Command{ + Use: "server", + Short: "Run Terrascan as an API server", +@@ -35,9 +43,11 @@ Run Terrascan as an API server that inspects incoming IaC (Infrastructure-as-Cod + } + + func server(cmd *cobra.Command, args []string) { +- httpserver.Start() ++ httpserver.Start(ConfigFile, CertFile, PrivateKeyFile) + } + + func init() { ++ rootCmd.PersistentFlags().StringVarP(&PrivateKeyFile, "key-path", "", "", "private key file path") ++ rootCmd.PersistentFlags().StringVarP(&CertFile, "cert-path", "", "", "certificate file path") + RegisterCommand(rootCmd, serverCmd) + } +diff --git a/pkg/config/config-reader.go b/pkg/config/config-reader.go +index f33edf0..f37821f 100644 +--- a/pkg/config/config-reader.go ++++ b/pkg/config/config-reader.go +@@ -87,3 +87,8 @@ func (r TerrascanConfigReader) GetRules() Rules { + func (r TerrascanConfigReader) GetSeverity() Severity { + return r.config.Severity + } ++ ++// GetK8sDenyRules will return the k8s deny rules specified in the terrascan config file ++func (r TerrascanConfigReader) GetK8sDenyRules() K8sDenyRules { ++ return r.config.K8sDenyRules ++} +diff --git a/pkg/config/types.go b/pkg/config/types.go +index 9c51446..c83744f 100644 +--- a/pkg/config/types.go ++++ b/pkg/config/types.go +@@ -16,7 +16,7 @@ + + package config + +-// Global initalizes GlobalConfig struct ++// Global initializes GlobalConfig struct + var Global *TerrascanConfig = &TerrascanConfig{} + + // TerrascanConfig struct defines global variables/configurations across terrascan +@@ -25,6 +25,7 @@ type TerrascanConfig struct { + Notifications map[string]Notifier `toml:"notifications,omitempty"` + Rules `toml:"rules,omitempty"` + Severity `toml:"severity,omitempty"` ++ K8sDenyRules `toml:"k8s-deny-rules,omitempty"` + } + + // Severity defines the minimum level of severity of violations that you want to be reported +@@ -54,3 +55,9 @@ type Rules struct { + ScanRules []string `toml:"scan-rules,omitempty"` + SkipRules []string `toml:"skip-rules,omitempty"` + } ++ ++// K8s deny rules in the terrascan config file ++type K8sDenyRules struct { ++ DeniedSeverity string `toml:"denied-severity,omitempty"` ++ Categories []string `toml:"denied-categories,omitempty"` ++} +diff --git a/pkg/http-server/assets/icons.svg b/pkg/http-server/assets/icons.svg +new file mode 100644 +index 0000000..cc8298a +--- /dev/null ++++ b/pkg/http-server/assets/icons.svg +@@ -0,0 +1 @@ ++ +\ No newline at end of file +diff --git a/pkg/http-server/assets/jsonTree.css b/pkg/http-server/assets/jsonTree.css +new file mode 100644 +index 0000000..ad17484 +--- /dev/null ++++ b/pkg/http-server/assets/jsonTree.css +@@ -0,0 +1,107 @@ ++/* ++ * JSON Tree Viewer ++ * http://github.com/summerstyle/jsonTreeViewer ++ * ++ * Copyright 2017 Vera Lobacheva (http://iamvera.com) ++ * Released under the MIT license (LICENSE.txt) ++ */ ++ ++/* Background for the tree. May use for element */ ++.jsontree_bg { ++ background: #FFF; ++} ++ ++/* Styles for the container of the tree (e.g. fonts, margins etc.) */ ++.jsontree_tree { ++ /*margin-left: 30px;*/ ++ font-family: 'PT Mono', monospace; ++ font-size: 14px; ++} ++ ++/* Styles for a list of child nodes */ ++.jsontree_child-nodes { ++ display: none; ++ margin-left: 35px; ++ margin-bottom: 5px; ++ line-height: 2; ++} ++.jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_child-nodes { ++ display: block; ++} ++ ++/* Styles for labels */ ++.jsontree_label-wrapper { ++ float: left; ++ margin-right: 8px; ++} ++.jsontree_label { ++ font-weight: normal; ++ vertical-align: top; ++ color: #000; ++ position: relative; ++ padding: 1px; ++ border-radius: 4px; ++ cursor: default; ++} ++.jsontree_node_marked > .jsontree_label-wrapper > .jsontree_label { ++ background: #fff2aa; ++} ++ ++/* Styles for values */ ++.jsontree_value-wrapper { ++ display: block; ++ /*overflow: hidden;*/ ++} ++.jsontree_node_complex > .jsontree_value-wrapper { ++ overflow: inherit; ++} ++.jsontree_value { ++ vertical-align: top; ++ display: inline; ++} ++.jsontree_value_null { ++ color: #777; ++ font-weight: bold; ++} ++.jsontree_value_string { ++ color: #025900; ++ font-weight: bold; ++} ++.jsontree_value_number { ++ color: #000E59; ++ font-weight: bold; ++} ++.jsontree_value_boolean { ++ color: #600100; ++ font-weight: bold; ++} ++ ++/* Styles for active elements */ ++.jsontree_expand-button { ++ position: absolute; ++ top: 3px; ++ left: -15px; ++ display: block; ++ width: 11px; ++ height: 11px; ++ background-image: url('icons.svg'); ++} ++.jsontree_node_expanded > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button { ++ background-position: 0 -11px; ++} ++.jsontree_show-more { ++ cursor: pointer; ++} ++.jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more { ++ display: none; ++} ++.jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button, ++.jsontree_node_empty > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more { ++ display: none !important; ++} ++.jsontree_node_complex > .jsontree_label-wrapper > .jsontree_label { ++ cursor: pointer; ++} ++.jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label { ++ cursor: default !important; ++} +diff --git a/pkg/http-server/assets/jsonTree.js b/pkg/http-server/assets/jsonTree.js +new file mode 100644 +index 0000000..51bbf01 +--- /dev/null ++++ b/pkg/http-server/assets/jsonTree.js +@@ -0,0 +1,819 @@ ++/** ++ * JSON Tree library (a part of jsonTreeViewer) ++ * http://github.com/summerstyle/jsonTreeViewer ++ * ++ * Copyright 2017 Vera Lobacheva (http://iamvera.com) ++ * Released under the MIT license (LICENSE.txt) ++ */ ++ ++var jsonTree = (function() { ++ ++ /* ---------- Utilities ---------- */ ++ var utils = { ++ ++ /* ++ * Returns js-"class" of value ++ * ++ * @param val {any type} - value ++ * @returns {string} - for example, "[object Function]" ++ */ ++ getClass : function(val) { ++ return Object.prototype.toString.call(val); ++ }, ++ ++ /** ++ * Checks for a type of value (for valid JSON data types). ++ * In other cases - throws an exception ++ * ++ * @param val {any type} - the value for new node ++ * @returns {string} ("object" | "array" | "null" | "boolean" | "number" | "string") ++ */ ++ getType : function(val) { ++ if (val === null) { ++ return 'null'; ++ } ++ ++ switch (typeof val) { ++ case 'number': ++ return 'number'; ++ ++ case 'string': ++ return 'string'; ++ ++ case 'boolean': ++ return 'boolean'; ++ } ++ ++ switch(utils.getClass(val)) { ++ case '[object Array]': ++ return 'array'; ++ ++ case '[object Object]': ++ return 'object'; ++ } ++ ++ throw new Error('Bad type: ' + utils.getClass(val)); ++ }, ++ ++ /** ++ * Applies for each item of list some function ++ * and checks for last element of the list ++ * ++ * @param obj {Object | Array} - a list or a dict with child nodes ++ * @param func {Function} - the function for each item ++ */ ++ forEachNode : function(obj, func) { ++ var type = utils.getType(obj), ++ isLast; ++ ++ switch (type) { ++ case 'array': ++ isLast = obj.length - 1; ++ ++ obj.forEach(function(item, i) { ++ func(i, item, i === isLast); ++ }); ++ ++ break; ++ ++ case 'object': ++ var keys = Object.keys(obj).sort(); ++ ++ isLast = keys.length - 1; ++ ++ keys.forEach(function(item, i) { ++ func(item, obj[item], i === isLast); ++ }); ++ ++ break; ++ } ++ ++ }, ++ ++ /** ++ * Implements the kind of an inheritance by ++ * using parent prototype and ++ * creating intermediate constructor ++ * ++ * @param Child {Function} - a child constructor ++ * @param Parent {Function} - a parent constructor ++ */ ++ inherits : (function() { ++ var F = function() {}; ++ ++ return function(Child, Parent) { ++ F.prototype = Parent.prototype; ++ Child.prototype = new F(); ++ Child.prototype.constructor = Child; ++ }; ++ })(), ++ ++ /* ++ * Checks for a valid type of root node* ++ * ++ * @param {any type} jsonObj - a value for root node ++ * @returns {boolean} - true for an object or an array, false otherwise ++ */ ++ isValidRoot : function(jsonObj) { ++ switch (utils.getType(jsonObj)) { ++ case 'object': ++ case 'array': ++ return true; ++ default: ++ return false; ++ } ++ }, ++ ++ /** ++ * Extends some object ++ */ ++ extend : function(targetObj, sourceObj) { ++ for (var prop in sourceObj) { ++ if (sourceObj.hasOwnProperty(prop)) { ++ targetObj[prop] = sourceObj[prop]; ++ } ++ } ++ } ++ }; ++ ++ ++ /* ---------- Node constructors ---------- */ ++ ++ /** ++ * The factory for creating nodes of defined type. ++ * ++ * ~~~ Node ~~~ is a structure element of an onject or an array ++ * with own label (a key of an object or an index of an array) ++ * and value of any json data type. The root object or array ++ * is a node without label. ++ * {... ++ * [+] "label": value, ++ * ...} ++ * ++ * Markup: ++ *
  • ++ * ++ * ++ * ++ * "label" ++ * ++ * : ++ * ++ * <(div|span) class="jsontree_value jsontree_value_(object|array|boolean|null|number|string)"> ++ * ... ++ * ++ *
  • ++ * ++ * @param label {string} - key name ++ * @param val {Object | Array | string | number | boolean | null} - a value of node ++ * @param isLast {boolean} - true if node is last in list of siblings ++ * ++ * @return {Node} ++ */ ++ function Node(label, val, isLast) { ++ var nodeType = utils.getType(val); ++ ++ if (nodeType in Node.CONSTRUCTORS) { ++ return new Node.CONSTRUCTORS[nodeType](label, val, isLast); ++ } else { ++ throw new Error('Bad type: ' + utils.getClass(val)); ++ } ++ } ++ ++ Node.CONSTRUCTORS = { ++ 'boolean' : NodeBoolean, ++ 'number' : NodeNumber, ++ 'string' : NodeString, ++ 'null' : NodeNull, ++ 'object' : NodeObject, ++ 'array' : NodeArray ++ }; ++ ++ ++ /* ++ * The constructor for simple types (string, number, boolean, null) ++ * {... ++ * [+] "label": value, ++ * ...} ++ * value = string || number || boolean || null ++ * ++ * Markup: ++ *
  • ++ * ++ * "age" ++ * : ++ * ++ * 25 ++ * , ++ *
  • ++ * ++ * @abstract ++ * @param label {string} - key name ++ * @param val {string | number | boolean | null} - a value of simple types ++ * @param isLast {boolean} - true if node is last in list of parent childNodes ++ */ ++ function _NodeSimple(label, val, isLast) { ++ if (this.constructor === _NodeSimple) { ++ throw new Error('This is abstract class'); ++ } ++ ++ var self = this, ++ el = document.createElement('li'), ++ labelEl, ++ template = function(label, val) { ++ var str = '\ ++ \ ++ "' + ++ label + ++ '" : \ ++ \ ++ \ ++ ' + ++ val + ++ '' + ++ (!isLast ? ',' : '') + ++ ''; ++ ++ return str; ++ }; ++ ++ self.label = label; ++ self.isComplex = false; ++ ++ el.classList.add('jsontree_node'); ++ el.innerHTML = template(label, val); ++ ++ self.el = el; ++ ++ labelEl = el.querySelector('.jsontree_label'); ++ ++ labelEl.addEventListener('click', function(e) { ++ if (e.altKey) { ++ self.toggleMarked(); ++ return; ++ } ++ ++ if (e.shiftKey) { ++ document.getSelection().removeAllRanges(); ++ alert(self.getJSONPath()); ++ return; ++ } ++ }, false); ++ } ++ ++ _NodeSimple.prototype = { ++ constructor : _NodeSimple, ++ ++ /** ++ * Mark node ++ */ ++ mark : function() { ++ this.el.classList.add('jsontree_node_marked'); ++ }, ++ ++ /** ++ * Unmark node ++ */ ++ unmark : function() { ++ this.el.classList.remove('jsontree_node_marked'); ++ }, ++ ++ /** ++ * Mark or unmark node ++ */ ++ toggleMarked : function() { ++ this.el.classList.toggle('jsontree_node_marked'); ++ }, ++ ++ /** ++ * Expands parent node of this node ++ * ++ * @param isRecursive {boolean} - if true, expands all parent nodes ++ * (from node to root) ++ */ ++ expandParent : function(isRecursive) { ++ if (!this.parent) { ++ return; ++ } ++ ++ this.parent.expand(); ++ this.parent.expandParent(isRecursive); ++ }, ++ ++ /** ++ * Returns JSON-path of this ++ * ++ * @param isInDotNotation {boolean} - kind of notation for returned json-path ++ * (by default, in bracket notation) ++ * @returns {string} ++ */ ++ getJSONPath : function(isInDotNotation) { ++ if (this.isRoot) { ++ return "$"; ++ } ++ ++ var currentPath; ++ ++ if (this.parent.type === 'array') { ++ currentPath = "[" + this.label + "]"; ++ } else { ++ currentPath = isInDotNotation ? "." + this.label : "['" + this.label + "']"; ++ } ++ ++ return this.parent.getJSONPath(isInDotNotation) + currentPath; ++ } ++ }; ++ ++ ++ /* ++ * The constructor for boolean values ++ * {... ++ * [+] "label": boolean, ++ * ...} ++ * boolean = true || false ++ * ++ * @constructor ++ * @param label {string} - key name ++ * @param val {boolean} - value of boolean type, true or false ++ * @param isLast {boolean} - true if node is last in list of parent childNodes ++ */ ++ function NodeBoolean(label, val, isLast) { ++ this.type = "boolean"; ++ ++ _NodeSimple.call(this, label, val, isLast); ++ } ++ utils.inherits(NodeBoolean,_NodeSimple); ++ ++ ++ /* ++ * The constructor for number values ++ * {... ++ * [+] "label": number, ++ * ...} ++ * number = 123 ++ * ++ * @constructor ++ * @param label {string} - key name ++ * @param val {number} - value of number type, for example 123 ++ * @param isLast {boolean} - true if node is last in list of parent childNodes ++ */ ++ function NodeNumber(label, val, isLast) { ++ this.type = "number"; ++ ++ _NodeSimple.call(this, label, val, isLast); ++ } ++ utils.inherits(NodeNumber,_NodeSimple); ++ ++ ++ /* ++ * The constructor for string values ++ * {... ++ * [+] "label": string, ++ * ...} ++ * string = "abc" ++ * ++ * @constructor ++ * @param label {string} - key name ++ * @param val {string} - value of string type, for example "abc" ++ * @param isLast {boolean} - true if node is last in list of parent childNodes ++ */ ++ function NodeString(label, val, isLast) { ++ this.type = "string"; ++ ++ _NodeSimple.call(this, label, '"' + val + '"', isLast); ++ } ++ utils.inherits(NodeString,_NodeSimple); ++ ++ ++ /* ++ * The constructor for null values ++ * {... ++ * [+] "label": null, ++ * ...} ++ * ++ * @constructor ++ * @param label {string} - key name ++ * @param val {null} - value (only null) ++ * @param isLast {boolean} - true if node is last in list of parent childNodes ++ */ ++ function NodeNull(label, val, isLast) { ++ this.type = "null"; ++ ++ _NodeSimple.call(this, label, val, isLast); ++ } ++ utils.inherits(NodeNull,_NodeSimple); ++ ++ ++ /* ++ * The constructor for complex types (object, array) ++ * {... ++ * [+] "label": value, ++ * ...} ++ * value = object || array ++ * ++ * Markup: ++ *
  • ++ * ++ * ++ * ++ * "label" ++ * ++ * : ++ * ++ *
    ++ * { ++ *
      ++ * } ++ * , ++ *
    ++ *
  • ++ * ++ * @abstract ++ * @param label {string} - key name ++ * @param val {Object | Array} - a value of complex types, object or array ++ * @param isLast {boolean} - true if node is last in list of parent childNodes ++ */ ++ function _NodeComplex(label, val, isLast) { ++ if (this.constructor === _NodeComplex) { ++ throw new Error('This is abstract class'); ++ } ++ ++ var self = this, ++ el = document.createElement('li'), ++ template = function(label, sym) { ++ var comma = (!isLast) ? ',' : '', ++ str = '\ ++
    \ ++
    \ ++ ' + sym[0] + '\ ++ \ ++
      \ ++ ' + sym[1] + '' + ++ '
      ' + comma + ++ '
      '; ++ ++ if (label !== null) { ++ str = '\ ++ \ ++ ' + ++ '' + ++ '"' + label + ++ '" : \ ++ ' + str; ++ } ++ ++ return str; ++ }, ++ childNodesUl, ++ labelEl, ++ moreContentEl, ++ childNodes = []; ++ ++ self.label = label; ++ self.isComplex = true; ++ ++ el.classList.add('jsontree_node'); ++ el.classList.add('jsontree_node_complex'); ++ el.innerHTML = template(label, self.sym); ++ ++ childNodesUl = el.querySelector('.jsontree_child-nodes'); ++ ++ if (label !== null) { ++ labelEl = el.querySelector('.jsontree_label'); ++ moreContentEl = el.querySelector('.jsontree_show-more'); ++ ++ labelEl.addEventListener('click', function(e) { ++ if (e.altKey) { ++ self.toggleMarked(); ++ return; ++ } ++ ++ if (e.shiftKey) { ++ document.getSelection().removeAllRanges(); ++ alert(self.getJSONPath()); ++ return; ++ } ++ ++ self.toggle(e.ctrlKey || e.metaKey); ++ }, false); ++ ++ moreContentEl.addEventListener('click', function(e) { ++ self.toggle(e.ctrlKey || e.metaKey); ++ }, false); ++ ++ self.isRoot = false; ++ } else { ++ self.isRoot = true; ++ self.parent = null; ++ ++ el.classList.add('jsontree_node_expanded'); ++ } ++ ++ self.el = el; ++ self.childNodes = childNodes; ++ self.childNodesUl = childNodesUl; ++ ++ utils.forEachNode(val, function(label, node, isLast) { ++ self.addChild(new Node(label, node, isLast)); ++ }); ++ ++ self.isEmpty = !Boolean(childNodes.length); ++ if (self.isEmpty) { ++ el.classList.add('jsontree_node_empty'); ++ } ++ } ++ ++ utils.inherits(_NodeComplex, _NodeSimple); ++ ++ utils.extend(_NodeComplex.prototype, { ++ constructor : _NodeComplex, ++ ++ /* ++ * Add child node to list of child nodes ++ * ++ * @param child {Node} - child node ++ */ ++ addChild : function(child) { ++ this.childNodes.push(child); ++ this.childNodesUl.appendChild(child.el); ++ child.parent = this; ++ }, ++ ++ /* ++ * Expands this list of node child nodes ++ * ++ * @param isRecursive {boolean} - if true, expands all child nodes ++ */ ++ expand : function(isRecursive){ ++ if (this.isEmpty) { ++ return; ++ } ++ ++ if (!this.isRoot) { ++ this.el.classList.add('jsontree_node_expanded'); ++ } ++ ++ if (isRecursive) { ++ this.childNodes.forEach(function(item, i) { ++ if (item.isComplex) { ++ item.expand(isRecursive); ++ } ++ }); ++ } ++ }, ++ ++ /* ++ * Collapses this list of node child nodes ++ * ++ * @param isRecursive {boolean} - if true, collapses all child nodes ++ */ ++ collapse : function(isRecursive) { ++ if (this.isEmpty) { ++ return; ++ } ++ ++ if (!this.isRoot) { ++ this.el.classList.remove('jsontree_node_expanded'); ++ } ++ ++ if (isRecursive) { ++ this.childNodes.forEach(function(item, i) { ++ if (item.isComplex) { ++ item.collapse(isRecursive); ++ } ++ }); ++ } ++ }, ++ ++ /* ++ * Expands collapsed or collapses expanded node ++ * ++ * @param {boolean} isRecursive - Expand all child nodes if this node is expanded ++ * and collapse it otherwise ++ */ ++ toggle : function(isRecursive) { ++ if (this.isEmpty) { ++ return; ++ } ++ ++ this.el.classList.toggle('jsontree_node_expanded'); ++ ++ if (isRecursive) { ++ var isExpanded = this.el.classList.contains('jsontree_node_expanded'); ++ ++ this.childNodes.forEach(function(item, i) { ++ if (item.isComplex) { ++ item[isExpanded ? 'expand' : 'collapse'](isRecursive); ++ } ++ }); ++ } ++ }, ++ ++ /** ++ * Find child nodes that match some conditions and handle it ++ * ++ * @param {Function} matcher ++ * @param {Function} handler ++ * @param {boolean} isRecursive ++ */ ++ findChildren : function(matcher, handler, isRecursive) { ++ if (this.isEmpty) { ++ return; ++ } ++ ++ this.childNodes.forEach(function(item, i) { ++ if (matcher(item)) { ++ handler(item); ++ } ++ ++ if (item.isComplex && isRecursive) { ++ item.findChildren(matcher, handler, isRecursive); ++ } ++ }); ++ } ++ }); ++ ++ ++ /* ++ * The constructor for object values ++ * {... ++ * [+] "label": object, ++ * ...} ++ * object = {"abc": "def"} ++ * ++ * @constructor ++ * @param label {string} - key name ++ * @param val {Object} - value of object type, {"abc": "def"} ++ * @param isLast {boolean} - true if node is last in list of siblings ++ */ ++ function NodeObject(label, val, isLast) { ++ this.sym = ['{', '}']; ++ this.type = "object"; ++ ++ _NodeComplex.call(this, label, val, isLast); ++ } ++ utils.inherits(NodeObject,_NodeComplex); ++ ++ ++ /* ++ * The constructor for array values ++ * {... ++ * [+] "label": array, ++ * ...} ++ * array = [1,2,3] ++ * ++ * @constructor ++ * @param label {string} - key name ++ * @param val {Array} - value of array type, [1,2,3] ++ * @param isLast {boolean} - true if node is last in list of siblings ++ */ ++ function NodeArray(label, val, isLast) { ++ this.sym = ['[', ']']; ++ this.type = "array"; ++ ++ _NodeComplex.call(this, label, val, isLast); ++ } ++ utils.inherits(NodeArray, _NodeComplex); ++ ++ ++ /* ---------- The tree constructor ---------- */ ++ ++ /* ++ * The constructor for json tree. ++ * It contains only one Node (Array or Object), without property name. ++ * CSS-styles of .tree define main tree styles like font-family, ++ * font-size and own margins. ++ * ++ * Markup: ++ *
        ++ * {Node} ++ *
      ++ * ++ * @constructor ++ * @param jsonObj {Object | Array} - data for tree ++ * @param domEl {DOMElement} - DOM-element, wrapper for tree ++ */ ++ function Tree(jsonObj, domEl) { ++ this.wrapper = document.createElement('ul'); ++ this.wrapper.className = 'jsontree_tree clearfix'; ++ ++ this.rootNode = null; ++ ++ this.sourceJSONObj = jsonObj; ++ ++ this.loadData(jsonObj); ++ this.appendTo(domEl); ++ } ++ ++ Tree.prototype = { ++ constructor : Tree, ++ ++ /** ++ * Fill new data in current json tree ++ * ++ * @param {Object | Array} jsonObj - json-data ++ */ ++ loadData : function(jsonObj) { ++ if (!utils.isValidRoot(jsonObj)) { ++ alert('The root should be an object or an array'); ++ return; ++ } ++ ++ this.sourceJSONObj = jsonObj; ++ ++ this.rootNode = new Node(null, jsonObj, 'last'); ++ this.wrapper.innerHTML = ''; ++ this.wrapper.appendChild(this.rootNode.el); ++ }, ++ ++ /** ++ * Appends tree to DOM-element (or move it to new place) ++ * ++ * @param {DOMElement} domEl ++ */ ++ appendTo : function(domEl) { ++ domEl.appendChild(this.wrapper); ++ }, ++ ++ /** ++ * Expands all tree nodes (objects or arrays) recursively ++ * ++ * @param {Function} filterFunc - 'true' if this node should be expanded ++ */ ++ expand : function(filterFunc) { ++ if (this.rootNode.isComplex) { ++ if (typeof filterFunc == 'function') { ++ this.rootNode.childNodes.forEach(function(item, i) { ++ if (item.isComplex && filterFunc(item)) { ++ item.expand(); ++ } ++ }); ++ } else { ++ this.rootNode.expand('recursive'); ++ } ++ } ++ }, ++ ++ /** ++ * Collapses all tree nodes (objects or arrays) recursively ++ */ ++ collapse : function() { ++ if (typeof this.rootNode.collapse === 'function') { ++ this.rootNode.collapse('recursive'); ++ } ++ }, ++ ++ /** ++ * Returns the source json-string (pretty-printed) ++ * ++ * @param {boolean} isPrettyPrinted - 'true' for pretty-printed string ++ * @returns {string} - for exemple, '{"a":2,"b":3}' ++ */ ++ toSourceJSON : function(isPrettyPrinted) { ++ if (!isPrettyPrinted) { ++ return JSON.stringify(this.sourceJSONObj); ++ } ++ ++ var DELIMETER = "[%^$#$%^%]", ++ jsonStr = JSON.stringify(this.sourceJSONObj, null, DELIMETER); ++ ++ jsonStr = jsonStr.split("\n").join("
      "); ++ jsonStr = jsonStr.split(DELIMETER).join("    "); ++ ++ return jsonStr; ++ }, ++ ++ /** ++ * Find all nodes that match some conditions and handle it ++ */ ++ findAndHandle : function(matcher, handler) { ++ this.rootNode.findChildren(matcher, handler, 'isRecursive'); ++ }, ++ ++ /** ++ * Unmark all nodes ++ */ ++ unmarkAll : function() { ++ this.rootNode.findChildren(function(node) { ++ return true; ++ }, function(node) { ++ node.unmark(); ++ }, 'isRecursive'); ++ } ++ }; ++ ++ ++ /* ---------- Public methods ---------- */ ++ return { ++ /** ++ * Creates new tree by data and appends it to the DOM-element ++ * ++ * @param jsonObj {Object | Array} - json-data ++ * @param domEl {DOMElement} - the wrapper element ++ * @returns {Tree} ++ */ ++ create : function(jsonObj, domEl) { ++ return new Tree(jsonObj, domEl); ++ } ++ }; ++})(); +diff --git a/pkg/http-server/assets/moment.js b/pkg/http-server/assets/moment.js +new file mode 100644 +index 0000000..43bb380 +--- /dev/null ++++ b/pkg/http-server/assets/moment.js +@@ -0,0 +1,5670 @@ ++//! moment.js ++//! version : 2.29.1 ++//! authors : Tim Wood, Iskren Chernev, Moment.js contributors ++//! license : MIT ++//! momentjs.com ++ ++;(function (global, factory) { ++ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : ++ typeof define === 'function' && define.amd ? define(factory) : ++ global.moment = factory() ++}(this, (function () { 'use strict'; ++ ++ var hookCallback; ++ ++ function hooks() { ++ return hookCallback.apply(null, arguments); ++ } ++ ++ // This is done to register the method called with moment() ++ // without creating circular dependencies. ++ function setHookCallback(callback) { ++ hookCallback = callback; ++ } ++ ++ function isArray(input) { ++ return ( ++ input instanceof Array || ++ Object.prototype.toString.call(input) === '[object Array]' ++ ); ++ } ++ ++ function isObject(input) { ++ // IE8 will treat undefined and null as object if it wasn't for ++ // input != null ++ return ( ++ input != null && ++ Object.prototype.toString.call(input) === '[object Object]' ++ ); ++ } ++ ++ function hasOwnProp(a, b) { ++ return Object.prototype.hasOwnProperty.call(a, b); ++ } ++ ++ function isObjectEmpty(obj) { ++ if (Object.getOwnPropertyNames) { ++ return Object.getOwnPropertyNames(obj).length === 0; ++ } else { ++ var k; ++ for (k in obj) { ++ if (hasOwnProp(obj, k)) { ++ return false; ++ } ++ } ++ return true; ++ } ++ } ++ ++ function isUndefined(input) { ++ return input === void 0; ++ } ++ ++ function isNumber(input) { ++ return ( ++ typeof input === 'number' || ++ Object.prototype.toString.call(input) === '[object Number]' ++ ); ++ } ++ ++ function isDate(input) { ++ return ( ++ input instanceof Date || ++ Object.prototype.toString.call(input) === '[object Date]' ++ ); ++ } ++ ++ function map(arr, fn) { ++ var res = [], ++ i; ++ for (i = 0; i < arr.length; ++i) { ++ res.push(fn(arr[i], i)); ++ } ++ return res; ++ } ++ ++ function extend(a, b) { ++ for (var i in b) { ++ if (hasOwnProp(b, i)) { ++ a[i] = b[i]; ++ } ++ } ++ ++ if (hasOwnProp(b, 'toString')) { ++ a.toString = b.toString; ++ } ++ ++ if (hasOwnProp(b, 'valueOf')) { ++ a.valueOf = b.valueOf; ++ } ++ ++ return a; ++ } ++ ++ function createUTC(input, format, locale, strict) { ++ return createLocalOrUTC(input, format, locale, strict, true).utc(); ++ } ++ ++ function defaultParsingFlags() { ++ // We need to deep clone this object. ++ return { ++ empty: false, ++ unusedTokens: [], ++ unusedInput: [], ++ overflow: -2, ++ charsLeftOver: 0, ++ nullInput: false, ++ invalidEra: null, ++ invalidMonth: null, ++ invalidFormat: false, ++ userInvalidated: false, ++ iso: false, ++ parsedDateParts: [], ++ era: null, ++ meridiem: null, ++ rfc2822: false, ++ weekdayMismatch: false, ++ }; ++ } ++ ++ function getParsingFlags(m) { ++ if (m._pf == null) { ++ m._pf = defaultParsingFlags(); ++ } ++ return m._pf; ++ } ++ ++ var some; ++ if (Array.prototype.some) { ++ some = Array.prototype.some; ++ } else { ++ some = function (fun) { ++ var t = Object(this), ++ len = t.length >>> 0, ++ i; ++ ++ for (i = 0; i < len; i++) { ++ if (i in t && fun.call(this, t[i], i, t)) { ++ return true; ++ } ++ } ++ ++ return false; ++ }; ++ } ++ ++ function isValid(m) { ++ if (m._isValid == null) { ++ var flags = getParsingFlags(m), ++ parsedParts = some.call(flags.parsedDateParts, function (i) { ++ return i != null; ++ }), ++ isNowValid = ++ !isNaN(m._d.getTime()) && ++ flags.overflow < 0 && ++ !flags.empty && ++ !flags.invalidEra && ++ !flags.invalidMonth && ++ !flags.invalidWeekday && ++ !flags.weekdayMismatch && ++ !flags.nullInput && ++ !flags.invalidFormat && ++ !flags.userInvalidated && ++ (!flags.meridiem || (flags.meridiem && parsedParts)); ++ ++ if (m._strict) { ++ isNowValid = ++ isNowValid && ++ flags.charsLeftOver === 0 && ++ flags.unusedTokens.length === 0 && ++ flags.bigHour === undefined; ++ } ++ ++ if (Object.isFrozen == null || !Object.isFrozen(m)) { ++ m._isValid = isNowValid; ++ } else { ++ return isNowValid; ++ } ++ } ++ return m._isValid; ++ } ++ ++ function createInvalid(flags) { ++ var m = createUTC(NaN); ++ if (flags != null) { ++ extend(getParsingFlags(m), flags); ++ } else { ++ getParsingFlags(m).userInvalidated = true; ++ } ++ ++ return m; ++ } ++ ++ // Plugins that add properties should also add the key here (null value), ++ // so we can properly clone ourselves. ++ var momentProperties = (hooks.momentProperties = []), ++ updateInProgress = false; ++ ++ function copyConfig(to, from) { ++ var i, prop, val; ++ ++ if (!isUndefined(from._isAMomentObject)) { ++ to._isAMomentObject = from._isAMomentObject; ++ } ++ if (!isUndefined(from._i)) { ++ to._i = from._i; ++ } ++ if (!isUndefined(from._f)) { ++ to._f = from._f; ++ } ++ if (!isUndefined(from._l)) { ++ to._l = from._l; ++ } ++ if (!isUndefined(from._strict)) { ++ to._strict = from._strict; ++ } ++ if (!isUndefined(from._tzm)) { ++ to._tzm = from._tzm; ++ } ++ if (!isUndefined(from._isUTC)) { ++ to._isUTC = from._isUTC; ++ } ++ if (!isUndefined(from._offset)) { ++ to._offset = from._offset; ++ } ++ if (!isUndefined(from._pf)) { ++ to._pf = getParsingFlags(from); ++ } ++ if (!isUndefined(from._locale)) { ++ to._locale = from._locale; ++ } ++ ++ if (momentProperties.length > 0) { ++ for (i = 0; i < momentProperties.length; i++) { ++ prop = momentProperties[i]; ++ val = from[prop]; ++ if (!isUndefined(val)) { ++ to[prop] = val; ++ } ++ } ++ } ++ ++ return to; ++ } ++ ++ // Moment prototype object ++ function Moment(config) { ++ copyConfig(this, config); ++ this._d = new Date(config._d != null ? config._d.getTime() : NaN); ++ if (!this.isValid()) { ++ this._d = new Date(NaN); ++ } ++ // Prevent infinite loop in case updateOffset creates new moment ++ // objects. ++ if (updateInProgress === false) { ++ updateInProgress = true; ++ hooks.updateOffset(this); ++ updateInProgress = false; ++ } ++ } ++ ++ function isMoment(obj) { ++ return ( ++ obj instanceof Moment || (obj != null && obj._isAMomentObject != null) ++ ); ++ } ++ ++ function warn(msg) { ++ if ( ++ hooks.suppressDeprecationWarnings === false && ++ typeof console !== 'undefined' && ++ console.warn ++ ) { ++ console.warn('Deprecation warning: ' + msg); ++ } ++ } ++ ++ function deprecate(msg, fn) { ++ var firstTime = true; ++ ++ return extend(function () { ++ if (hooks.deprecationHandler != null) { ++ hooks.deprecationHandler(null, msg); ++ } ++ if (firstTime) { ++ var args = [], ++ arg, ++ i, ++ key; ++ for (i = 0; i < arguments.length; i++) { ++ arg = ''; ++ if (typeof arguments[i] === 'object') { ++ arg += '\n[' + i + '] '; ++ for (key in arguments[0]) { ++ if (hasOwnProp(arguments[0], key)) { ++ arg += key + ': ' + arguments[0][key] + ', '; ++ } ++ } ++ arg = arg.slice(0, -2); // Remove trailing comma and space ++ } else { ++ arg = arguments[i]; ++ } ++ args.push(arg); ++ } ++ warn( ++ msg + ++ '\nArguments: ' + ++ Array.prototype.slice.call(args).join('') + ++ '\n' + ++ new Error().stack ++ ); ++ firstTime = false; ++ } ++ return fn.apply(this, arguments); ++ }, fn); ++ } ++ ++ var deprecations = {}; ++ ++ function deprecateSimple(name, msg) { ++ if (hooks.deprecationHandler != null) { ++ hooks.deprecationHandler(name, msg); ++ } ++ if (!deprecations[name]) { ++ warn(msg); ++ deprecations[name] = true; ++ } ++ } ++ ++ hooks.suppressDeprecationWarnings = false; ++ hooks.deprecationHandler = null; ++ ++ function isFunction(input) { ++ return ( ++ (typeof Function !== 'undefined' && input instanceof Function) || ++ Object.prototype.toString.call(input) === '[object Function]' ++ ); ++ } ++ ++ function set(config) { ++ var prop, i; ++ for (i in config) { ++ if (hasOwnProp(config, i)) { ++ prop = config[i]; ++ if (isFunction(prop)) { ++ this[i] = prop; ++ } else { ++ this['_' + i] = prop; ++ } ++ } ++ } ++ this._config = config; ++ // Lenient ordinal parsing accepts just a number in addition to ++ // number + (possibly) stuff coming from _dayOfMonthOrdinalParse. ++ // TODO: Remove "ordinalParse" fallback in next major release. ++ this._dayOfMonthOrdinalParseLenient = new RegExp( ++ (this._dayOfMonthOrdinalParse.source || this._ordinalParse.source) + ++ '|' + ++ /\d{1,2}/.source ++ ); ++ } ++ ++ function mergeConfigs(parentConfig, childConfig) { ++ var res = extend({}, parentConfig), ++ prop; ++ for (prop in childConfig) { ++ if (hasOwnProp(childConfig, prop)) { ++ if (isObject(parentConfig[prop]) && isObject(childConfig[prop])) { ++ res[prop] = {}; ++ extend(res[prop], parentConfig[prop]); ++ extend(res[prop], childConfig[prop]); ++ } else if (childConfig[prop] != null) { ++ res[prop] = childConfig[prop]; ++ } else { ++ delete res[prop]; ++ } ++ } ++ } ++ for (prop in parentConfig) { ++ if ( ++ hasOwnProp(parentConfig, prop) && ++ !hasOwnProp(childConfig, prop) && ++ isObject(parentConfig[prop]) ++ ) { ++ // make sure changes to properties don't modify parent config ++ res[prop] = extend({}, res[prop]); ++ } ++ } ++ return res; ++ } ++ ++ function Locale(config) { ++ if (config != null) { ++ this.set(config); ++ } ++ } ++ ++ var keys; ++ ++ if (Object.keys) { ++ keys = Object.keys; ++ } else { ++ keys = function (obj) { ++ var i, ++ res = []; ++ for (i in obj) { ++ if (hasOwnProp(obj, i)) { ++ res.push(i); ++ } ++ } ++ return res; ++ }; ++ } ++ ++ var defaultCalendar = { ++ sameDay: '[Today at] LT', ++ nextDay: '[Tomorrow at] LT', ++ nextWeek: 'dddd [at] LT', ++ lastDay: '[Yesterday at] LT', ++ lastWeek: '[Last] dddd [at] LT', ++ sameElse: 'L', ++ }; ++ ++ function calendar(key, mom, now) { ++ var output = this._calendar[key] || this._calendar['sameElse']; ++ return isFunction(output) ? output.call(mom, now) : output; ++ } ++ ++ function zeroFill(number, targetLength, forceSign) { ++ var absNumber = '' + Math.abs(number), ++ zerosToFill = targetLength - absNumber.length, ++ sign = number >= 0; ++ return ( ++ (sign ? (forceSign ? '+' : '') : '-') + ++ Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + ++ absNumber ++ ); ++ } ++ ++ var formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g, ++ localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, ++ formatFunctions = {}, ++ formatTokenFunctions = {}; ++ ++ // token: 'M' ++ // padded: ['MM', 2] ++ // ordinal: 'Mo' ++ // callback: function () { this.month() + 1 } ++ function addFormatToken(token, padded, ordinal, callback) { ++ var func = callback; ++ if (typeof callback === 'string') { ++ func = function () { ++ return this[callback](); ++ }; ++ } ++ if (token) { ++ formatTokenFunctions[token] = func; ++ } ++ if (padded) { ++ formatTokenFunctions[padded[0]] = function () { ++ return zeroFill(func.apply(this, arguments), padded[1], padded[2]); ++ }; ++ } ++ if (ordinal) { ++ formatTokenFunctions[ordinal] = function () { ++ return this.localeData().ordinal( ++ func.apply(this, arguments), ++ token ++ ); ++ }; ++ } ++ } ++ ++ function removeFormattingTokens(input) { ++ if (input.match(/\[[\s\S]/)) { ++ return input.replace(/^\[|\]$/g, ''); ++ } ++ return input.replace(/\\/g, ''); ++ } ++ ++ function makeFormatFunction(format) { ++ var array = format.match(formattingTokens), ++ i, ++ length; ++ ++ for (i = 0, length = array.length; i < length; i++) { ++ if (formatTokenFunctions[array[i]]) { ++ array[i] = formatTokenFunctions[array[i]]; ++ } else { ++ array[i] = removeFormattingTokens(array[i]); ++ } ++ } ++ ++ return function (mom) { ++ var output = '', ++ i; ++ for (i = 0; i < length; i++) { ++ output += isFunction(array[i]) ++ ? array[i].call(mom, format) ++ : array[i]; ++ } ++ return output; ++ }; ++ } ++ ++ // format date using native date object ++ function formatMoment(m, format) { ++ if (!m.isValid()) { ++ return m.localeData().invalidDate(); ++ } ++ ++ format = expandFormat(format, m.localeData()); ++ formatFunctions[format] = ++ formatFunctions[format] || makeFormatFunction(format); ++ ++ return formatFunctions[format](m); ++ } ++ ++ function expandFormat(format, locale) { ++ var i = 5; ++ ++ function replaceLongDateFormatTokens(input) { ++ return locale.longDateFormat(input) || input; ++ } ++ ++ localFormattingTokens.lastIndex = 0; ++ while (i >= 0 && localFormattingTokens.test(format)) { ++ format = format.replace( ++ localFormattingTokens, ++ replaceLongDateFormatTokens ++ ); ++ localFormattingTokens.lastIndex = 0; ++ i -= 1; ++ } ++ ++ return format; ++ } ++ ++ var defaultLongDateFormat = { ++ LTS: 'h:mm:ss A', ++ LT: 'h:mm A', ++ L: 'MM/DD/YYYY', ++ LL: 'MMMM D, YYYY', ++ LLL: 'MMMM D, YYYY h:mm A', ++ LLLL: 'dddd, MMMM D, YYYY h:mm A', ++ }; ++ ++ function longDateFormat(key) { ++ var format = this._longDateFormat[key], ++ formatUpper = this._longDateFormat[key.toUpperCase()]; ++ ++ if (format || !formatUpper) { ++ return format; ++ } ++ ++ this._longDateFormat[key] = formatUpper ++ .match(formattingTokens) ++ .map(function (tok) { ++ if ( ++ tok === 'MMMM' || ++ tok === 'MM' || ++ tok === 'DD' || ++ tok === 'dddd' ++ ) { ++ return tok.slice(1); ++ } ++ return tok; ++ }) ++ .join(''); ++ ++ return this._longDateFormat[key]; ++ } ++ ++ var defaultInvalidDate = 'Invalid date'; ++ ++ function invalidDate() { ++ return this._invalidDate; ++ } ++ ++ var defaultOrdinal = '%d', ++ defaultDayOfMonthOrdinalParse = /\d{1,2}/; ++ ++ function ordinal(number) { ++ return this._ordinal.replace('%d', number); ++ } ++ ++ var defaultRelativeTime = { ++ future: 'in %s', ++ past: '%s ago', ++ s: 'a few seconds', ++ ss: '%d seconds', ++ m: 'a minute', ++ mm: '%d minutes', ++ h: 'an hour', ++ hh: '%d hours', ++ d: 'a day', ++ dd: '%d days', ++ w: 'a week', ++ ww: '%d weeks', ++ M: 'a month', ++ MM: '%d months', ++ y: 'a year', ++ yy: '%d years', ++ }; ++ ++ function relativeTime(number, withoutSuffix, string, isFuture) { ++ var output = this._relativeTime[string]; ++ return isFunction(output) ++ ? output(number, withoutSuffix, string, isFuture) ++ : output.replace(/%d/i, number); ++ } ++ ++ function pastFuture(diff, output) { ++ var format = this._relativeTime[diff > 0 ? 'future' : 'past']; ++ return isFunction(format) ? format(output) : format.replace(/%s/i, output); ++ } ++ ++ var aliases = {}; ++ ++ function addUnitAlias(unit, shorthand) { ++ var lowerCase = unit.toLowerCase(); ++ aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; ++ } ++ ++ function normalizeUnits(units) { ++ return typeof units === 'string' ++ ? aliases[units] || aliases[units.toLowerCase()] ++ : undefined; ++ } ++ ++ function normalizeObjectUnits(inputObject) { ++ var normalizedInput = {}, ++ normalizedProp, ++ prop; ++ ++ for (prop in inputObject) { ++ if (hasOwnProp(inputObject, prop)) { ++ normalizedProp = normalizeUnits(prop); ++ if (normalizedProp) { ++ normalizedInput[normalizedProp] = inputObject[prop]; ++ } ++ } ++ } ++ ++ return normalizedInput; ++ } ++ ++ var priorities = {}; ++ ++ function addUnitPriority(unit, priority) { ++ priorities[unit] = priority; ++ } ++ ++ function getPrioritizedUnits(unitsObj) { ++ var units = [], ++ u; ++ for (u in unitsObj) { ++ if (hasOwnProp(unitsObj, u)) { ++ units.push({ unit: u, priority: priorities[u] }); ++ } ++ } ++ units.sort(function (a, b) { ++ return a.priority - b.priority; ++ }); ++ return units; ++ } ++ ++ function isLeapYear(year) { ++ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; ++ } ++ ++ function absFloor(number) { ++ if (number < 0) { ++ // -0 -> 0 ++ return Math.ceil(number) || 0; ++ } else { ++ return Math.floor(number); ++ } ++ } ++ ++ function toInt(argumentForCoercion) { ++ var coercedNumber = +argumentForCoercion, ++ value = 0; ++ ++ if (coercedNumber !== 0 && isFinite(coercedNumber)) { ++ value = absFloor(coercedNumber); ++ } ++ ++ return value; ++ } ++ ++ function makeGetSet(unit, keepTime) { ++ return function (value) { ++ if (value != null) { ++ set$1(this, unit, value); ++ hooks.updateOffset(this, keepTime); ++ return this; ++ } else { ++ return get(this, unit); ++ } ++ }; ++ } ++ ++ function get(mom, unit) { ++ return mom.isValid() ++ ? mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]() ++ : NaN; ++ } ++ ++ function set$1(mom, unit, value) { ++ if (mom.isValid() && !isNaN(value)) { ++ if ( ++ unit === 'FullYear' && ++ isLeapYear(mom.year()) && ++ mom.month() === 1 && ++ mom.date() === 29 ++ ) { ++ value = toInt(value); ++ mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit]( ++ value, ++ mom.month(), ++ daysInMonth(value, mom.month()) ++ ); ++ } else { ++ mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); ++ } ++ } ++ } ++ ++ // MOMENTS ++ ++ function stringGet(units) { ++ units = normalizeUnits(units); ++ if (isFunction(this[units])) { ++ return this[units](); ++ } ++ return this; ++ } ++ ++ function stringSet(units, value) { ++ if (typeof units === 'object') { ++ units = normalizeObjectUnits(units); ++ var prioritized = getPrioritizedUnits(units), ++ i; ++ for (i = 0; i < prioritized.length; i++) { ++ this[prioritized[i].unit](units[prioritized[i].unit]); ++ } ++ } else { ++ units = normalizeUnits(units); ++ if (isFunction(this[units])) { ++ return this[units](value); ++ } ++ } ++ return this; ++ } ++ ++ var match1 = /\d/, // 0 - 9 ++ match2 = /\d\d/, // 00 - 99 ++ match3 = /\d{3}/, // 000 - 999 ++ match4 = /\d{4}/, // 0000 - 9999 ++ match6 = /[+-]?\d{6}/, // -999999 - 999999 ++ match1to2 = /\d\d?/, // 0 - 99 ++ match3to4 = /\d\d\d\d?/, // 999 - 9999 ++ match5to6 = /\d\d\d\d\d\d?/, // 99999 - 999999 ++ match1to3 = /\d{1,3}/, // 0 - 999 ++ match1to4 = /\d{1,4}/, // 0 - 9999 ++ match1to6 = /[+-]?\d{1,6}/, // -999999 - 999999 ++ matchUnsigned = /\d+/, // 0 - inf ++ matchSigned = /[+-]?\d+/, // -inf - inf ++ matchOffset = /Z|[+-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z ++ matchShortOffset = /Z|[+-]\d\d(?::?\d\d)?/gi, // +00 -00 +00:00 -00:00 +0000 -0000 or Z ++ matchTimestamp = /[+-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123 ++ // any word (or two) characters or numbers including two/three word month in arabic. ++ // includes scottish gaelic two word and hyphenated months ++ matchWord = /[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i, ++ regexes; ++ ++ regexes = {}; ++ ++ function addRegexToken(token, regex, strictRegex) { ++ regexes[token] = isFunction(regex) ++ ? regex ++ : function (isStrict, localeData) { ++ return isStrict && strictRegex ? strictRegex : regex; ++ }; ++ } ++ ++ function getParseRegexForToken(token, config) { ++ if (!hasOwnProp(regexes, token)) { ++ return new RegExp(unescapeFormat(token)); ++ } ++ ++ return regexes[token](config._strict, config._locale); ++ } ++ ++ // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript ++ function unescapeFormat(s) { ++ return regexEscape( ++ s ++ .replace('\\', '') ++ .replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function ( ++ matched, ++ p1, ++ p2, ++ p3, ++ p4 ++ ) { ++ return p1 || p2 || p3 || p4; ++ }) ++ ); ++ } ++ ++ function regexEscape(s) { ++ return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); ++ } ++ ++ var tokens = {}; ++ ++ function addParseToken(token, callback) { ++ var i, ++ func = callback; ++ if (typeof token === 'string') { ++ token = [token]; ++ } ++ if (isNumber(callback)) { ++ func = function (input, array) { ++ array[callback] = toInt(input); ++ }; ++ } ++ for (i = 0; i < token.length; i++) { ++ tokens[token[i]] = func; ++ } ++ } ++ ++ function addWeekParseToken(token, callback) { ++ addParseToken(token, function (input, array, config, token) { ++ config._w = config._w || {}; ++ callback(input, config._w, config, token); ++ }); ++ } ++ ++ function addTimeToArrayFromToken(token, input, config) { ++ if (input != null && hasOwnProp(tokens, token)) { ++ tokens[token](input, config._a, config, token); ++ } ++ } ++ ++ var YEAR = 0, ++ MONTH = 1, ++ DATE = 2, ++ HOUR = 3, ++ MINUTE = 4, ++ SECOND = 5, ++ MILLISECOND = 6, ++ WEEK = 7, ++ WEEKDAY = 8; ++ ++ function mod(n, x) { ++ return ((n % x) + x) % x; ++ } ++ ++ var indexOf; ++ ++ if (Array.prototype.indexOf) { ++ indexOf = Array.prototype.indexOf; ++ } else { ++ indexOf = function (o) { ++ // I know ++ var i; ++ for (i = 0; i < this.length; ++i) { ++ if (this[i] === o) { ++ return i; ++ } ++ } ++ return -1; ++ }; ++ } ++ ++ function daysInMonth(year, month) { ++ if (isNaN(year) || isNaN(month)) { ++ return NaN; ++ } ++ var modMonth = mod(month, 12); ++ year += (month - modMonth) / 12; ++ return modMonth === 1 ++ ? isLeapYear(year) ++ ? 29 ++ : 28 ++ : 31 - ((modMonth % 7) % 2); ++ } ++ ++ // FORMATTING ++ ++ addFormatToken('M', ['MM', 2], 'Mo', function () { ++ return this.month() + 1; ++ }); ++ ++ addFormatToken('MMM', 0, 0, function (format) { ++ return this.localeData().monthsShort(this, format); ++ }); ++ ++ addFormatToken('MMMM', 0, 0, function (format) { ++ return this.localeData().months(this, format); ++ }); ++ ++ // ALIASES ++ ++ addUnitAlias('month', 'M'); ++ ++ // PRIORITY ++ ++ addUnitPriority('month', 8); ++ ++ // PARSING ++ ++ addRegexToken('M', match1to2); ++ addRegexToken('MM', match1to2, match2); ++ addRegexToken('MMM', function (isStrict, locale) { ++ return locale.monthsShortRegex(isStrict); ++ }); ++ addRegexToken('MMMM', function (isStrict, locale) { ++ return locale.monthsRegex(isStrict); ++ }); ++ ++ addParseToken(['M', 'MM'], function (input, array) { ++ array[MONTH] = toInt(input) - 1; ++ }); ++ ++ addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { ++ var month = config._locale.monthsParse(input, token, config._strict); ++ // if we didn't find a month name, mark the date as invalid. ++ if (month != null) { ++ array[MONTH] = month; ++ } else { ++ getParsingFlags(config).invalidMonth = input; ++ } ++ }); ++ ++ // LOCALES ++ ++ var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split( ++ '_' ++ ), ++ defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split( ++ '_' ++ ), ++ MONTHS_IN_FORMAT = /D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/, ++ defaultMonthsShortRegex = matchWord, ++ defaultMonthsRegex = matchWord; ++ ++ function localeMonths(m, format) { ++ if (!m) { ++ return isArray(this._months) ++ ? this._months ++ : this._months['standalone']; ++ } ++ return isArray(this._months) ++ ? this._months[m.month()] ++ : this._months[ ++ (this._months.isFormat || MONTHS_IN_FORMAT).test(format) ++ ? 'format' ++ : 'standalone' ++ ][m.month()]; ++ } ++ ++ function localeMonthsShort(m, format) { ++ if (!m) { ++ return isArray(this._monthsShort) ++ ? this._monthsShort ++ : this._monthsShort['standalone']; ++ } ++ return isArray(this._monthsShort) ++ ? this._monthsShort[m.month()] ++ : this._monthsShort[ ++ MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone' ++ ][m.month()]; ++ } ++ ++ function handleStrictParse(monthName, format, strict) { ++ var i, ++ ii, ++ mom, ++ llc = monthName.toLocaleLowerCase(); ++ if (!this._monthsParse) { ++ // this is not used ++ this._monthsParse = []; ++ this._longMonthsParse = []; ++ this._shortMonthsParse = []; ++ for (i = 0; i < 12; ++i) { ++ mom = createUTC([2000, i]); ++ this._shortMonthsParse[i] = this.monthsShort( ++ mom, ++ '' ++ ).toLocaleLowerCase(); ++ this._longMonthsParse[i] = this.months(mom, '').toLocaleLowerCase(); ++ } ++ } ++ ++ if (strict) { ++ if (format === 'MMM') { ++ ii = indexOf.call(this._shortMonthsParse, llc); ++ return ii !== -1 ? ii : null; ++ } else { ++ ii = indexOf.call(this._longMonthsParse, llc); ++ return ii !== -1 ? ii : null; ++ } ++ } else { ++ if (format === 'MMM') { ++ ii = indexOf.call(this._shortMonthsParse, llc); ++ if (ii !== -1) { ++ return ii; ++ } ++ ii = indexOf.call(this._longMonthsParse, llc); ++ return ii !== -1 ? ii : null; ++ } else { ++ ii = indexOf.call(this._longMonthsParse, llc); ++ if (ii !== -1) { ++ return ii; ++ } ++ ii = indexOf.call(this._shortMonthsParse, llc); ++ return ii !== -1 ? ii : null; ++ } ++ } ++ } ++ ++ function localeMonthsParse(monthName, format, strict) { ++ var i, mom, regex; ++ ++ if (this._monthsParseExact) { ++ return handleStrictParse.call(this, monthName, format, strict); ++ } ++ ++ if (!this._monthsParse) { ++ this._monthsParse = []; ++ this._longMonthsParse = []; ++ this._shortMonthsParse = []; ++ } ++ ++ // TODO: add sorting ++ // Sorting makes sure if one month (or abbr) is a prefix of another ++ // see sorting in computeMonthsParse ++ for (i = 0; i < 12; i++) { ++ // make the regex if we don't have it already ++ mom = createUTC([2000, i]); ++ if (strict && !this._longMonthsParse[i]) { ++ this._longMonthsParse[i] = new RegExp( ++ '^' + this.months(mom, '').replace('.', '') + '$', ++ 'i' ++ ); ++ this._shortMonthsParse[i] = new RegExp( ++ '^' + this.monthsShort(mom, '').replace('.', '') + '$', ++ 'i' ++ ); ++ } ++ if (!strict && !this._monthsParse[i]) { ++ regex = ++ '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); ++ this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); ++ } ++ // test the regex ++ if ( ++ strict && ++ format === 'MMMM' && ++ this._longMonthsParse[i].test(monthName) ++ ) { ++ return i; ++ } else if ( ++ strict && ++ format === 'MMM' && ++ this._shortMonthsParse[i].test(monthName) ++ ) { ++ return i; ++ } else if (!strict && this._monthsParse[i].test(monthName)) { ++ return i; ++ } ++ } ++ } ++ ++ // MOMENTS ++ ++ function setMonth(mom, value) { ++ var dayOfMonth; ++ ++ if (!mom.isValid()) { ++ // No op ++ return mom; ++ } ++ ++ if (typeof value === 'string') { ++ if (/^\d+$/.test(value)) { ++ value = toInt(value); ++ } else { ++ value = mom.localeData().monthsParse(value); ++ // TODO: Another silent failure? ++ if (!isNumber(value)) { ++ return mom; ++ } ++ } ++ } ++ ++ dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); ++ mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); ++ return mom; ++ } ++ ++ function getSetMonth(value) { ++ if (value != null) { ++ setMonth(this, value); ++ hooks.updateOffset(this, true); ++ return this; ++ } else { ++ return get(this, 'Month'); ++ } ++ } ++ ++ function getDaysInMonth() { ++ return daysInMonth(this.year(), this.month()); ++ } ++ ++ function monthsShortRegex(isStrict) { ++ if (this._monthsParseExact) { ++ if (!hasOwnProp(this, '_monthsRegex')) { ++ computeMonthsParse.call(this); ++ } ++ if (isStrict) { ++ return this._monthsShortStrictRegex; ++ } else { ++ return this._monthsShortRegex; ++ } ++ } else { ++ if (!hasOwnProp(this, '_monthsShortRegex')) { ++ this._monthsShortRegex = defaultMonthsShortRegex; ++ } ++ return this._monthsShortStrictRegex && isStrict ++ ? this._monthsShortStrictRegex ++ : this._monthsShortRegex; ++ } ++ } ++ ++ function monthsRegex(isStrict) { ++ if (this._monthsParseExact) { ++ if (!hasOwnProp(this, '_monthsRegex')) { ++ computeMonthsParse.call(this); ++ } ++ if (isStrict) { ++ return this._monthsStrictRegex; ++ } else { ++ return this._monthsRegex; ++ } ++ } else { ++ if (!hasOwnProp(this, '_monthsRegex')) { ++ this._monthsRegex = defaultMonthsRegex; ++ } ++ return this._monthsStrictRegex && isStrict ++ ? this._monthsStrictRegex ++ : this._monthsRegex; ++ } ++ } ++ ++ function computeMonthsParse() { ++ function cmpLenRev(a, b) { ++ return b.length - a.length; ++ } ++ ++ var shortPieces = [], ++ longPieces = [], ++ mixedPieces = [], ++ i, ++ mom; ++ for (i = 0; i < 12; i++) { ++ // make the regex if we don't have it already ++ mom = createUTC([2000, i]); ++ shortPieces.push(this.monthsShort(mom, '')); ++ longPieces.push(this.months(mom, '')); ++ mixedPieces.push(this.months(mom, '')); ++ mixedPieces.push(this.monthsShort(mom, '')); ++ } ++ // Sorting makes sure if one month (or abbr) is a prefix of another it ++ // will match the longer piece. ++ shortPieces.sort(cmpLenRev); ++ longPieces.sort(cmpLenRev); ++ mixedPieces.sort(cmpLenRev); ++ for (i = 0; i < 12; i++) { ++ shortPieces[i] = regexEscape(shortPieces[i]); ++ longPieces[i] = regexEscape(longPieces[i]); ++ } ++ for (i = 0; i < 24; i++) { ++ mixedPieces[i] = regexEscape(mixedPieces[i]); ++ } ++ ++ this._monthsRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); ++ this._monthsShortRegex = this._monthsRegex; ++ this._monthsStrictRegex = new RegExp( ++ '^(' + longPieces.join('|') + ')', ++ 'i' ++ ); ++ this._monthsShortStrictRegex = new RegExp( ++ '^(' + shortPieces.join('|') + ')', ++ 'i' ++ ); ++ } ++ ++ // FORMATTING ++ ++ addFormatToken('Y', 0, 0, function () { ++ var y = this.year(); ++ return y <= 9999 ? zeroFill(y, 4) : '+' + y; ++ }); ++ ++ addFormatToken(0, ['YY', 2], 0, function () { ++ return this.year() % 100; ++ }); ++ ++ addFormatToken(0, ['YYYY', 4], 0, 'year'); ++ addFormatToken(0, ['YYYYY', 5], 0, 'year'); ++ addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); ++ ++ // ALIASES ++ ++ addUnitAlias('year', 'y'); ++ ++ // PRIORITIES ++ ++ addUnitPriority('year', 1); ++ ++ // PARSING ++ ++ addRegexToken('Y', matchSigned); ++ addRegexToken('YY', match1to2, match2); ++ addRegexToken('YYYY', match1to4, match4); ++ addRegexToken('YYYYY', match1to6, match6); ++ addRegexToken('YYYYYY', match1to6, match6); ++ ++ addParseToken(['YYYYY', 'YYYYYY'], YEAR); ++ addParseToken('YYYY', function (input, array) { ++ array[YEAR] = ++ input.length === 2 ? hooks.parseTwoDigitYear(input) : toInt(input); ++ }); ++ addParseToken('YY', function (input, array) { ++ array[YEAR] = hooks.parseTwoDigitYear(input); ++ }); ++ addParseToken('Y', function (input, array) { ++ array[YEAR] = parseInt(input, 10); ++ }); ++ ++ // HELPERS ++ ++ function daysInYear(year) { ++ return isLeapYear(year) ? 366 : 365; ++ } ++ ++ // HOOKS ++ ++ hooks.parseTwoDigitYear = function (input) { ++ return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); ++ }; ++ ++ // MOMENTS ++ ++ var getSetYear = makeGetSet('FullYear', true); ++ ++ function getIsLeapYear() { ++ return isLeapYear(this.year()); ++ } ++ ++ function createDate(y, m, d, h, M, s, ms) { ++ // can't just apply() to create a date: ++ // https://stackoverflow.com/q/181348 ++ var date; ++ // the date constructor remaps years 0-99 to 1900-1999 ++ if (y < 100 && y >= 0) { ++ // preserve leap years using a full 400 year cycle, then reset ++ date = new Date(y + 400, m, d, h, M, s, ms); ++ if (isFinite(date.getFullYear())) { ++ date.setFullYear(y); ++ } ++ } else { ++ date = new Date(y, m, d, h, M, s, ms); ++ } ++ ++ return date; ++ } ++ ++ function createUTCDate(y) { ++ var date, args; ++ // the Date.UTC function remaps years 0-99 to 1900-1999 ++ if (y < 100 && y >= 0) { ++ args = Array.prototype.slice.call(arguments); ++ // preserve leap years using a full 400 year cycle, then reset ++ args[0] = y + 400; ++ date = new Date(Date.UTC.apply(null, args)); ++ if (isFinite(date.getUTCFullYear())) { ++ date.setUTCFullYear(y); ++ } ++ } else { ++ date = new Date(Date.UTC.apply(null, arguments)); ++ } ++ ++ return date; ++ } ++ ++ // start-of-first-week - start-of-year ++ function firstWeekOffset(year, dow, doy) { ++ var // first-week day -- which january is always in the first week (4 for iso, 1 for other) ++ fwd = 7 + dow - doy, ++ // first-week day local weekday -- which local weekday is fwd ++ fwdlw = (7 + createUTCDate(year, 0, fwd).getUTCDay() - dow) % 7; ++ ++ return -fwdlw + fwd - 1; ++ } ++ ++ // https://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday ++ function dayOfYearFromWeeks(year, week, weekday, dow, doy) { ++ var localWeekday = (7 + weekday - dow) % 7, ++ weekOffset = firstWeekOffset(year, dow, doy), ++ dayOfYear = 1 + 7 * (week - 1) + localWeekday + weekOffset, ++ resYear, ++ resDayOfYear; ++ ++ if (dayOfYear <= 0) { ++ resYear = year - 1; ++ resDayOfYear = daysInYear(resYear) + dayOfYear; ++ } else if (dayOfYear > daysInYear(year)) { ++ resYear = year + 1; ++ resDayOfYear = dayOfYear - daysInYear(year); ++ } else { ++ resYear = year; ++ resDayOfYear = dayOfYear; ++ } ++ ++ return { ++ year: resYear, ++ dayOfYear: resDayOfYear, ++ }; ++ } ++ ++ function weekOfYear(mom, dow, doy) { ++ var weekOffset = firstWeekOffset(mom.year(), dow, doy), ++ week = Math.floor((mom.dayOfYear() - weekOffset - 1) / 7) + 1, ++ resWeek, ++ resYear; ++ ++ if (week < 1) { ++ resYear = mom.year() - 1; ++ resWeek = week + weeksInYear(resYear, dow, doy); ++ } else if (week > weeksInYear(mom.year(), dow, doy)) { ++ resWeek = week - weeksInYear(mom.year(), dow, doy); ++ resYear = mom.year() + 1; ++ } else { ++ resYear = mom.year(); ++ resWeek = week; ++ } ++ ++ return { ++ week: resWeek, ++ year: resYear, ++ }; ++ } ++ ++ function weeksInYear(year, dow, doy) { ++ var weekOffset = firstWeekOffset(year, dow, doy), ++ weekOffsetNext = firstWeekOffset(year + 1, dow, doy); ++ return (daysInYear(year) - weekOffset + weekOffsetNext) / 7; ++ } ++ ++ // FORMATTING ++ ++ addFormatToken('w', ['ww', 2], 'wo', 'week'); ++ addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); ++ ++ // ALIASES ++ ++ addUnitAlias('week', 'w'); ++ addUnitAlias('isoWeek', 'W'); ++ ++ // PRIORITIES ++ ++ addUnitPriority('week', 5); ++ addUnitPriority('isoWeek', 5); ++ ++ // PARSING ++ ++ addRegexToken('w', match1to2); ++ addRegexToken('ww', match1to2, match2); ++ addRegexToken('W', match1to2); ++ addRegexToken('WW', match1to2, match2); ++ ++ addWeekParseToken(['w', 'ww', 'W', 'WW'], function ( ++ input, ++ week, ++ config, ++ token ++ ) { ++ week[token.substr(0, 1)] = toInt(input); ++ }); ++ ++ // HELPERS ++ ++ // LOCALES ++ ++ function localeWeek(mom) { ++ return weekOfYear(mom, this._week.dow, this._week.doy).week; ++ } ++ ++ var defaultLocaleWeek = { ++ dow: 0, // Sunday is the first day of the week. ++ doy: 6, // The week that contains Jan 6th is the first week of the year. ++ }; ++ ++ function localeFirstDayOfWeek() { ++ return this._week.dow; ++ } ++ ++ function localeFirstDayOfYear() { ++ return this._week.doy; ++ } ++ ++ // MOMENTS ++ ++ function getSetWeek(input) { ++ var week = this.localeData().week(this); ++ return input == null ? week : this.add((input - week) * 7, 'd'); ++ } ++ ++ function getSetISOWeek(input) { ++ var week = weekOfYear(this, 1, 4).week; ++ return input == null ? week : this.add((input - week) * 7, 'd'); ++ } ++ ++ // FORMATTING ++ ++ addFormatToken('d', 0, 'do', 'day'); ++ ++ addFormatToken('dd', 0, 0, function (format) { ++ return this.localeData().weekdaysMin(this, format); ++ }); ++ ++ addFormatToken('ddd', 0, 0, function (format) { ++ return this.localeData().weekdaysShort(this, format); ++ }); ++ ++ addFormatToken('dddd', 0, 0, function (format) { ++ return this.localeData().weekdays(this, format); ++ }); ++ ++ addFormatToken('e', 0, 0, 'weekday'); ++ addFormatToken('E', 0, 0, 'isoWeekday'); ++ ++ // ALIASES ++ ++ addUnitAlias('day', 'd'); ++ addUnitAlias('weekday', 'e'); ++ addUnitAlias('isoWeekday', 'E'); ++ ++ // PRIORITY ++ addUnitPriority('day', 11); ++ addUnitPriority('weekday', 11); ++ addUnitPriority('isoWeekday', 11); ++ ++ // PARSING ++ ++ addRegexToken('d', match1to2); ++ addRegexToken('e', match1to2); ++ addRegexToken('E', match1to2); ++ addRegexToken('dd', function (isStrict, locale) { ++ return locale.weekdaysMinRegex(isStrict); ++ }); ++ addRegexToken('ddd', function (isStrict, locale) { ++ return locale.weekdaysShortRegex(isStrict); ++ }); ++ addRegexToken('dddd', function (isStrict, locale) { ++ return locale.weekdaysRegex(isStrict); ++ }); ++ ++ addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config, token) { ++ var weekday = config._locale.weekdaysParse(input, token, config._strict); ++ // if we didn't get a weekday name, mark the date as invalid ++ if (weekday != null) { ++ week.d = weekday; ++ } else { ++ getParsingFlags(config).invalidWeekday = input; ++ } ++ }); ++ ++ addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { ++ week[token] = toInt(input); ++ }); ++ ++ // HELPERS ++ ++ function parseWeekday(input, locale) { ++ if (typeof input !== 'string') { ++ return input; ++ } ++ ++ if (!isNaN(input)) { ++ return parseInt(input, 10); ++ } ++ ++ input = locale.weekdaysParse(input); ++ if (typeof input === 'number') { ++ return input; ++ } ++ ++ return null; ++ } ++ ++ function parseIsoWeekday(input, locale) { ++ if (typeof input === 'string') { ++ return locale.weekdaysParse(input) % 7 || 7; ++ } ++ return isNaN(input) ? null : input; ++ } ++ ++ // LOCALES ++ function shiftWeekdays(ws, n) { ++ return ws.slice(n, 7).concat(ws.slice(0, n)); ++ } ++ ++ var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split( ++ '_' ++ ), ++ defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), ++ defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), ++ defaultWeekdaysRegex = matchWord, ++ defaultWeekdaysShortRegex = matchWord, ++ defaultWeekdaysMinRegex = matchWord; ++ ++ function localeWeekdays(m, format) { ++ var weekdays = isArray(this._weekdays) ++ ? this._weekdays ++ : this._weekdays[ ++ m && m !== true && this._weekdays.isFormat.test(format) ++ ? 'format' ++ : 'standalone' ++ ]; ++ return m === true ++ ? shiftWeekdays(weekdays, this._week.dow) ++ : m ++ ? weekdays[m.day()] ++ : weekdays; ++ } ++ ++ function localeWeekdaysShort(m) { ++ return m === true ++ ? shiftWeekdays(this._weekdaysShort, this._week.dow) ++ : m ++ ? this._weekdaysShort[m.day()] ++ : this._weekdaysShort; ++ } ++ ++ function localeWeekdaysMin(m) { ++ return m === true ++ ? shiftWeekdays(this._weekdaysMin, this._week.dow) ++ : m ++ ? this._weekdaysMin[m.day()] ++ : this._weekdaysMin; ++ } ++ ++ function handleStrictParse$1(weekdayName, format, strict) { ++ var i, ++ ii, ++ mom, ++ llc = weekdayName.toLocaleLowerCase(); ++ if (!this._weekdaysParse) { ++ this._weekdaysParse = []; ++ this._shortWeekdaysParse = []; ++ this._minWeekdaysParse = []; ++ ++ for (i = 0; i < 7; ++i) { ++ mom = createUTC([2000, 1]).day(i); ++ this._minWeekdaysParse[i] = this.weekdaysMin( ++ mom, ++ '' ++ ).toLocaleLowerCase(); ++ this._shortWeekdaysParse[i] = this.weekdaysShort( ++ mom, ++ '' ++ ).toLocaleLowerCase(); ++ this._weekdaysParse[i] = this.weekdays(mom, '').toLocaleLowerCase(); ++ } ++ } ++ ++ if (strict) { ++ if (format === 'dddd') { ++ ii = indexOf.call(this._weekdaysParse, llc); ++ return ii !== -1 ? ii : null; ++ } else if (format === 'ddd') { ++ ii = indexOf.call(this._shortWeekdaysParse, llc); ++ return ii !== -1 ? ii : null; ++ } else { ++ ii = indexOf.call(this._minWeekdaysParse, llc); ++ return ii !== -1 ? ii : null; ++ } ++ } else { ++ if (format === 'dddd') { ++ ii = indexOf.call(this._weekdaysParse, llc); ++ if (ii !== -1) { ++ return ii; ++ } ++ ii = indexOf.call(this._shortWeekdaysParse, llc); ++ if (ii !== -1) { ++ return ii; ++ } ++ ii = indexOf.call(this._minWeekdaysParse, llc); ++ return ii !== -1 ? ii : null; ++ } else if (format === 'ddd') { ++ ii = indexOf.call(this._shortWeekdaysParse, llc); ++ if (ii !== -1) { ++ return ii; ++ } ++ ii = indexOf.call(this._weekdaysParse, llc); ++ if (ii !== -1) { ++ return ii; ++ } ++ ii = indexOf.call(this._minWeekdaysParse, llc); ++ return ii !== -1 ? ii : null; ++ } else { ++ ii = indexOf.call(this._minWeekdaysParse, llc); ++ if (ii !== -1) { ++ return ii; ++ } ++ ii = indexOf.call(this._weekdaysParse, llc); ++ if (ii !== -1) { ++ return ii; ++ } ++ ii = indexOf.call(this._shortWeekdaysParse, llc); ++ return ii !== -1 ? ii : null; ++ } ++ } ++ } ++ ++ function localeWeekdaysParse(weekdayName, format, strict) { ++ var i, mom, regex; ++ ++ if (this._weekdaysParseExact) { ++ return handleStrictParse$1.call(this, weekdayName, format, strict); ++ } ++ ++ if (!this._weekdaysParse) { ++ this._weekdaysParse = []; ++ this._minWeekdaysParse = []; ++ this._shortWeekdaysParse = []; ++ this._fullWeekdaysParse = []; ++ } ++ ++ for (i = 0; i < 7; i++) { ++ // make the regex if we don't have it already ++ ++ mom = createUTC([2000, 1]).day(i); ++ if (strict && !this._fullWeekdaysParse[i]) { ++ this._fullWeekdaysParse[i] = new RegExp( ++ '^' + this.weekdays(mom, '').replace('.', '\\.?') + '$', ++ 'i' ++ ); ++ this._shortWeekdaysParse[i] = new RegExp( ++ '^' + this.weekdaysShort(mom, '').replace('.', '\\.?') + '$', ++ 'i' ++ ); ++ this._minWeekdaysParse[i] = new RegExp( ++ '^' + this.weekdaysMin(mom, '').replace('.', '\\.?') + '$', ++ 'i' ++ ); ++ } ++ if (!this._weekdaysParse[i]) { ++ regex = ++ '^' + ++ this.weekdays(mom, '') + ++ '|^' + ++ this.weekdaysShort(mom, '') + ++ '|^' + ++ this.weekdaysMin(mom, ''); ++ this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); ++ } ++ // test the regex ++ if ( ++ strict && ++ format === 'dddd' && ++ this._fullWeekdaysParse[i].test(weekdayName) ++ ) { ++ return i; ++ } else if ( ++ strict && ++ format === 'ddd' && ++ this._shortWeekdaysParse[i].test(weekdayName) ++ ) { ++ return i; ++ } else if ( ++ strict && ++ format === 'dd' && ++ this._minWeekdaysParse[i].test(weekdayName) ++ ) { ++ return i; ++ } else if (!strict && this._weekdaysParse[i].test(weekdayName)) { ++ return i; ++ } ++ } ++ } ++ ++ // MOMENTS ++ ++ function getSetDayOfWeek(input) { ++ if (!this.isValid()) { ++ return input != null ? this : NaN; ++ } ++ var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); ++ if (input != null) { ++ input = parseWeekday(input, this.localeData()); ++ return this.add(input - day, 'd'); ++ } else { ++ return day; ++ } ++ } ++ ++ function getSetLocaleDayOfWeek(input) { ++ if (!this.isValid()) { ++ return input != null ? this : NaN; ++ } ++ var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; ++ return input == null ? weekday : this.add(input - weekday, 'd'); ++ } ++ ++ function getSetISODayOfWeek(input) { ++ if (!this.isValid()) { ++ return input != null ? this : NaN; ++ } ++ ++ // behaves the same as moment#day except ++ // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) ++ // as a setter, sunday should belong to the previous week. ++ ++ if (input != null) { ++ var weekday = parseIsoWeekday(input, this.localeData()); ++ return this.day(this.day() % 7 ? weekday : weekday - 7); ++ } else { ++ return this.day() || 7; ++ } ++ } ++ ++ function weekdaysRegex(isStrict) { ++ if (this._weekdaysParseExact) { ++ if (!hasOwnProp(this, '_weekdaysRegex')) { ++ computeWeekdaysParse.call(this); ++ } ++ if (isStrict) { ++ return this._weekdaysStrictRegex; ++ } else { ++ return this._weekdaysRegex; ++ } ++ } else { ++ if (!hasOwnProp(this, '_weekdaysRegex')) { ++ this._weekdaysRegex = defaultWeekdaysRegex; ++ } ++ return this._weekdaysStrictRegex && isStrict ++ ? this._weekdaysStrictRegex ++ : this._weekdaysRegex; ++ } ++ } ++ ++ function weekdaysShortRegex(isStrict) { ++ if (this._weekdaysParseExact) { ++ if (!hasOwnProp(this, '_weekdaysRegex')) { ++ computeWeekdaysParse.call(this); ++ } ++ if (isStrict) { ++ return this._weekdaysShortStrictRegex; ++ } else { ++ return this._weekdaysShortRegex; ++ } ++ } else { ++ if (!hasOwnProp(this, '_weekdaysShortRegex')) { ++ this._weekdaysShortRegex = defaultWeekdaysShortRegex; ++ } ++ return this._weekdaysShortStrictRegex && isStrict ++ ? this._weekdaysShortStrictRegex ++ : this._weekdaysShortRegex; ++ } ++ } ++ ++ function weekdaysMinRegex(isStrict) { ++ if (this._weekdaysParseExact) { ++ if (!hasOwnProp(this, '_weekdaysRegex')) { ++ computeWeekdaysParse.call(this); ++ } ++ if (isStrict) { ++ return this._weekdaysMinStrictRegex; ++ } else { ++ return this._weekdaysMinRegex; ++ } ++ } else { ++ if (!hasOwnProp(this, '_weekdaysMinRegex')) { ++ this._weekdaysMinRegex = defaultWeekdaysMinRegex; ++ } ++ return this._weekdaysMinStrictRegex && isStrict ++ ? this._weekdaysMinStrictRegex ++ : this._weekdaysMinRegex; ++ } ++ } ++ ++ function computeWeekdaysParse() { ++ function cmpLenRev(a, b) { ++ return b.length - a.length; ++ } ++ ++ var minPieces = [], ++ shortPieces = [], ++ longPieces = [], ++ mixedPieces = [], ++ i, ++ mom, ++ minp, ++ shortp, ++ longp; ++ for (i = 0; i < 7; i++) { ++ // make the regex if we don't have it already ++ mom = createUTC([2000, 1]).day(i); ++ minp = regexEscape(this.weekdaysMin(mom, '')); ++ shortp = regexEscape(this.weekdaysShort(mom, '')); ++ longp = regexEscape(this.weekdays(mom, '')); ++ minPieces.push(minp); ++ shortPieces.push(shortp); ++ longPieces.push(longp); ++ mixedPieces.push(minp); ++ mixedPieces.push(shortp); ++ mixedPieces.push(longp); ++ } ++ // Sorting makes sure if one weekday (or abbr) is a prefix of another it ++ // will match the longer piece. ++ minPieces.sort(cmpLenRev); ++ shortPieces.sort(cmpLenRev); ++ longPieces.sort(cmpLenRev); ++ mixedPieces.sort(cmpLenRev); ++ ++ this._weekdaysRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); ++ this._weekdaysShortRegex = this._weekdaysRegex; ++ this._weekdaysMinRegex = this._weekdaysRegex; ++ ++ this._weekdaysStrictRegex = new RegExp( ++ '^(' + longPieces.join('|') + ')', ++ 'i' ++ ); ++ this._weekdaysShortStrictRegex = new RegExp( ++ '^(' + shortPieces.join('|') + ')', ++ 'i' ++ ); ++ this._weekdaysMinStrictRegex = new RegExp( ++ '^(' + minPieces.join('|') + ')', ++ 'i' ++ ); ++ } ++ ++ // FORMATTING ++ ++ function hFormat() { ++ return this.hours() % 12 || 12; ++ } ++ ++ function kFormat() { ++ return this.hours() || 24; ++ } ++ ++ addFormatToken('H', ['HH', 2], 0, 'hour'); ++ addFormatToken('h', ['hh', 2], 0, hFormat); ++ addFormatToken('k', ['kk', 2], 0, kFormat); ++ ++ addFormatToken('hmm', 0, 0, function () { ++ return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2); ++ }); ++ ++ addFormatToken('hmmss', 0, 0, function () { ++ return ( ++ '' + ++ hFormat.apply(this) + ++ zeroFill(this.minutes(), 2) + ++ zeroFill(this.seconds(), 2) ++ ); ++ }); ++ ++ addFormatToken('Hmm', 0, 0, function () { ++ return '' + this.hours() + zeroFill(this.minutes(), 2); ++ }); ++ ++ addFormatToken('Hmmss', 0, 0, function () { ++ return ( ++ '' + ++ this.hours() + ++ zeroFill(this.minutes(), 2) + ++ zeroFill(this.seconds(), 2) ++ ); ++ }); ++ ++ function meridiem(token, lowercase) { ++ addFormatToken(token, 0, 0, function () { ++ return this.localeData().meridiem( ++ this.hours(), ++ this.minutes(), ++ lowercase ++ ); ++ }); ++ } ++ ++ meridiem('a', true); ++ meridiem('A', false); ++ ++ // ALIASES ++ ++ addUnitAlias('hour', 'h'); ++ ++ // PRIORITY ++ addUnitPriority('hour', 13); ++ ++ // PARSING ++ ++ function matchMeridiem(isStrict, locale) { ++ return locale._meridiemParse; ++ } ++ ++ addRegexToken('a', matchMeridiem); ++ addRegexToken('A', matchMeridiem); ++ addRegexToken('H', match1to2); ++ addRegexToken('h', match1to2); ++ addRegexToken('k', match1to2); ++ addRegexToken('HH', match1to2, match2); ++ addRegexToken('hh', match1to2, match2); ++ addRegexToken('kk', match1to2, match2); ++ ++ addRegexToken('hmm', match3to4); ++ addRegexToken('hmmss', match5to6); ++ addRegexToken('Hmm', match3to4); ++ addRegexToken('Hmmss', match5to6); ++ ++ addParseToken(['H', 'HH'], HOUR); ++ addParseToken(['k', 'kk'], function (input, array, config) { ++ var kInput = toInt(input); ++ array[HOUR] = kInput === 24 ? 0 : kInput; ++ }); ++ addParseToken(['a', 'A'], function (input, array, config) { ++ config._isPm = config._locale.isPM(input); ++ config._meridiem = input; ++ }); ++ addParseToken(['h', 'hh'], function (input, array, config) { ++ array[HOUR] = toInt(input); ++ getParsingFlags(config).bigHour = true; ++ }); ++ addParseToken('hmm', function (input, array, config) { ++ var pos = input.length - 2; ++ array[HOUR] = toInt(input.substr(0, pos)); ++ array[MINUTE] = toInt(input.substr(pos)); ++ getParsingFlags(config).bigHour = true; ++ }); ++ addParseToken('hmmss', function (input, array, config) { ++ var pos1 = input.length - 4, ++ pos2 = input.length - 2; ++ array[HOUR] = toInt(input.substr(0, pos1)); ++ array[MINUTE] = toInt(input.substr(pos1, 2)); ++ array[SECOND] = toInt(input.substr(pos2)); ++ getParsingFlags(config).bigHour = true; ++ }); ++ addParseToken('Hmm', function (input, array, config) { ++ var pos = input.length - 2; ++ array[HOUR] = toInt(input.substr(0, pos)); ++ array[MINUTE] = toInt(input.substr(pos)); ++ }); ++ addParseToken('Hmmss', function (input, array, config) { ++ var pos1 = input.length - 4, ++ pos2 = input.length - 2; ++ array[HOUR] = toInt(input.substr(0, pos1)); ++ array[MINUTE] = toInt(input.substr(pos1, 2)); ++ array[SECOND] = toInt(input.substr(pos2)); ++ }); ++ ++ // LOCALES ++ ++ function localeIsPM(input) { ++ // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays ++ // Using charAt should be more compatible. ++ return (input + '').toLowerCase().charAt(0) === 'p'; ++ } ++ ++ var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i, ++ // Setting the hour should keep the time, because the user explicitly ++ // specified which hour they want. So trying to maintain the same hour (in ++ // a new timezone) makes sense. Adding/subtracting hours does not follow ++ // this rule. ++ getSetHour = makeGetSet('Hours', true); ++ ++ function localeMeridiem(hours, minutes, isLower) { ++ if (hours > 11) { ++ return isLower ? 'pm' : 'PM'; ++ } else { ++ return isLower ? 'am' : 'AM'; ++ } ++ } ++ ++ var baseConfig = { ++ calendar: defaultCalendar, ++ longDateFormat: defaultLongDateFormat, ++ invalidDate: defaultInvalidDate, ++ ordinal: defaultOrdinal, ++ dayOfMonthOrdinalParse: defaultDayOfMonthOrdinalParse, ++ relativeTime: defaultRelativeTime, ++ ++ months: defaultLocaleMonths, ++ monthsShort: defaultLocaleMonthsShort, ++ ++ week: defaultLocaleWeek, ++ ++ weekdays: defaultLocaleWeekdays, ++ weekdaysMin: defaultLocaleWeekdaysMin, ++ weekdaysShort: defaultLocaleWeekdaysShort, ++ ++ meridiemParse: defaultLocaleMeridiemParse, ++ }; ++ ++ // internal storage for locale config files ++ var locales = {}, ++ localeFamilies = {}, ++ globalLocale; ++ ++ function commonPrefix(arr1, arr2) { ++ var i, ++ minl = Math.min(arr1.length, arr2.length); ++ for (i = 0; i < minl; i += 1) { ++ if (arr1[i] !== arr2[i]) { ++ return i; ++ } ++ } ++ return minl; ++ } ++ ++ function normalizeLocale(key) { ++ return key ? key.toLowerCase().replace('_', '-') : key; ++ } ++ ++ // pick the locale from the array ++ // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each ++ // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root ++ function chooseLocale(names) { ++ var i = 0, ++ j, ++ next, ++ locale, ++ split; ++ ++ while (i < names.length) { ++ split = normalizeLocale(names[i]).split('-'); ++ j = split.length; ++ next = normalizeLocale(names[i + 1]); ++ next = next ? next.split('-') : null; ++ while (j > 0) { ++ locale = loadLocale(split.slice(0, j).join('-')); ++ if (locale) { ++ return locale; ++ } ++ if ( ++ next && ++ next.length >= j && ++ commonPrefix(split, next) >= j - 1 ++ ) { ++ //the next array item is better than a shallower substring of this one ++ break; ++ } ++ j--; ++ } ++ i++; ++ } ++ return globalLocale; ++ } ++ ++ function loadLocale(name) { ++ var oldLocale = null, ++ aliasedRequire; ++ // TODO: Find a better way to register and load all the locales in Node ++ if ( ++ locales[name] === undefined && ++ typeof module !== 'undefined' && ++ module && ++ module.exports ++ ) { ++ try { ++ oldLocale = globalLocale._abbr; ++ aliasedRequire = require; ++ aliasedRequire('./locale/' + name); ++ getSetGlobalLocale(oldLocale); ++ } catch (e) { ++ // mark as not found to avoid repeating expensive file require call causing high CPU ++ // when trying to find en-US, en_US, en-us for every format call ++ locales[name] = null; // null means not found ++ } ++ } ++ return locales[name]; ++ } ++ ++ // This function will load locale and then set the global locale. If ++ // no arguments are passed in, it will simply return the current global ++ // locale key. ++ function getSetGlobalLocale(key, values) { ++ var data; ++ if (key) { ++ if (isUndefined(values)) { ++ data = getLocale(key); ++ } else { ++ data = defineLocale(key, values); ++ } ++ ++ if (data) { ++ // moment.duration._locale = moment._locale = data; ++ globalLocale = data; ++ } else { ++ if (typeof console !== 'undefined' && console.warn) { ++ //warn user if arguments are passed but the locale could not be set ++ console.warn( ++ 'Locale ' + key + ' not found. Did you forget to load it?' ++ ); ++ } ++ } ++ } ++ ++ return globalLocale._abbr; ++ } ++ ++ function defineLocale(name, config) { ++ if (config !== null) { ++ var locale, ++ parentConfig = baseConfig; ++ config.abbr = name; ++ if (locales[name] != null) { ++ deprecateSimple( ++ 'defineLocaleOverride', ++ 'use moment.updateLocale(localeName, config) to change ' + ++ 'an existing locale. moment.defineLocale(localeName, ' + ++ 'config) should only be used for creating a new locale ' + ++ 'See http://momentjs.com/guides/#/warnings/define-locale/ for more info.' ++ ); ++ parentConfig = locales[name]._config; ++ } else if (config.parentLocale != null) { ++ if (locales[config.parentLocale] != null) { ++ parentConfig = locales[config.parentLocale]._config; ++ } else { ++ locale = loadLocale(config.parentLocale); ++ if (locale != null) { ++ parentConfig = locale._config; ++ } else { ++ if (!localeFamilies[config.parentLocale]) { ++ localeFamilies[config.parentLocale] = []; ++ } ++ localeFamilies[config.parentLocale].push({ ++ name: name, ++ config: config, ++ }); ++ return null; ++ } ++ } ++ } ++ locales[name] = new Locale(mergeConfigs(parentConfig, config)); ++ ++ if (localeFamilies[name]) { ++ localeFamilies[name].forEach(function (x) { ++ defineLocale(x.name, x.config); ++ }); ++ } ++ ++ // backwards compat for now: also set the locale ++ // make sure we set the locale AFTER all child locales have been ++ // created, so we won't end up with the child locale set. ++ getSetGlobalLocale(name); ++ ++ return locales[name]; ++ } else { ++ // useful for testing ++ delete locales[name]; ++ return null; ++ } ++ } ++ ++ function updateLocale(name, config) { ++ if (config != null) { ++ var locale, ++ tmpLocale, ++ parentConfig = baseConfig; ++ ++ if (locales[name] != null && locales[name].parentLocale != null) { ++ // Update existing child locale in-place to avoid memory-leaks ++ locales[name].set(mergeConfigs(locales[name]._config, config)); ++ } else { ++ // MERGE ++ tmpLocale = loadLocale(name); ++ if (tmpLocale != null) { ++ parentConfig = tmpLocale._config; ++ } ++ config = mergeConfigs(parentConfig, config); ++ if (tmpLocale == null) { ++ // updateLocale is called for creating a new locale ++ // Set abbr so it will have a name (getters return ++ // undefined otherwise). ++ config.abbr = name; ++ } ++ locale = new Locale(config); ++ locale.parentLocale = locales[name]; ++ locales[name] = locale; ++ } ++ ++ // backwards compat for now: also set the locale ++ getSetGlobalLocale(name); ++ } else { ++ // pass null for config to unupdate, useful for tests ++ if (locales[name] != null) { ++ if (locales[name].parentLocale != null) { ++ locales[name] = locales[name].parentLocale; ++ if (name === getSetGlobalLocale()) { ++ getSetGlobalLocale(name); ++ } ++ } else if (locales[name] != null) { ++ delete locales[name]; ++ } ++ } ++ } ++ return locales[name]; ++ } ++ ++ // returns locale data ++ function getLocale(key) { ++ var locale; ++ ++ if (key && key._locale && key._locale._abbr) { ++ key = key._locale._abbr; ++ } ++ ++ if (!key) { ++ return globalLocale; ++ } ++ ++ if (!isArray(key)) { ++ //short-circuit everything else ++ locale = loadLocale(key); ++ if (locale) { ++ return locale; ++ } ++ key = [key]; ++ } ++ ++ return chooseLocale(key); ++ } ++ ++ function listLocales() { ++ return keys(locales); ++ } ++ ++ function checkOverflow(m) { ++ var overflow, ++ a = m._a; ++ ++ if (a && getParsingFlags(m).overflow === -2) { ++ overflow = ++ a[MONTH] < 0 || a[MONTH] > 11 ++ ? MONTH ++ : a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ++ ? DATE ++ : a[HOUR] < 0 || ++ a[HOUR] > 24 || ++ (a[HOUR] === 24 && ++ (a[MINUTE] !== 0 || ++ a[SECOND] !== 0 || ++ a[MILLISECOND] !== 0)) ++ ? HOUR ++ : a[MINUTE] < 0 || a[MINUTE] > 59 ++ ? MINUTE ++ : a[SECOND] < 0 || a[SECOND] > 59 ++ ? SECOND ++ : a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ++ ? MILLISECOND ++ : -1; ++ ++ if ( ++ getParsingFlags(m)._overflowDayOfYear && ++ (overflow < YEAR || overflow > DATE) ++ ) { ++ overflow = DATE; ++ } ++ if (getParsingFlags(m)._overflowWeeks && overflow === -1) { ++ overflow = WEEK; ++ } ++ if (getParsingFlags(m)._overflowWeekday && overflow === -1) { ++ overflow = WEEKDAY; ++ } ++ ++ getParsingFlags(m).overflow = overflow; ++ } ++ ++ return m; ++ } ++ ++ // iso 8601 regex ++ // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00) ++ var extendedIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/, ++ basicIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d|))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/, ++ tzRegex = /Z|[+-]\d\d(?::?\d\d)?/, ++ isoDates = [ ++ ['YYYYYY-MM-DD', /[+-]\d{6}-\d\d-\d\d/], ++ ['YYYY-MM-DD', /\d{4}-\d\d-\d\d/], ++ ['GGGG-[W]WW-E', /\d{4}-W\d\d-\d/], ++ ['GGGG-[W]WW', /\d{4}-W\d\d/, false], ++ ['YYYY-DDD', /\d{4}-\d{3}/], ++ ['YYYY-MM', /\d{4}-\d\d/, false], ++ ['YYYYYYMMDD', /[+-]\d{10}/], ++ ['YYYYMMDD', /\d{8}/], ++ ['GGGG[W]WWE', /\d{4}W\d{3}/], ++ ['GGGG[W]WW', /\d{4}W\d{2}/, false], ++ ['YYYYDDD', /\d{7}/], ++ ['YYYYMM', /\d{6}/, false], ++ ['YYYY', /\d{4}/, false], ++ ], ++ // iso time formats and regexes ++ isoTimes = [ ++ ['HH:mm:ss.SSSS', /\d\d:\d\d:\d\d\.\d+/], ++ ['HH:mm:ss,SSSS', /\d\d:\d\d:\d\d,\d+/], ++ ['HH:mm:ss', /\d\d:\d\d:\d\d/], ++ ['HH:mm', /\d\d:\d\d/], ++ ['HHmmss.SSSS', /\d\d\d\d\d\d\.\d+/], ++ ['HHmmss,SSSS', /\d\d\d\d\d\d,\d+/], ++ ['HHmmss', /\d\d\d\d\d\d/], ++ ['HHmm', /\d\d\d\d/], ++ ['HH', /\d\d/], ++ ], ++ aspNetJsonRegex = /^\/?Date\((-?\d+)/i, ++ // RFC 2822 regex: For details see https://tools.ietf.org/html/rfc2822#section-3.3 ++ rfc2822 = /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/, ++ obsOffsets = { ++ UT: 0, ++ GMT: 0, ++ EDT: -4 * 60, ++ EST: -5 * 60, ++ CDT: -5 * 60, ++ CST: -6 * 60, ++ MDT: -6 * 60, ++ MST: -7 * 60, ++ PDT: -7 * 60, ++ PST: -8 * 60, ++ }; ++ ++ // date from iso format ++ function configFromISO(config) { ++ var i, ++ l, ++ string = config._i, ++ match = extendedIsoRegex.exec(string) || basicIsoRegex.exec(string), ++ allowTime, ++ dateFormat, ++ timeFormat, ++ tzFormat; ++ ++ if (match) { ++ getParsingFlags(config).iso = true; ++ ++ for (i = 0, l = isoDates.length; i < l; i++) { ++ if (isoDates[i][1].exec(match[1])) { ++ dateFormat = isoDates[i][0]; ++ allowTime = isoDates[i][2] !== false; ++ break; ++ } ++ } ++ if (dateFormat == null) { ++ config._isValid = false; ++ return; ++ } ++ if (match[3]) { ++ for (i = 0, l = isoTimes.length; i < l; i++) { ++ if (isoTimes[i][1].exec(match[3])) { ++ // match[2] should be 'T' or space ++ timeFormat = (match[2] || ' ') + isoTimes[i][0]; ++ break; ++ } ++ } ++ if (timeFormat == null) { ++ config._isValid = false; ++ return; ++ } ++ } ++ if (!allowTime && timeFormat != null) { ++ config._isValid = false; ++ return; ++ } ++ if (match[4]) { ++ if (tzRegex.exec(match[4])) { ++ tzFormat = 'Z'; ++ } else { ++ config._isValid = false; ++ return; ++ } ++ } ++ config._f = dateFormat + (timeFormat || '') + (tzFormat || ''); ++ configFromStringAndFormat(config); ++ } else { ++ config._isValid = false; ++ } ++ } ++ ++ function extractFromRFC2822Strings( ++ yearStr, ++ monthStr, ++ dayStr, ++ hourStr, ++ minuteStr, ++ secondStr ++ ) { ++ var result = [ ++ untruncateYear(yearStr), ++ defaultLocaleMonthsShort.indexOf(monthStr), ++ parseInt(dayStr, 10), ++ parseInt(hourStr, 10), ++ parseInt(minuteStr, 10), ++ ]; ++ ++ if (secondStr) { ++ result.push(parseInt(secondStr, 10)); ++ } ++ ++ return result; ++ } ++ ++ function untruncateYear(yearStr) { ++ var year = parseInt(yearStr, 10); ++ if (year <= 49) { ++ return 2000 + year; ++ } else if (year <= 999) { ++ return 1900 + year; ++ } ++ return year; ++ } ++ ++ function preprocessRFC2822(s) { ++ // Remove comments and folding whitespace and replace multiple-spaces with a single space ++ return s ++ .replace(/\([^)]*\)|[\n\t]/g, ' ') ++ .replace(/(\s\s+)/g, ' ') ++ .replace(/^\s\s*/, '') ++ .replace(/\s\s*$/, ''); ++ } ++ ++ function checkWeekday(weekdayStr, parsedInput, config) { ++ if (weekdayStr) { ++ // TODO: Replace the vanilla JS Date object with an independent day-of-week check. ++ var weekdayProvided = defaultLocaleWeekdaysShort.indexOf(weekdayStr), ++ weekdayActual = new Date( ++ parsedInput[0], ++ parsedInput[1], ++ parsedInput[2] ++ ).getDay(); ++ if (weekdayProvided !== weekdayActual) { ++ getParsingFlags(config).weekdayMismatch = true; ++ config._isValid = false; ++ return false; ++ } ++ } ++ return true; ++ } ++ ++ function calculateOffset(obsOffset, militaryOffset, numOffset) { ++ if (obsOffset) { ++ return obsOffsets[obsOffset]; ++ } else if (militaryOffset) { ++ // the only allowed military tz is Z ++ return 0; ++ } else { ++ var hm = parseInt(numOffset, 10), ++ m = hm % 100, ++ h = (hm - m) / 100; ++ return h * 60 + m; ++ } ++ } ++ ++ // date and time from ref 2822 format ++ function configFromRFC2822(config) { ++ var match = rfc2822.exec(preprocessRFC2822(config._i)), ++ parsedArray; ++ if (match) { ++ parsedArray = extractFromRFC2822Strings( ++ match[4], ++ match[3], ++ match[2], ++ match[5], ++ match[6], ++ match[7] ++ ); ++ if (!checkWeekday(match[1], parsedArray, config)) { ++ return; ++ } ++ ++ config._a = parsedArray; ++ config._tzm = calculateOffset(match[8], match[9], match[10]); ++ ++ config._d = createUTCDate.apply(null, config._a); ++ config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); ++ ++ getParsingFlags(config).rfc2822 = true; ++ } else { ++ config._isValid = false; ++ } ++ } ++ ++ // date from 1) ASP.NET, 2) ISO, 3) RFC 2822 formats, or 4) optional fallback if parsing isn't strict ++ function configFromString(config) { ++ var matched = aspNetJsonRegex.exec(config._i); ++ if (matched !== null) { ++ config._d = new Date(+matched[1]); ++ return; ++ } ++ ++ configFromISO(config); ++ if (config._isValid === false) { ++ delete config._isValid; ++ } else { ++ return; ++ } ++ ++ configFromRFC2822(config); ++ if (config._isValid === false) { ++ delete config._isValid; ++ } else { ++ return; ++ } ++ ++ if (config._strict) { ++ config._isValid = false; ++ } else { ++ // Final attempt, use Input Fallback ++ hooks.createFromInputFallback(config); ++ } ++ } ++ ++ hooks.createFromInputFallback = deprecate( ++ 'value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), ' + ++ 'which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are ' + ++ 'discouraged. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.', ++ function (config) { ++ config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); ++ } ++ ); ++ ++ // Pick the first defined of two or three arguments. ++ function defaults(a, b, c) { ++ if (a != null) { ++ return a; ++ } ++ if (b != null) { ++ return b; ++ } ++ return c; ++ } ++ ++ function currentDateArray(config) { ++ // hooks is actually the exported moment object ++ var nowValue = new Date(hooks.now()); ++ if (config._useUTC) { ++ return [ ++ nowValue.getUTCFullYear(), ++ nowValue.getUTCMonth(), ++ nowValue.getUTCDate(), ++ ]; ++ } ++ return [nowValue.getFullYear(), nowValue.getMonth(), nowValue.getDate()]; ++ } ++ ++ // convert an array to a date. ++ // the array should mirror the parameters below ++ // note: all values past the year are optional and will default to the lowest possible value. ++ // [year, month, day , hour, minute, second, millisecond] ++ function configFromArray(config) { ++ var i, ++ date, ++ input = [], ++ currentDate, ++ expectedWeekday, ++ yearToUse; ++ ++ if (config._d) { ++ return; ++ } ++ ++ currentDate = currentDateArray(config); ++ ++ //compute day of the year from weeks and weekdays ++ if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { ++ dayOfYearFromWeekInfo(config); ++ } ++ ++ //if the day of the year is set, figure out what it is ++ if (config._dayOfYear != null) { ++ yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); ++ ++ if ( ++ config._dayOfYear > daysInYear(yearToUse) || ++ config._dayOfYear === 0 ++ ) { ++ getParsingFlags(config)._overflowDayOfYear = true; ++ } ++ ++ date = createUTCDate(yearToUse, 0, config._dayOfYear); ++ config._a[MONTH] = date.getUTCMonth(); ++ config._a[DATE] = date.getUTCDate(); ++ } ++ ++ // Default to current date. ++ // * if no year, month, day of month are given, default to today ++ // * if day of month is given, default month and year ++ // * if month is given, default only year ++ // * if year is given, don't default anything ++ for (i = 0; i < 3 && config._a[i] == null; ++i) { ++ config._a[i] = input[i] = currentDate[i]; ++ } ++ ++ // Zero out whatever was not defaulted, including time ++ for (; i < 7; i++) { ++ config._a[i] = input[i] = ++ config._a[i] == null ? (i === 2 ? 1 : 0) : config._a[i]; ++ } ++ ++ // Check for 24:00:00.000 ++ if ( ++ config._a[HOUR] === 24 && ++ config._a[MINUTE] === 0 && ++ config._a[SECOND] === 0 && ++ config._a[MILLISECOND] === 0 ++ ) { ++ config._nextDay = true; ++ config._a[HOUR] = 0; ++ } ++ ++ config._d = (config._useUTC ? createUTCDate : createDate).apply( ++ null, ++ input ++ ); ++ expectedWeekday = config._useUTC ++ ? config._d.getUTCDay() ++ : config._d.getDay(); ++ ++ // Apply timezone offset from input. The actual utcOffset can be changed ++ // with parseZone. ++ if (config._tzm != null) { ++ config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); ++ } ++ ++ if (config._nextDay) { ++ config._a[HOUR] = 24; ++ } ++ ++ // check for mismatching day of week ++ if ( ++ config._w && ++ typeof config._w.d !== 'undefined' && ++ config._w.d !== expectedWeekday ++ ) { ++ getParsingFlags(config).weekdayMismatch = true; ++ } ++ } ++ ++ function dayOfYearFromWeekInfo(config) { ++ var w, weekYear, week, weekday, dow, doy, temp, weekdayOverflow, curWeek; ++ ++ w = config._w; ++ if (w.GG != null || w.W != null || w.E != null) { ++ dow = 1; ++ doy = 4; ++ ++ // TODO: We need to take the current isoWeekYear, but that depends on ++ // how we interpret now (local, utc, fixed offset). So create ++ // a now version of current config (take local/utc/offset flags, and ++ // create now). ++ weekYear = defaults( ++ w.GG, ++ config._a[YEAR], ++ weekOfYear(createLocal(), 1, 4).year ++ ); ++ week = defaults(w.W, 1); ++ weekday = defaults(w.E, 1); ++ if (weekday < 1 || weekday > 7) { ++ weekdayOverflow = true; ++ } ++ } else { ++ dow = config._locale._week.dow; ++ doy = config._locale._week.doy; ++ ++ curWeek = weekOfYear(createLocal(), dow, doy); ++ ++ weekYear = defaults(w.gg, config._a[YEAR], curWeek.year); ++ ++ // Default to current week. ++ week = defaults(w.w, curWeek.week); ++ ++ if (w.d != null) { ++ // weekday -- low day numbers are considered next week ++ weekday = w.d; ++ if (weekday < 0 || weekday > 6) { ++ weekdayOverflow = true; ++ } ++ } else if (w.e != null) { ++ // local weekday -- counting starts from beginning of week ++ weekday = w.e + dow; ++ if (w.e < 0 || w.e > 6) { ++ weekdayOverflow = true; ++ } ++ } else { ++ // default to beginning of week ++ weekday = dow; ++ } ++ } ++ if (week < 1 || week > weeksInYear(weekYear, dow, doy)) { ++ getParsingFlags(config)._overflowWeeks = true; ++ } else if (weekdayOverflow != null) { ++ getParsingFlags(config)._overflowWeekday = true; ++ } else { ++ temp = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy); ++ config._a[YEAR] = temp.year; ++ config._dayOfYear = temp.dayOfYear; ++ } ++ } ++ ++ // constant that refers to the ISO standard ++ hooks.ISO_8601 = function () {}; ++ ++ // constant that refers to the RFC 2822 form ++ hooks.RFC_2822 = function () {}; ++ ++ // date from string and format string ++ function configFromStringAndFormat(config) { ++ // TODO: Move this to another part of the creation flow to prevent circular deps ++ if (config._f === hooks.ISO_8601) { ++ configFromISO(config); ++ return; ++ } ++ if (config._f === hooks.RFC_2822) { ++ configFromRFC2822(config); ++ return; ++ } ++ config._a = []; ++ getParsingFlags(config).empty = true; ++ ++ // This array is used to make a Date, either with `new Date` or `Date.UTC` ++ var string = '' + config._i, ++ i, ++ parsedInput, ++ tokens, ++ token, ++ skipped, ++ stringLength = string.length, ++ totalParsedInputLength = 0, ++ era; ++ ++ tokens = ++ expandFormat(config._f, config._locale).match(formattingTokens) || []; ++ ++ for (i = 0; i < tokens.length; i++) { ++ token = tokens[i]; ++ parsedInput = (string.match(getParseRegexForToken(token, config)) || ++ [])[0]; ++ if (parsedInput) { ++ skipped = string.substr(0, string.indexOf(parsedInput)); ++ if (skipped.length > 0) { ++ getParsingFlags(config).unusedInput.push(skipped); ++ } ++ string = string.slice( ++ string.indexOf(parsedInput) + parsedInput.length ++ ); ++ totalParsedInputLength += parsedInput.length; ++ } ++ // don't parse if it's not a known token ++ if (formatTokenFunctions[token]) { ++ if (parsedInput) { ++ getParsingFlags(config).empty = false; ++ } else { ++ getParsingFlags(config).unusedTokens.push(token); ++ } ++ addTimeToArrayFromToken(token, parsedInput, config); ++ } else if (config._strict && !parsedInput) { ++ getParsingFlags(config).unusedTokens.push(token); ++ } ++ } ++ ++ // add remaining unparsed input length to the string ++ getParsingFlags(config).charsLeftOver = ++ stringLength - totalParsedInputLength; ++ if (string.length > 0) { ++ getParsingFlags(config).unusedInput.push(string); ++ } ++ ++ // clear _12h flag if hour is <= 12 ++ if ( ++ config._a[HOUR] <= 12 && ++ getParsingFlags(config).bigHour === true && ++ config._a[HOUR] > 0 ++ ) { ++ getParsingFlags(config).bigHour = undefined; ++ } ++ ++ getParsingFlags(config).parsedDateParts = config._a.slice(0); ++ getParsingFlags(config).meridiem = config._meridiem; ++ // handle meridiem ++ config._a[HOUR] = meridiemFixWrap( ++ config._locale, ++ config._a[HOUR], ++ config._meridiem ++ ); ++ ++ // handle era ++ era = getParsingFlags(config).era; ++ if (era !== null) { ++ config._a[YEAR] = config._locale.erasConvertYear(era, config._a[YEAR]); ++ } ++ ++ configFromArray(config); ++ checkOverflow(config); ++ } ++ ++ function meridiemFixWrap(locale, hour, meridiem) { ++ var isPm; ++ ++ if (meridiem == null) { ++ // nothing to do ++ return hour; ++ } ++ if (locale.meridiemHour != null) { ++ return locale.meridiemHour(hour, meridiem); ++ } else if (locale.isPM != null) { ++ // Fallback ++ isPm = locale.isPM(meridiem); ++ if (isPm && hour < 12) { ++ hour += 12; ++ } ++ if (!isPm && hour === 12) { ++ hour = 0; ++ } ++ return hour; ++ } else { ++ // this is not supposed to happen ++ return hour; ++ } ++ } ++ ++ // date from string and array of format strings ++ function configFromStringAndArray(config) { ++ var tempConfig, ++ bestMoment, ++ scoreToBeat, ++ i, ++ currentScore, ++ validFormatFound, ++ bestFormatIsValid = false; ++ ++ if (config._f.length === 0) { ++ getParsingFlags(config).invalidFormat = true; ++ config._d = new Date(NaN); ++ return; ++ } ++ ++ for (i = 0; i < config._f.length; i++) { ++ currentScore = 0; ++ validFormatFound = false; ++ tempConfig = copyConfig({}, config); ++ if (config._useUTC != null) { ++ tempConfig._useUTC = config._useUTC; ++ } ++ tempConfig._f = config._f[i]; ++ configFromStringAndFormat(tempConfig); ++ ++ if (isValid(tempConfig)) { ++ validFormatFound = true; ++ } ++ ++ // if there is any input that was not parsed add a penalty for that format ++ currentScore += getParsingFlags(tempConfig).charsLeftOver; ++ ++ //or tokens ++ currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; ++ ++ getParsingFlags(tempConfig).score = currentScore; ++ ++ if (!bestFormatIsValid) { ++ if ( ++ scoreToBeat == null || ++ currentScore < scoreToBeat || ++ validFormatFound ++ ) { ++ scoreToBeat = currentScore; ++ bestMoment = tempConfig; ++ if (validFormatFound) { ++ bestFormatIsValid = true; ++ } ++ } ++ } else { ++ if (currentScore < scoreToBeat) { ++ scoreToBeat = currentScore; ++ bestMoment = tempConfig; ++ } ++ } ++ } ++ ++ extend(config, bestMoment || tempConfig); ++ } ++ ++ function configFromObject(config) { ++ if (config._d) { ++ return; ++ } ++ ++ var i = normalizeObjectUnits(config._i), ++ dayOrDate = i.day === undefined ? i.date : i.day; ++ config._a = map( ++ [i.year, i.month, dayOrDate, i.hour, i.minute, i.second, i.millisecond], ++ function (obj) { ++ return obj && parseInt(obj, 10); ++ } ++ ); ++ ++ configFromArray(config); ++ } ++ ++ function createFromConfig(config) { ++ var res = new Moment(checkOverflow(prepareConfig(config))); ++ if (res._nextDay) { ++ // Adding is smart enough around DST ++ res.add(1, 'd'); ++ res._nextDay = undefined; ++ } ++ ++ return res; ++ } ++ ++ function prepareConfig(config) { ++ var input = config._i, ++ format = config._f; ++ ++ config._locale = config._locale || getLocale(config._l); ++ ++ if (input === null || (format === undefined && input === '')) { ++ return createInvalid({ nullInput: true }); ++ } ++ ++ if (typeof input === 'string') { ++ config._i = input = config._locale.preparse(input); ++ } ++ ++ if (isMoment(input)) { ++ return new Moment(checkOverflow(input)); ++ } else if (isDate(input)) { ++ config._d = input; ++ } else if (isArray(format)) { ++ configFromStringAndArray(config); ++ } else if (format) { ++ configFromStringAndFormat(config); ++ } else { ++ configFromInput(config); ++ } ++ ++ if (!isValid(config)) { ++ config._d = null; ++ } ++ ++ return config; ++ } ++ ++ function configFromInput(config) { ++ var input = config._i; ++ if (isUndefined(input)) { ++ config._d = new Date(hooks.now()); ++ } else if (isDate(input)) { ++ config._d = new Date(input.valueOf()); ++ } else if (typeof input === 'string') { ++ configFromString(config); ++ } else if (isArray(input)) { ++ config._a = map(input.slice(0), function (obj) { ++ return parseInt(obj, 10); ++ }); ++ configFromArray(config); ++ } else if (isObject(input)) { ++ configFromObject(config); ++ } else if (isNumber(input)) { ++ // from milliseconds ++ config._d = new Date(input); ++ } else { ++ hooks.createFromInputFallback(config); ++ } ++ } ++ ++ function createLocalOrUTC(input, format, locale, strict, isUTC) { ++ var c = {}; ++ ++ if (format === true || format === false) { ++ strict = format; ++ format = undefined; ++ } ++ ++ if (locale === true || locale === false) { ++ strict = locale; ++ locale = undefined; ++ } ++ ++ if ( ++ (isObject(input) && isObjectEmpty(input)) || ++ (isArray(input) && input.length === 0) ++ ) { ++ input = undefined; ++ } ++ // object construction must be done this way. ++ // https://github.com/moment/moment/issues/1423 ++ c._isAMomentObject = true; ++ c._useUTC = c._isUTC = isUTC; ++ c._l = locale; ++ c._i = input; ++ c._f = format; ++ c._strict = strict; ++ ++ return createFromConfig(c); ++ } ++ ++ function createLocal(input, format, locale, strict) { ++ return createLocalOrUTC(input, format, locale, strict, false); ++ } ++ ++ var prototypeMin = deprecate( ++ 'moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/', ++ function () { ++ var other = createLocal.apply(null, arguments); ++ if (this.isValid() && other.isValid()) { ++ return other < this ? this : other; ++ } else { ++ return createInvalid(); ++ } ++ } ++ ), ++ prototypeMax = deprecate( ++ 'moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/', ++ function () { ++ var other = createLocal.apply(null, arguments); ++ if (this.isValid() && other.isValid()) { ++ return other > this ? this : other; ++ } else { ++ return createInvalid(); ++ } ++ } ++ ); ++ ++ // Pick a moment m from moments so that m[fn](other) is true for all ++ // other. This relies on the function fn to be transitive. ++ // ++ // moments should either be an array of moment objects or an array, whose ++ // first element is an array of moment objects. ++ function pickBy(fn, moments) { ++ var res, i; ++ if (moments.length === 1 && isArray(moments[0])) { ++ moments = moments[0]; ++ } ++ if (!moments.length) { ++ return createLocal(); ++ } ++ res = moments[0]; ++ for (i = 1; i < moments.length; ++i) { ++ if (!moments[i].isValid() || moments[i][fn](res)) { ++ res = moments[i]; ++ } ++ } ++ return res; ++ } ++ ++ // TODO: Use [].sort instead? ++ function min() { ++ var args = [].slice.call(arguments, 0); ++ ++ return pickBy('isBefore', args); ++ } ++ ++ function max() { ++ var args = [].slice.call(arguments, 0); ++ ++ return pickBy('isAfter', args); ++ } ++ ++ var now = function () { ++ return Date.now ? Date.now() : +new Date(); ++ }; ++ ++ var ordering = [ ++ 'year', ++ 'quarter', ++ 'month', ++ 'week', ++ 'day', ++ 'hour', ++ 'minute', ++ 'second', ++ 'millisecond', ++ ]; ++ ++ function isDurationValid(m) { ++ var key, ++ unitHasDecimal = false, ++ i; ++ for (key in m) { ++ if ( ++ hasOwnProp(m, key) && ++ !( ++ indexOf.call(ordering, key) !== -1 && ++ (m[key] == null || !isNaN(m[key])) ++ ) ++ ) { ++ return false; ++ } ++ } ++ ++ for (i = 0; i < ordering.length; ++i) { ++ if (m[ordering[i]]) { ++ if (unitHasDecimal) { ++ return false; // only allow non-integers for smallest unit ++ } ++ if (parseFloat(m[ordering[i]]) !== toInt(m[ordering[i]])) { ++ unitHasDecimal = true; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ function isValid$1() { ++ return this._isValid; ++ } ++ ++ function createInvalid$1() { ++ return createDuration(NaN); ++ } ++ ++ function Duration(duration) { ++ var normalizedInput = normalizeObjectUnits(duration), ++ years = normalizedInput.year || 0, ++ quarters = normalizedInput.quarter || 0, ++ months = normalizedInput.month || 0, ++ weeks = normalizedInput.week || normalizedInput.isoWeek || 0, ++ days = normalizedInput.day || 0, ++ hours = normalizedInput.hour || 0, ++ minutes = normalizedInput.minute || 0, ++ seconds = normalizedInput.second || 0, ++ milliseconds = normalizedInput.millisecond || 0; ++ ++ this._isValid = isDurationValid(normalizedInput); ++ ++ // representation for dateAddRemove ++ this._milliseconds = ++ +milliseconds + ++ seconds * 1e3 + // 1000 ++ minutes * 6e4 + // 1000 * 60 ++ hours * 1000 * 60 * 60; //using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978 ++ // Because of dateAddRemove treats 24 hours as different from a ++ // day when working around DST, we need to store them separately ++ this._days = +days + weeks * 7; ++ // It is impossible to translate months into days without knowing ++ // which months you are are talking about, so we have to store ++ // it separately. ++ this._months = +months + quarters * 3 + years * 12; ++ ++ this._data = {}; ++ ++ this._locale = getLocale(); ++ ++ this._bubble(); ++ } ++ ++ function isDuration(obj) { ++ return obj instanceof Duration; ++ } ++ ++ function absRound(number) { ++ if (number < 0) { ++ return Math.round(-1 * number) * -1; ++ } else { ++ return Math.round(number); ++ } ++ } ++ ++ // compare two arrays, return the number of differences ++ function compareArrays(array1, array2, dontConvert) { ++ var len = Math.min(array1.length, array2.length), ++ lengthDiff = Math.abs(array1.length - array2.length), ++ diffs = 0, ++ i; ++ for (i = 0; i < len; i++) { ++ if ( ++ (dontConvert && array1[i] !== array2[i]) || ++ (!dontConvert && toInt(array1[i]) !== toInt(array2[i])) ++ ) { ++ diffs++; ++ } ++ } ++ return diffs + lengthDiff; ++ } ++ ++ // FORMATTING ++ ++ function offset(token, separator) { ++ addFormatToken(token, 0, 0, function () { ++ var offset = this.utcOffset(), ++ sign = '+'; ++ if (offset < 0) { ++ offset = -offset; ++ sign = '-'; ++ } ++ return ( ++ sign + ++ zeroFill(~~(offset / 60), 2) + ++ separator + ++ zeroFill(~~offset % 60, 2) ++ ); ++ }); ++ } ++ ++ offset('Z', ':'); ++ offset('ZZ', ''); ++ ++ // PARSING ++ ++ addRegexToken('Z', matchShortOffset); ++ addRegexToken('ZZ', matchShortOffset); ++ addParseToken(['Z', 'ZZ'], function (input, array, config) { ++ config._useUTC = true; ++ config._tzm = offsetFromString(matchShortOffset, input); ++ }); ++ ++ // HELPERS ++ ++ // timezone chunker ++ // '+10:00' > ['10', '00'] ++ // '-1530' > ['-15', '30'] ++ var chunkOffset = /([\+\-]|\d\d)/gi; ++ ++ function offsetFromString(matcher, string) { ++ var matches = (string || '').match(matcher), ++ chunk, ++ parts, ++ minutes; ++ ++ if (matches === null) { ++ return null; ++ } ++ ++ chunk = matches[matches.length - 1] || []; ++ parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; ++ minutes = +(parts[1] * 60) + toInt(parts[2]); ++ ++ return minutes === 0 ? 0 : parts[0] === '+' ? minutes : -minutes; ++ } ++ ++ // Return a moment from input, that is local/utc/zone equivalent to model. ++ function cloneWithOffset(input, model) { ++ var res, diff; ++ if (model._isUTC) { ++ res = model.clone(); ++ diff = ++ (isMoment(input) || isDate(input) ++ ? input.valueOf() ++ : createLocal(input).valueOf()) - res.valueOf(); ++ // Use low-level api, because this fn is low-level api. ++ res._d.setTime(res._d.valueOf() + diff); ++ hooks.updateOffset(res, false); ++ return res; ++ } else { ++ return createLocal(input).local(); ++ } ++ } ++ ++ function getDateOffset(m) { ++ // On Firefox.24 Date#getTimezoneOffset returns a floating point. ++ // https://github.com/moment/moment/pull/1871 ++ return -Math.round(m._d.getTimezoneOffset()); ++ } ++ ++ // HOOKS ++ ++ // This function will be called whenever a moment is mutated. ++ // It is intended to keep the offset in sync with the timezone. ++ hooks.updateOffset = function () {}; ++ ++ // MOMENTS ++ ++ // keepLocalTime = true means only change the timezone, without ++ // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> ++ // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset ++ // +0200, so we adjust the time as needed, to be valid. ++ // ++ // Keeping the time actually adds/subtracts (one hour) ++ // from the actual represented time. That is why we call updateOffset ++ // a second time. In case it wants us to change the offset again ++ // _changeInProgress == true case, then we have to adjust, because ++ // there is no such time in the given timezone. ++ function getSetOffset(input, keepLocalTime, keepMinutes) { ++ var offset = this._offset || 0, ++ localAdjust; ++ if (!this.isValid()) { ++ return input != null ? this : NaN; ++ } ++ if (input != null) { ++ if (typeof input === 'string') { ++ input = offsetFromString(matchShortOffset, input); ++ if (input === null) { ++ return this; ++ } ++ } else if (Math.abs(input) < 16 && !keepMinutes) { ++ input = input * 60; ++ } ++ if (!this._isUTC && keepLocalTime) { ++ localAdjust = getDateOffset(this); ++ } ++ this._offset = input; ++ this._isUTC = true; ++ if (localAdjust != null) { ++ this.add(localAdjust, 'm'); ++ } ++ if (offset !== input) { ++ if (!keepLocalTime || this._changeInProgress) { ++ addSubtract( ++ this, ++ createDuration(input - offset, 'm'), ++ 1, ++ false ++ ); ++ } else if (!this._changeInProgress) { ++ this._changeInProgress = true; ++ hooks.updateOffset(this, true); ++ this._changeInProgress = null; ++ } ++ } ++ return this; ++ } else { ++ return this._isUTC ? offset : getDateOffset(this); ++ } ++ } ++ ++ function getSetZone(input, keepLocalTime) { ++ if (input != null) { ++ if (typeof input !== 'string') { ++ input = -input; ++ } ++ ++ this.utcOffset(input, keepLocalTime); ++ ++ return this; ++ } else { ++ return -this.utcOffset(); ++ } ++ } ++ ++ function setOffsetToUTC(keepLocalTime) { ++ return this.utcOffset(0, keepLocalTime); ++ } ++ ++ function setOffsetToLocal(keepLocalTime) { ++ if (this._isUTC) { ++ this.utcOffset(0, keepLocalTime); ++ this._isUTC = false; ++ ++ if (keepLocalTime) { ++ this.subtract(getDateOffset(this), 'm'); ++ } ++ } ++ return this; ++ } ++ ++ function setOffsetToParsedOffset() { ++ if (this._tzm != null) { ++ this.utcOffset(this._tzm, false, true); ++ } else if (typeof this._i === 'string') { ++ var tZone = offsetFromString(matchOffset, this._i); ++ if (tZone != null) { ++ this.utcOffset(tZone); ++ } else { ++ this.utcOffset(0, true); ++ } ++ } ++ return this; ++ } ++ ++ function hasAlignedHourOffset(input) { ++ if (!this.isValid()) { ++ return false; ++ } ++ input = input ? createLocal(input).utcOffset() : 0; ++ ++ return (this.utcOffset() - input) % 60 === 0; ++ } ++ ++ function isDaylightSavingTime() { ++ return ( ++ this.utcOffset() > this.clone().month(0).utcOffset() || ++ this.utcOffset() > this.clone().month(5).utcOffset() ++ ); ++ } ++ ++ function isDaylightSavingTimeShifted() { ++ if (!isUndefined(this._isDSTShifted)) { ++ return this._isDSTShifted; ++ } ++ ++ var c = {}, ++ other; ++ ++ copyConfig(c, this); ++ c = prepareConfig(c); ++ ++ if (c._a) { ++ other = c._isUTC ? createUTC(c._a) : createLocal(c._a); ++ this._isDSTShifted = ++ this.isValid() && compareArrays(c._a, other.toArray()) > 0; ++ } else { ++ this._isDSTShifted = false; ++ } ++ ++ return this._isDSTShifted; ++ } ++ ++ function isLocal() { ++ return this.isValid() ? !this._isUTC : false; ++ } ++ ++ function isUtcOffset() { ++ return this.isValid() ? this._isUTC : false; ++ } ++ ++ function isUtc() { ++ return this.isValid() ? this._isUTC && this._offset === 0 : false; ++ } ++ ++ // ASP.NET json date format regex ++ var aspNetRegex = /^(-|\+)?(?:(\d*)[. ])?(\d+):(\d+)(?::(\d+)(\.\d*)?)?$/, ++ // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html ++ // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere ++ // and further modified to allow for strings containing both week and day ++ isoRegex = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/; ++ ++ function createDuration(input, key) { ++ var duration = input, ++ // matching against regexp is expensive, do it on demand ++ match = null, ++ sign, ++ ret, ++ diffRes; ++ ++ if (isDuration(input)) { ++ duration = { ++ ms: input._milliseconds, ++ d: input._days, ++ M: input._months, ++ }; ++ } else if (isNumber(input) || !isNaN(+input)) { ++ duration = {}; ++ if (key) { ++ duration[key] = +input; ++ } else { ++ duration.milliseconds = +input; ++ } ++ } else if ((match = aspNetRegex.exec(input))) { ++ sign = match[1] === '-' ? -1 : 1; ++ duration = { ++ y: 0, ++ d: toInt(match[DATE]) * sign, ++ h: toInt(match[HOUR]) * sign, ++ m: toInt(match[MINUTE]) * sign, ++ s: toInt(match[SECOND]) * sign, ++ ms: toInt(absRound(match[MILLISECOND] * 1000)) * sign, // the millisecond decimal point is included in the match ++ }; ++ } else if ((match = isoRegex.exec(input))) { ++ sign = match[1] === '-' ? -1 : 1; ++ duration = { ++ y: parseIso(match[2], sign), ++ M: parseIso(match[3], sign), ++ w: parseIso(match[4], sign), ++ d: parseIso(match[5], sign), ++ h: parseIso(match[6], sign), ++ m: parseIso(match[7], sign), ++ s: parseIso(match[8], sign), ++ }; ++ } else if (duration == null) { ++ // checks for null or undefined ++ duration = {}; ++ } else if ( ++ typeof duration === 'object' && ++ ('from' in duration || 'to' in duration) ++ ) { ++ diffRes = momentsDifference( ++ createLocal(duration.from), ++ createLocal(duration.to) ++ ); ++ ++ duration = {}; ++ duration.ms = diffRes.milliseconds; ++ duration.M = diffRes.months; ++ } ++ ++ ret = new Duration(duration); ++ ++ if (isDuration(input) && hasOwnProp(input, '_locale')) { ++ ret._locale = input._locale; ++ } ++ ++ if (isDuration(input) && hasOwnProp(input, '_isValid')) { ++ ret._isValid = input._isValid; ++ } ++ ++ return ret; ++ } ++ ++ createDuration.fn = Duration.prototype; ++ createDuration.invalid = createInvalid$1; ++ ++ function parseIso(inp, sign) { ++ // We'd normally use ~~inp for this, but unfortunately it also ++ // converts floats to ints. ++ // inp may be undefined, so careful calling replace on it. ++ var res = inp && parseFloat(inp.replace(',', '.')); ++ // apply sign while we're at it ++ return (isNaN(res) ? 0 : res) * sign; ++ } ++ ++ function positiveMomentsDifference(base, other) { ++ var res = {}; ++ ++ res.months = ++ other.month() - base.month() + (other.year() - base.year()) * 12; ++ if (base.clone().add(res.months, 'M').isAfter(other)) { ++ --res.months; ++ } ++ ++ res.milliseconds = +other - +base.clone().add(res.months, 'M'); ++ ++ return res; ++ } ++ ++ function momentsDifference(base, other) { ++ var res; ++ if (!(base.isValid() && other.isValid())) { ++ return { milliseconds: 0, months: 0 }; ++ } ++ ++ other = cloneWithOffset(other, base); ++ if (base.isBefore(other)) { ++ res = positiveMomentsDifference(base, other); ++ } else { ++ res = positiveMomentsDifference(other, base); ++ res.milliseconds = -res.milliseconds; ++ res.months = -res.months; ++ } ++ ++ return res; ++ } ++ ++ // TODO: remove 'name' arg after deprecation is removed ++ function createAdder(direction, name) { ++ return function (val, period) { ++ var dur, tmp; ++ //invert the arguments, but complain about it ++ if (period !== null && !isNaN(+period)) { ++ deprecateSimple( ++ name, ++ 'moment().' + ++ name + ++ '(period, number) is deprecated. Please use moment().' + ++ name + ++ '(number, period). ' + ++ 'See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info.' ++ ); ++ tmp = val; ++ val = period; ++ period = tmp; ++ } ++ ++ dur = createDuration(val, period); ++ addSubtract(this, dur, direction); ++ return this; ++ }; ++ } ++ ++ function addSubtract(mom, duration, isAdding, updateOffset) { ++ var milliseconds = duration._milliseconds, ++ days = absRound(duration._days), ++ months = absRound(duration._months); ++ ++ if (!mom.isValid()) { ++ // No op ++ return; ++ } ++ ++ updateOffset = updateOffset == null ? true : updateOffset; ++ ++ if (months) { ++ setMonth(mom, get(mom, 'Month') + months * isAdding); ++ } ++ if (days) { ++ set$1(mom, 'Date', get(mom, 'Date') + days * isAdding); ++ } ++ if (milliseconds) { ++ mom._d.setTime(mom._d.valueOf() + milliseconds * isAdding); ++ } ++ if (updateOffset) { ++ hooks.updateOffset(mom, days || months); ++ } ++ } ++ ++ var add = createAdder(1, 'add'), ++ subtract = createAdder(-1, 'subtract'); ++ ++ function isString(input) { ++ return typeof input === 'string' || input instanceof String; ++ } ++ ++ // type MomentInput = Moment | Date | string | number | (number | string)[] | MomentInputObject | void; // null | undefined ++ function isMomentInput(input) { ++ return ( ++ isMoment(input) || ++ isDate(input) || ++ isString(input) || ++ isNumber(input) || ++ isNumberOrStringArray(input) || ++ isMomentInputObject(input) || ++ input === null || ++ input === undefined ++ ); ++ } ++ ++ function isMomentInputObject(input) { ++ var objectTest = isObject(input) && !isObjectEmpty(input), ++ propertyTest = false, ++ properties = [ ++ 'years', ++ 'year', ++ 'y', ++ 'months', ++ 'month', ++ 'M', ++ 'days', ++ 'day', ++ 'd', ++ 'dates', ++ 'date', ++ 'D', ++ 'hours', ++ 'hour', ++ 'h', ++ 'minutes', ++ 'minute', ++ 'm', ++ 'seconds', ++ 'second', ++ 's', ++ 'milliseconds', ++ 'millisecond', ++ 'ms', ++ ], ++ i, ++ property; ++ ++ for (i = 0; i < properties.length; i += 1) { ++ property = properties[i]; ++ propertyTest = propertyTest || hasOwnProp(input, property); ++ } ++ ++ return objectTest && propertyTest; ++ } ++ ++ function isNumberOrStringArray(input) { ++ var arrayTest = isArray(input), ++ dataTypeTest = false; ++ if (arrayTest) { ++ dataTypeTest = ++ input.filter(function (item) { ++ return !isNumber(item) && isString(input); ++ }).length === 0; ++ } ++ return arrayTest && dataTypeTest; ++ } ++ ++ function isCalendarSpec(input) { ++ var objectTest = isObject(input) && !isObjectEmpty(input), ++ propertyTest = false, ++ properties = [ ++ 'sameDay', ++ 'nextDay', ++ 'lastDay', ++ 'nextWeek', ++ 'lastWeek', ++ 'sameElse', ++ ], ++ i, ++ property; ++ ++ for (i = 0; i < properties.length; i += 1) { ++ property = properties[i]; ++ propertyTest = propertyTest || hasOwnProp(input, property); ++ } ++ ++ return objectTest && propertyTest; ++ } ++ ++ function getCalendarFormat(myMoment, now) { ++ var diff = myMoment.diff(now, 'days', true); ++ return diff < -6 ++ ? 'sameElse' ++ : diff < -1 ++ ? 'lastWeek' ++ : diff < 0 ++ ? 'lastDay' ++ : diff < 1 ++ ? 'sameDay' ++ : diff < 2 ++ ? 'nextDay' ++ : diff < 7 ++ ? 'nextWeek' ++ : 'sameElse'; ++ } ++ ++ function calendar$1(time, formats) { ++ // Support for single parameter, formats only overload to the calendar function ++ if (arguments.length === 1) { ++ if (!arguments[0]) { ++ time = undefined; ++ formats = undefined; ++ } else if (isMomentInput(arguments[0])) { ++ time = arguments[0]; ++ formats = undefined; ++ } else if (isCalendarSpec(arguments[0])) { ++ formats = arguments[0]; ++ time = undefined; ++ } ++ } ++ // We want to compare the start of today, vs this. ++ // Getting start-of-today depends on whether we're local/utc/offset or not. ++ var now = time || createLocal(), ++ sod = cloneWithOffset(now, this).startOf('day'), ++ format = hooks.calendarFormat(this, sod) || 'sameElse', ++ output = ++ formats && ++ (isFunction(formats[format]) ++ ? formats[format].call(this, now) ++ : formats[format]); ++ ++ return this.format( ++ output || this.localeData().calendar(format, this, createLocal(now)) ++ ); ++ } ++ ++ function clone() { ++ return new Moment(this); ++ } ++ ++ function isAfter(input, units) { ++ var localInput = isMoment(input) ? input : createLocal(input); ++ if (!(this.isValid() && localInput.isValid())) { ++ return false; ++ } ++ units = normalizeUnits(units) || 'millisecond'; ++ if (units === 'millisecond') { ++ return this.valueOf() > localInput.valueOf(); ++ } else { ++ return localInput.valueOf() < this.clone().startOf(units).valueOf(); ++ } ++ } ++ ++ function isBefore(input, units) { ++ var localInput = isMoment(input) ? input : createLocal(input); ++ if (!(this.isValid() && localInput.isValid())) { ++ return false; ++ } ++ units = normalizeUnits(units) || 'millisecond'; ++ if (units === 'millisecond') { ++ return this.valueOf() < localInput.valueOf(); ++ } else { ++ return this.clone().endOf(units).valueOf() < localInput.valueOf(); ++ } ++ } ++ ++ function isBetween(from, to, units, inclusivity) { ++ var localFrom = isMoment(from) ? from : createLocal(from), ++ localTo = isMoment(to) ? to : createLocal(to); ++ if (!(this.isValid() && localFrom.isValid() && localTo.isValid())) { ++ return false; ++ } ++ inclusivity = inclusivity || '()'; ++ return ( ++ (inclusivity[0] === '(' ++ ? this.isAfter(localFrom, units) ++ : !this.isBefore(localFrom, units)) && ++ (inclusivity[1] === ')' ++ ? this.isBefore(localTo, units) ++ : !this.isAfter(localTo, units)) ++ ); ++ } ++ ++ function isSame(input, units) { ++ var localInput = isMoment(input) ? input : createLocal(input), ++ inputMs; ++ if (!(this.isValid() && localInput.isValid())) { ++ return false; ++ } ++ units = normalizeUnits(units) || 'millisecond'; ++ if (units === 'millisecond') { ++ return this.valueOf() === localInput.valueOf(); ++ } else { ++ inputMs = localInput.valueOf(); ++ return ( ++ this.clone().startOf(units).valueOf() <= inputMs && ++ inputMs <= this.clone().endOf(units).valueOf() ++ ); ++ } ++ } ++ ++ function isSameOrAfter(input, units) { ++ return this.isSame(input, units) || this.isAfter(input, units); ++ } ++ ++ function isSameOrBefore(input, units) { ++ return this.isSame(input, units) || this.isBefore(input, units); ++ } ++ ++ function diff(input, units, asFloat) { ++ var that, zoneDelta, output; ++ ++ if (!this.isValid()) { ++ return NaN; ++ } ++ ++ that = cloneWithOffset(input, this); ++ ++ if (!that.isValid()) { ++ return NaN; ++ } ++ ++ zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4; ++ ++ units = normalizeUnits(units); ++ ++ switch (units) { ++ case 'year': ++ output = monthDiff(this, that) / 12; ++ break; ++ case 'month': ++ output = monthDiff(this, that); ++ break; ++ case 'quarter': ++ output = monthDiff(this, that) / 3; ++ break; ++ case 'second': ++ output = (this - that) / 1e3; ++ break; // 1000 ++ case 'minute': ++ output = (this - that) / 6e4; ++ break; // 1000 * 60 ++ case 'hour': ++ output = (this - that) / 36e5; ++ break; // 1000 * 60 * 60 ++ case 'day': ++ output = (this - that - zoneDelta) / 864e5; ++ break; // 1000 * 60 * 60 * 24, negate dst ++ case 'week': ++ output = (this - that - zoneDelta) / 6048e5; ++ break; // 1000 * 60 * 60 * 24 * 7, negate dst ++ default: ++ output = this - that; ++ } ++ ++ return asFloat ? output : absFloor(output); ++ } ++ ++ function monthDiff(a, b) { ++ if (a.date() < b.date()) { ++ // end-of-month calculations work correct when the start month has more ++ // days than the end month. ++ return -monthDiff(b, a); ++ } ++ // difference in months ++ var wholeMonthDiff = (b.year() - a.year()) * 12 + (b.month() - a.month()), ++ // b is in (anchor - 1 month, anchor + 1 month) ++ anchor = a.clone().add(wholeMonthDiff, 'months'), ++ anchor2, ++ adjust; ++ ++ if (b - anchor < 0) { ++ anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); ++ // linear across the month ++ adjust = (b - anchor) / (anchor - anchor2); ++ } else { ++ anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); ++ // linear across the month ++ adjust = (b - anchor) / (anchor2 - anchor); ++ } ++ ++ //check for negative zero, return zero if negative zero ++ return -(wholeMonthDiff + adjust) || 0; ++ } ++ ++ hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; ++ hooks.defaultFormatUtc = 'YYYY-MM-DDTHH:mm:ss[Z]'; ++ ++ function toString() { ++ return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); ++ } ++ ++ function toISOString(keepOffset) { ++ if (!this.isValid()) { ++ return null; ++ } ++ var utc = keepOffset !== true, ++ m = utc ? this.clone().utc() : this; ++ if (m.year() < 0 || m.year() > 9999) { ++ return formatMoment( ++ m, ++ utc ++ ? 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]' ++ : 'YYYYYY-MM-DD[T]HH:mm:ss.SSSZ' ++ ); ++ } ++ if (isFunction(Date.prototype.toISOString)) { ++ // native implementation is ~50x faster, use it when we can ++ if (utc) { ++ return this.toDate().toISOString(); ++ } else { ++ return new Date(this.valueOf() + this.utcOffset() * 60 * 1000) ++ .toISOString() ++ .replace('Z', formatMoment(m, 'Z')); ++ } ++ } ++ return formatMoment( ++ m, ++ utc ? 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]' : 'YYYY-MM-DD[T]HH:mm:ss.SSSZ' ++ ); ++ } ++ ++ /** ++ * Return a human readable representation of a moment that can ++ * also be evaluated to get a new moment which is the same ++ * ++ * @link https://nodejs.org/dist/latest/docs/api/util.html#util_custom_inspect_function_on_objects ++ */ ++ function inspect() { ++ if (!this.isValid()) { ++ return 'moment.invalid(/* ' + this._i + ' */)'; ++ } ++ var func = 'moment', ++ zone = '', ++ prefix, ++ year, ++ datetime, ++ suffix; ++ if (!this.isLocal()) { ++ func = this.utcOffset() === 0 ? 'moment.utc' : 'moment.parseZone'; ++ zone = 'Z'; ++ } ++ prefix = '[' + func + '("]'; ++ year = 0 <= this.year() && this.year() <= 9999 ? 'YYYY' : 'YYYYYY'; ++ datetime = '-MM-DD[T]HH:mm:ss.SSS'; ++ suffix = zone + '[")]'; ++ ++ return this.format(prefix + year + datetime + suffix); ++ } ++ ++ function format(inputString) { ++ if (!inputString) { ++ inputString = this.isUtc() ++ ? hooks.defaultFormatUtc ++ : hooks.defaultFormat; ++ } ++ var output = formatMoment(this, inputString); ++ return this.localeData().postformat(output); ++ } ++ ++ function from(time, withoutSuffix) { ++ if ( ++ this.isValid() && ++ ((isMoment(time) && time.isValid()) || createLocal(time).isValid()) ++ ) { ++ return createDuration({ to: this, from: time }) ++ .locale(this.locale()) ++ .humanize(!withoutSuffix); ++ } else { ++ return this.localeData().invalidDate(); ++ } ++ } ++ ++ function fromNow(withoutSuffix) { ++ return this.from(createLocal(), withoutSuffix); ++ } ++ ++ function to(time, withoutSuffix) { ++ if ( ++ this.isValid() && ++ ((isMoment(time) && time.isValid()) || createLocal(time).isValid()) ++ ) { ++ return createDuration({ from: this, to: time }) ++ .locale(this.locale()) ++ .humanize(!withoutSuffix); ++ } else { ++ return this.localeData().invalidDate(); ++ } ++ } ++ ++ function toNow(withoutSuffix) { ++ return this.to(createLocal(), withoutSuffix); ++ } ++ ++ // If passed a locale key, it will set the locale for this ++ // instance. Otherwise, it will return the locale configuration ++ // variables for this instance. ++ function locale(key) { ++ var newLocaleData; ++ ++ if (key === undefined) { ++ return this._locale._abbr; ++ } else { ++ newLocaleData = getLocale(key); ++ if (newLocaleData != null) { ++ this._locale = newLocaleData; ++ } ++ return this; ++ } ++ } ++ ++ var lang = deprecate( ++ 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', ++ function (key) { ++ if (key === undefined) { ++ return this.localeData(); ++ } else { ++ return this.locale(key); ++ } ++ } ++ ); ++ ++ function localeData() { ++ return this._locale; ++ } ++ ++ var MS_PER_SECOND = 1000, ++ MS_PER_MINUTE = 60 * MS_PER_SECOND, ++ MS_PER_HOUR = 60 * MS_PER_MINUTE, ++ MS_PER_400_YEARS = (365 * 400 + 97) * 24 * MS_PER_HOUR; ++ ++ // actual modulo - handles negative numbers (for dates before 1970): ++ function mod$1(dividend, divisor) { ++ return ((dividend % divisor) + divisor) % divisor; ++ } ++ ++ function localStartOfDate(y, m, d) { ++ // the date constructor remaps years 0-99 to 1900-1999 ++ if (y < 100 && y >= 0) { ++ // preserve leap years using a full 400 year cycle, then reset ++ return new Date(y + 400, m, d) - MS_PER_400_YEARS; ++ } else { ++ return new Date(y, m, d).valueOf(); ++ } ++ } ++ ++ function utcStartOfDate(y, m, d) { ++ // Date.UTC remaps years 0-99 to 1900-1999 ++ if (y < 100 && y >= 0) { ++ // preserve leap years using a full 400 year cycle, then reset ++ return Date.UTC(y + 400, m, d) - MS_PER_400_YEARS; ++ } else { ++ return Date.UTC(y, m, d); ++ } ++ } ++ ++ function startOf(units) { ++ var time, startOfDate; ++ units = normalizeUnits(units); ++ if (units === undefined || units === 'millisecond' || !this.isValid()) { ++ return this; ++ } ++ ++ startOfDate = this._isUTC ? utcStartOfDate : localStartOfDate; ++ ++ switch (units) { ++ case 'year': ++ time = startOfDate(this.year(), 0, 1); ++ break; ++ case 'quarter': ++ time = startOfDate( ++ this.year(), ++ this.month() - (this.month() % 3), ++ 1 ++ ); ++ break; ++ case 'month': ++ time = startOfDate(this.year(), this.month(), 1); ++ break; ++ case 'week': ++ time = startOfDate( ++ this.year(), ++ this.month(), ++ this.date() - this.weekday() ++ ); ++ break; ++ case 'isoWeek': ++ time = startOfDate( ++ this.year(), ++ this.month(), ++ this.date() - (this.isoWeekday() - 1) ++ ); ++ break; ++ case 'day': ++ case 'date': ++ time = startOfDate(this.year(), this.month(), this.date()); ++ break; ++ case 'hour': ++ time = this._d.valueOf(); ++ time -= mod$1( ++ time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), ++ MS_PER_HOUR ++ ); ++ break; ++ case 'minute': ++ time = this._d.valueOf(); ++ time -= mod$1(time, MS_PER_MINUTE); ++ break; ++ case 'second': ++ time = this._d.valueOf(); ++ time -= mod$1(time, MS_PER_SECOND); ++ break; ++ } ++ ++ this._d.setTime(time); ++ hooks.updateOffset(this, true); ++ return this; ++ } ++ ++ function endOf(units) { ++ var time, startOfDate; ++ units = normalizeUnits(units); ++ if (units === undefined || units === 'millisecond' || !this.isValid()) { ++ return this; ++ } ++ ++ startOfDate = this._isUTC ? utcStartOfDate : localStartOfDate; ++ ++ switch (units) { ++ case 'year': ++ time = startOfDate(this.year() + 1, 0, 1) - 1; ++ break; ++ case 'quarter': ++ time = ++ startOfDate( ++ this.year(), ++ this.month() - (this.month() % 3) + 3, ++ 1 ++ ) - 1; ++ break; ++ case 'month': ++ time = startOfDate(this.year(), this.month() + 1, 1) - 1; ++ break; ++ case 'week': ++ time = ++ startOfDate( ++ this.year(), ++ this.month(), ++ this.date() - this.weekday() + 7 ++ ) - 1; ++ break; ++ case 'isoWeek': ++ time = ++ startOfDate( ++ this.year(), ++ this.month(), ++ this.date() - (this.isoWeekday() - 1) + 7 ++ ) - 1; ++ break; ++ case 'day': ++ case 'date': ++ time = startOfDate(this.year(), this.month(), this.date() + 1) - 1; ++ break; ++ case 'hour': ++ time = this._d.valueOf(); ++ time += ++ MS_PER_HOUR - ++ mod$1( ++ time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), ++ MS_PER_HOUR ++ ) - ++ 1; ++ break; ++ case 'minute': ++ time = this._d.valueOf(); ++ time += MS_PER_MINUTE - mod$1(time, MS_PER_MINUTE) - 1; ++ break; ++ case 'second': ++ time = this._d.valueOf(); ++ time += MS_PER_SECOND - mod$1(time, MS_PER_SECOND) - 1; ++ break; ++ } ++ ++ this._d.setTime(time); ++ hooks.updateOffset(this, true); ++ return this; ++ } ++ ++ function valueOf() { ++ return this._d.valueOf() - (this._offset || 0) * 60000; ++ } ++ ++ function unix() { ++ return Math.floor(this.valueOf() / 1000); ++ } ++ ++ function toDate() { ++ return new Date(this.valueOf()); ++ } ++ ++ function toArray() { ++ var m = this; ++ return [ ++ m.year(), ++ m.month(), ++ m.date(), ++ m.hour(), ++ m.minute(), ++ m.second(), ++ m.millisecond(), ++ ]; ++ } ++ ++ function toObject() { ++ var m = this; ++ return { ++ years: m.year(), ++ months: m.month(), ++ date: m.date(), ++ hours: m.hours(), ++ minutes: m.minutes(), ++ seconds: m.seconds(), ++ milliseconds: m.milliseconds(), ++ }; ++ } ++ ++ function toJSON() { ++ // new Date(NaN).toJSON() === null ++ return this.isValid() ? this.toISOString() : null; ++ } ++ ++ function isValid$2() { ++ return isValid(this); ++ } ++ ++ function parsingFlags() { ++ return extend({}, getParsingFlags(this)); ++ } ++ ++ function invalidAt() { ++ return getParsingFlags(this).overflow; ++ } ++ ++ function creationData() { ++ return { ++ input: this._i, ++ format: this._f, ++ locale: this._locale, ++ isUTC: this._isUTC, ++ strict: this._strict, ++ }; ++ } ++ ++ addFormatToken('N', 0, 0, 'eraAbbr'); ++ addFormatToken('NN', 0, 0, 'eraAbbr'); ++ addFormatToken('NNN', 0, 0, 'eraAbbr'); ++ addFormatToken('NNNN', 0, 0, 'eraName'); ++ addFormatToken('NNNNN', 0, 0, 'eraNarrow'); ++ ++ addFormatToken('y', ['y', 1], 'yo', 'eraYear'); ++ addFormatToken('y', ['yy', 2], 0, 'eraYear'); ++ addFormatToken('y', ['yyy', 3], 0, 'eraYear'); ++ addFormatToken('y', ['yyyy', 4], 0, 'eraYear'); ++ ++ addRegexToken('N', matchEraAbbr); ++ addRegexToken('NN', matchEraAbbr); ++ addRegexToken('NNN', matchEraAbbr); ++ addRegexToken('NNNN', matchEraName); ++ addRegexToken('NNNNN', matchEraNarrow); ++ ++ addParseToken(['N', 'NN', 'NNN', 'NNNN', 'NNNNN'], function ( ++ input, ++ array, ++ config, ++ token ++ ) { ++ var era = config._locale.erasParse(input, token, config._strict); ++ if (era) { ++ getParsingFlags(config).era = era; ++ } else { ++ getParsingFlags(config).invalidEra = input; ++ } ++ }); ++ ++ addRegexToken('y', matchUnsigned); ++ addRegexToken('yy', matchUnsigned); ++ addRegexToken('yyy', matchUnsigned); ++ addRegexToken('yyyy', matchUnsigned); ++ addRegexToken('yo', matchEraYearOrdinal); ++ ++ addParseToken(['y', 'yy', 'yyy', 'yyyy'], YEAR); ++ addParseToken(['yo'], function (input, array, config, token) { ++ var match; ++ if (config._locale._eraYearOrdinalRegex) { ++ match = input.match(config._locale._eraYearOrdinalRegex); ++ } ++ ++ if (config._locale.eraYearOrdinalParse) { ++ array[YEAR] = config._locale.eraYearOrdinalParse(input, match); ++ } else { ++ array[YEAR] = parseInt(input, 10); ++ } ++ }); ++ ++ function localeEras(m, format) { ++ var i, ++ l, ++ date, ++ eras = this._eras || getLocale('en')._eras; ++ for (i = 0, l = eras.length; i < l; ++i) { ++ switch (typeof eras[i].since) { ++ case 'string': ++ // truncate time ++ date = hooks(eras[i].since).startOf('day'); ++ eras[i].since = date.valueOf(); ++ break; ++ } ++ ++ switch (typeof eras[i].until) { ++ case 'undefined': ++ eras[i].until = +Infinity; ++ break; ++ case 'string': ++ // truncate time ++ date = hooks(eras[i].until).startOf('day').valueOf(); ++ eras[i].until = date.valueOf(); ++ break; ++ } ++ } ++ return eras; ++ } ++ ++ function localeErasParse(eraName, format, strict) { ++ var i, ++ l, ++ eras = this.eras(), ++ name, ++ abbr, ++ narrow; ++ eraName = eraName.toUpperCase(); ++ ++ for (i = 0, l = eras.length; i < l; ++i) { ++ name = eras[i].name.toUpperCase(); ++ abbr = eras[i].abbr.toUpperCase(); ++ narrow = eras[i].narrow.toUpperCase(); ++ ++ if (strict) { ++ switch (format) { ++ case 'N': ++ case 'NN': ++ case 'NNN': ++ if (abbr === eraName) { ++ return eras[i]; ++ } ++ break; ++ ++ case 'NNNN': ++ if (name === eraName) { ++ return eras[i]; ++ } ++ break; ++ ++ case 'NNNNN': ++ if (narrow === eraName) { ++ return eras[i]; ++ } ++ break; ++ } ++ } else if ([name, abbr, narrow].indexOf(eraName) >= 0) { ++ return eras[i]; ++ } ++ } ++ } ++ ++ function localeErasConvertYear(era, year) { ++ var dir = era.since <= era.until ? +1 : -1; ++ if (year === undefined) { ++ return hooks(era.since).year(); ++ } else { ++ return hooks(era.since).year() + (year - era.offset) * dir; ++ } ++ } ++ ++ function getEraName() { ++ var i, ++ l, ++ val, ++ eras = this.localeData().eras(); ++ for (i = 0, l = eras.length; i < l; ++i) { ++ // truncate time ++ val = this.clone().startOf('day').valueOf(); ++ ++ if (eras[i].since <= val && val <= eras[i].until) { ++ return eras[i].name; ++ } ++ if (eras[i].until <= val && val <= eras[i].since) { ++ return eras[i].name; ++ } ++ } ++ ++ return ''; ++ } ++ ++ function getEraNarrow() { ++ var i, ++ l, ++ val, ++ eras = this.localeData().eras(); ++ for (i = 0, l = eras.length; i < l; ++i) { ++ // truncate time ++ val = this.clone().startOf('day').valueOf(); ++ ++ if (eras[i].since <= val && val <= eras[i].until) { ++ return eras[i].narrow; ++ } ++ if (eras[i].until <= val && val <= eras[i].since) { ++ return eras[i].narrow; ++ } ++ } ++ ++ return ''; ++ } ++ ++ function getEraAbbr() { ++ var i, ++ l, ++ val, ++ eras = this.localeData().eras(); ++ for (i = 0, l = eras.length; i < l; ++i) { ++ // truncate time ++ val = this.clone().startOf('day').valueOf(); ++ ++ if (eras[i].since <= val && val <= eras[i].until) { ++ return eras[i].abbr; ++ } ++ if (eras[i].until <= val && val <= eras[i].since) { ++ return eras[i].abbr; ++ } ++ } ++ ++ return ''; ++ } ++ ++ function getEraYear() { ++ var i, ++ l, ++ dir, ++ val, ++ eras = this.localeData().eras(); ++ for (i = 0, l = eras.length; i < l; ++i) { ++ dir = eras[i].since <= eras[i].until ? +1 : -1; ++ ++ // truncate time ++ val = this.clone().startOf('day').valueOf(); ++ ++ if ( ++ (eras[i].since <= val && val <= eras[i].until) || ++ (eras[i].until <= val && val <= eras[i].since) ++ ) { ++ return ( ++ (this.year() - hooks(eras[i].since).year()) * dir + ++ eras[i].offset ++ ); ++ } ++ } ++ ++ return this.year(); ++ } ++ ++ function erasNameRegex(isStrict) { ++ if (!hasOwnProp(this, '_erasNameRegex')) { ++ computeErasParse.call(this); ++ } ++ return isStrict ? this._erasNameRegex : this._erasRegex; ++ } ++ ++ function erasAbbrRegex(isStrict) { ++ if (!hasOwnProp(this, '_erasAbbrRegex')) { ++ computeErasParse.call(this); ++ } ++ return isStrict ? this._erasAbbrRegex : this._erasRegex; ++ } ++ ++ function erasNarrowRegex(isStrict) { ++ if (!hasOwnProp(this, '_erasNarrowRegex')) { ++ computeErasParse.call(this); ++ } ++ return isStrict ? this._erasNarrowRegex : this._erasRegex; ++ } ++ ++ function matchEraAbbr(isStrict, locale) { ++ return locale.erasAbbrRegex(isStrict); ++ } ++ ++ function matchEraName(isStrict, locale) { ++ return locale.erasNameRegex(isStrict); ++ } ++ ++ function matchEraNarrow(isStrict, locale) { ++ return locale.erasNarrowRegex(isStrict); ++ } ++ ++ function matchEraYearOrdinal(isStrict, locale) { ++ return locale._eraYearOrdinalRegex || matchUnsigned; ++ } ++ ++ function computeErasParse() { ++ var abbrPieces = [], ++ namePieces = [], ++ narrowPieces = [], ++ mixedPieces = [], ++ i, ++ l, ++ eras = this.eras(); ++ ++ for (i = 0, l = eras.length; i < l; ++i) { ++ namePieces.push(regexEscape(eras[i].name)); ++ abbrPieces.push(regexEscape(eras[i].abbr)); ++ narrowPieces.push(regexEscape(eras[i].narrow)); ++ ++ mixedPieces.push(regexEscape(eras[i].name)); ++ mixedPieces.push(regexEscape(eras[i].abbr)); ++ mixedPieces.push(regexEscape(eras[i].narrow)); ++ } ++ ++ this._erasRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); ++ this._erasNameRegex = new RegExp('^(' + namePieces.join('|') + ')', 'i'); ++ this._erasAbbrRegex = new RegExp('^(' + abbrPieces.join('|') + ')', 'i'); ++ this._erasNarrowRegex = new RegExp( ++ '^(' + narrowPieces.join('|') + ')', ++ 'i' ++ ); ++ } ++ ++ // FORMATTING ++ ++ addFormatToken(0, ['gg', 2], 0, function () { ++ return this.weekYear() % 100; ++ }); ++ ++ addFormatToken(0, ['GG', 2], 0, function () { ++ return this.isoWeekYear() % 100; ++ }); ++ ++ function addWeekYearFormatToken(token, getter) { ++ addFormatToken(0, [token, token.length], 0, getter); ++ } ++ ++ addWeekYearFormatToken('gggg', 'weekYear'); ++ addWeekYearFormatToken('ggggg', 'weekYear'); ++ addWeekYearFormatToken('GGGG', 'isoWeekYear'); ++ addWeekYearFormatToken('GGGGG', 'isoWeekYear'); ++ ++ // ALIASES ++ ++ addUnitAlias('weekYear', 'gg'); ++ addUnitAlias('isoWeekYear', 'GG'); ++ ++ // PRIORITY ++ ++ addUnitPriority('weekYear', 1); ++ addUnitPriority('isoWeekYear', 1); ++ ++ // PARSING ++ ++ addRegexToken('G', matchSigned); ++ addRegexToken('g', matchSigned); ++ addRegexToken('GG', match1to2, match2); ++ addRegexToken('gg', match1to2, match2); ++ addRegexToken('GGGG', match1to4, match4); ++ addRegexToken('gggg', match1to4, match4); ++ addRegexToken('GGGGG', match1to6, match6); ++ addRegexToken('ggggg', match1to6, match6); ++ ++ addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function ( ++ input, ++ week, ++ config, ++ token ++ ) { ++ week[token.substr(0, 2)] = toInt(input); ++ }); ++ ++ addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { ++ week[token] = hooks.parseTwoDigitYear(input); ++ }); ++ ++ // MOMENTS ++ ++ function getSetWeekYear(input) { ++ return getSetWeekYearHelper.call( ++ this, ++ input, ++ this.week(), ++ this.weekday(), ++ this.localeData()._week.dow, ++ this.localeData()._week.doy ++ ); ++ } ++ ++ function getSetISOWeekYear(input) { ++ return getSetWeekYearHelper.call( ++ this, ++ input, ++ this.isoWeek(), ++ this.isoWeekday(), ++ 1, ++ 4 ++ ); ++ } ++ ++ function getISOWeeksInYear() { ++ return weeksInYear(this.year(), 1, 4); ++ } ++ ++ function getISOWeeksInISOWeekYear() { ++ return weeksInYear(this.isoWeekYear(), 1, 4); ++ } ++ ++ function getWeeksInYear() { ++ var weekInfo = this.localeData()._week; ++ return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); ++ } ++ ++ function getWeeksInWeekYear() { ++ var weekInfo = this.localeData()._week; ++ return weeksInYear(this.weekYear(), weekInfo.dow, weekInfo.doy); ++ } ++ ++ function getSetWeekYearHelper(input, week, weekday, dow, doy) { ++ var weeksTarget; ++ if (input == null) { ++ return weekOfYear(this, dow, doy).year; ++ } else { ++ weeksTarget = weeksInYear(input, dow, doy); ++ if (week > weeksTarget) { ++ week = weeksTarget; ++ } ++ return setWeekAll.call(this, input, week, weekday, dow, doy); ++ } ++ } ++ ++ function setWeekAll(weekYear, week, weekday, dow, doy) { ++ var dayOfYearData = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy), ++ date = createUTCDate(dayOfYearData.year, 0, dayOfYearData.dayOfYear); ++ ++ this.year(date.getUTCFullYear()); ++ this.month(date.getUTCMonth()); ++ this.date(date.getUTCDate()); ++ return this; ++ } ++ ++ // FORMATTING ++ ++ addFormatToken('Q', 0, 'Qo', 'quarter'); ++ ++ // ALIASES ++ ++ addUnitAlias('quarter', 'Q'); ++ ++ // PRIORITY ++ ++ addUnitPriority('quarter', 7); ++ ++ // PARSING ++ ++ addRegexToken('Q', match1); ++ addParseToken('Q', function (input, array) { ++ array[MONTH] = (toInt(input) - 1) * 3; ++ }); ++ ++ // MOMENTS ++ ++ function getSetQuarter(input) { ++ return input == null ++ ? Math.ceil((this.month() + 1) / 3) ++ : this.month((input - 1) * 3 + (this.month() % 3)); ++ } ++ ++ // FORMATTING ++ ++ addFormatToken('D', ['DD', 2], 'Do', 'date'); ++ ++ // ALIASES ++ ++ addUnitAlias('date', 'D'); ++ ++ // PRIORITY ++ addUnitPriority('date', 9); ++ ++ // PARSING ++ ++ addRegexToken('D', match1to2); ++ addRegexToken('DD', match1to2, match2); ++ addRegexToken('Do', function (isStrict, locale) { ++ // TODO: Remove "ordinalParse" fallback in next major release. ++ return isStrict ++ ? locale._dayOfMonthOrdinalParse || locale._ordinalParse ++ : locale._dayOfMonthOrdinalParseLenient; ++ }); ++ ++ addParseToken(['D', 'DD'], DATE); ++ addParseToken('Do', function (input, array) { ++ array[DATE] = toInt(input.match(match1to2)[0]); ++ }); ++ ++ // MOMENTS ++ ++ var getSetDayOfMonth = makeGetSet('Date', true); ++ ++ // FORMATTING ++ ++ addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); ++ ++ // ALIASES ++ ++ addUnitAlias('dayOfYear', 'DDD'); ++ ++ // PRIORITY ++ addUnitPriority('dayOfYear', 4); ++ ++ // PARSING ++ ++ addRegexToken('DDD', match1to3); ++ addRegexToken('DDDD', match3); ++ addParseToken(['DDD', 'DDDD'], function (input, array, config) { ++ config._dayOfYear = toInt(input); ++ }); ++ ++ // HELPERS ++ ++ // MOMENTS ++ ++ function getSetDayOfYear(input) { ++ var dayOfYear = ++ Math.round( ++ (this.clone().startOf('day') - this.clone().startOf('year')) / 864e5 ++ ) + 1; ++ return input == null ? dayOfYear : this.add(input - dayOfYear, 'd'); ++ } ++ ++ // FORMATTING ++ ++ addFormatToken('m', ['mm', 2], 0, 'minute'); ++ ++ // ALIASES ++ ++ addUnitAlias('minute', 'm'); ++ ++ // PRIORITY ++ ++ addUnitPriority('minute', 14); ++ ++ // PARSING ++ ++ addRegexToken('m', match1to2); ++ addRegexToken('mm', match1to2, match2); ++ addParseToken(['m', 'mm'], MINUTE); ++ ++ // MOMENTS ++ ++ var getSetMinute = makeGetSet('Minutes', false); ++ ++ // FORMATTING ++ ++ addFormatToken('s', ['ss', 2], 0, 'second'); ++ ++ // ALIASES ++ ++ addUnitAlias('second', 's'); ++ ++ // PRIORITY ++ ++ addUnitPriority('second', 15); ++ ++ // PARSING ++ ++ addRegexToken('s', match1to2); ++ addRegexToken('ss', match1to2, match2); ++ addParseToken(['s', 'ss'], SECOND); ++ ++ // MOMENTS ++ ++ var getSetSecond = makeGetSet('Seconds', false); ++ ++ // FORMATTING ++ ++ addFormatToken('S', 0, 0, function () { ++ return ~~(this.millisecond() / 100); ++ }); ++ ++ addFormatToken(0, ['SS', 2], 0, function () { ++ return ~~(this.millisecond() / 10); ++ }); ++ ++ addFormatToken(0, ['SSS', 3], 0, 'millisecond'); ++ addFormatToken(0, ['SSSS', 4], 0, function () { ++ return this.millisecond() * 10; ++ }); ++ addFormatToken(0, ['SSSSS', 5], 0, function () { ++ return this.millisecond() * 100; ++ }); ++ addFormatToken(0, ['SSSSSS', 6], 0, function () { ++ return this.millisecond() * 1000; ++ }); ++ addFormatToken(0, ['SSSSSSS', 7], 0, function () { ++ return this.millisecond() * 10000; ++ }); ++ addFormatToken(0, ['SSSSSSSS', 8], 0, function () { ++ return this.millisecond() * 100000; ++ }); ++ addFormatToken(0, ['SSSSSSSSS', 9], 0, function () { ++ return this.millisecond() * 1000000; ++ }); ++ ++ // ALIASES ++ ++ addUnitAlias('millisecond', 'ms'); ++ ++ // PRIORITY ++ ++ addUnitPriority('millisecond', 16); ++ ++ // PARSING ++ ++ addRegexToken('S', match1to3, match1); ++ addRegexToken('SS', match1to3, match2); ++ addRegexToken('SSS', match1to3, match3); ++ ++ var token, getSetMillisecond; ++ for (token = 'SSSS'; token.length <= 9; token += 'S') { ++ addRegexToken(token, matchUnsigned); ++ } ++ ++ function parseMs(input, array) { ++ array[MILLISECOND] = toInt(('0.' + input) * 1000); ++ } ++ ++ for (token = 'S'; token.length <= 9; token += 'S') { ++ addParseToken(token, parseMs); ++ } ++ ++ getSetMillisecond = makeGetSet('Milliseconds', false); ++ ++ // FORMATTING ++ ++ addFormatToken('z', 0, 0, 'zoneAbbr'); ++ addFormatToken('zz', 0, 0, 'zoneName'); ++ ++ // MOMENTS ++ ++ function getZoneAbbr() { ++ return this._isUTC ? 'UTC' : ''; ++ } ++ ++ function getZoneName() { ++ return this._isUTC ? 'Coordinated Universal Time' : ''; ++ } ++ ++ var proto = Moment.prototype; ++ ++ proto.add = add; ++ proto.calendar = calendar$1; ++ proto.clone = clone; ++ proto.diff = diff; ++ proto.endOf = endOf; ++ proto.format = format; ++ proto.from = from; ++ proto.fromNow = fromNow; ++ proto.to = to; ++ proto.toNow = toNow; ++ proto.get = stringGet; ++ proto.invalidAt = invalidAt; ++ proto.isAfter = isAfter; ++ proto.isBefore = isBefore; ++ proto.isBetween = isBetween; ++ proto.isSame = isSame; ++ proto.isSameOrAfter = isSameOrAfter; ++ proto.isSameOrBefore = isSameOrBefore; ++ proto.isValid = isValid$2; ++ proto.lang = lang; ++ proto.locale = locale; ++ proto.localeData = localeData; ++ proto.max = prototypeMax; ++ proto.min = prototypeMin; ++ proto.parsingFlags = parsingFlags; ++ proto.set = stringSet; ++ proto.startOf = startOf; ++ proto.subtract = subtract; ++ proto.toArray = toArray; ++ proto.toObject = toObject; ++ proto.toDate = toDate; ++ proto.toISOString = toISOString; ++ proto.inspect = inspect; ++ if (typeof Symbol !== 'undefined' && Symbol.for != null) { ++ proto[Symbol.for('nodejs.util.inspect.custom')] = function () { ++ return 'Moment<' + this.format() + '>'; ++ }; ++ } ++ proto.toJSON = toJSON; ++ proto.toString = toString; ++ proto.unix = unix; ++ proto.valueOf = valueOf; ++ proto.creationData = creationData; ++ proto.eraName = getEraName; ++ proto.eraNarrow = getEraNarrow; ++ proto.eraAbbr = getEraAbbr; ++ proto.eraYear = getEraYear; ++ proto.year = getSetYear; ++ proto.isLeapYear = getIsLeapYear; ++ proto.weekYear = getSetWeekYear; ++ proto.isoWeekYear = getSetISOWeekYear; ++ proto.quarter = proto.quarters = getSetQuarter; ++ proto.month = getSetMonth; ++ proto.daysInMonth = getDaysInMonth; ++ proto.week = proto.weeks = getSetWeek; ++ proto.isoWeek = proto.isoWeeks = getSetISOWeek; ++ proto.weeksInYear = getWeeksInYear; ++ proto.weeksInWeekYear = getWeeksInWeekYear; ++ proto.isoWeeksInYear = getISOWeeksInYear; ++ proto.isoWeeksInISOWeekYear = getISOWeeksInISOWeekYear; ++ proto.date = getSetDayOfMonth; ++ proto.day = proto.days = getSetDayOfWeek; ++ proto.weekday = getSetLocaleDayOfWeek; ++ proto.isoWeekday = getSetISODayOfWeek; ++ proto.dayOfYear = getSetDayOfYear; ++ proto.hour = proto.hours = getSetHour; ++ proto.minute = proto.minutes = getSetMinute; ++ proto.second = proto.seconds = getSetSecond; ++ proto.millisecond = proto.milliseconds = getSetMillisecond; ++ proto.utcOffset = getSetOffset; ++ proto.utc = setOffsetToUTC; ++ proto.local = setOffsetToLocal; ++ proto.parseZone = setOffsetToParsedOffset; ++ proto.hasAlignedHourOffset = hasAlignedHourOffset; ++ proto.isDST = isDaylightSavingTime; ++ proto.isLocal = isLocal; ++ proto.isUtcOffset = isUtcOffset; ++ proto.isUtc = isUtc; ++ proto.isUTC = isUtc; ++ proto.zoneAbbr = getZoneAbbr; ++ proto.zoneName = getZoneName; ++ proto.dates = deprecate( ++ 'dates accessor is deprecated. Use date instead.', ++ getSetDayOfMonth ++ ); ++ proto.months = deprecate( ++ 'months accessor is deprecated. Use month instead', ++ getSetMonth ++ ); ++ proto.years = deprecate( ++ 'years accessor is deprecated. Use year instead', ++ getSetYear ++ ); ++ proto.zone = deprecate( ++ 'moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/', ++ getSetZone ++ ); ++ proto.isDSTShifted = deprecate( ++ 'isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information', ++ isDaylightSavingTimeShifted ++ ); ++ ++ function createUnix(input) { ++ return createLocal(input * 1000); ++ } ++ ++ function createInZone() { ++ return createLocal.apply(null, arguments).parseZone(); ++ } ++ ++ function preParsePostFormat(string) { ++ return string; ++ } ++ ++ var proto$1 = Locale.prototype; ++ ++ proto$1.calendar = calendar; ++ proto$1.longDateFormat = longDateFormat; ++ proto$1.invalidDate = invalidDate; ++ proto$1.ordinal = ordinal; ++ proto$1.preparse = preParsePostFormat; ++ proto$1.postformat = preParsePostFormat; ++ proto$1.relativeTime = relativeTime; ++ proto$1.pastFuture = pastFuture; ++ proto$1.set = set; ++ proto$1.eras = localeEras; ++ proto$1.erasParse = localeErasParse; ++ proto$1.erasConvertYear = localeErasConvertYear; ++ proto$1.erasAbbrRegex = erasAbbrRegex; ++ proto$1.erasNameRegex = erasNameRegex; ++ proto$1.erasNarrowRegex = erasNarrowRegex; ++ ++ proto$1.months = localeMonths; ++ proto$1.monthsShort = localeMonthsShort; ++ proto$1.monthsParse = localeMonthsParse; ++ proto$1.monthsRegex = monthsRegex; ++ proto$1.monthsShortRegex = monthsShortRegex; ++ proto$1.week = localeWeek; ++ proto$1.firstDayOfYear = localeFirstDayOfYear; ++ proto$1.firstDayOfWeek = localeFirstDayOfWeek; ++ ++ proto$1.weekdays = localeWeekdays; ++ proto$1.weekdaysMin = localeWeekdaysMin; ++ proto$1.weekdaysShort = localeWeekdaysShort; ++ proto$1.weekdaysParse = localeWeekdaysParse; ++ ++ proto$1.weekdaysRegex = weekdaysRegex; ++ proto$1.weekdaysShortRegex = weekdaysShortRegex; ++ proto$1.weekdaysMinRegex = weekdaysMinRegex; ++ ++ proto$1.isPM = localeIsPM; ++ proto$1.meridiem = localeMeridiem; ++ ++ function get$1(format, index, field, setter) { ++ var locale = getLocale(), ++ utc = createUTC().set(setter, index); ++ return locale[field](utc, format); ++ } ++ ++ function listMonthsImpl(format, index, field) { ++ if (isNumber(format)) { ++ index = format; ++ format = undefined; ++ } ++ ++ format = format || ''; ++ ++ if (index != null) { ++ return get$1(format, index, field, 'month'); ++ } ++ ++ var i, ++ out = []; ++ for (i = 0; i < 12; i++) { ++ out[i] = get$1(format, i, field, 'month'); ++ } ++ return out; ++ } ++ ++ // () ++ // (5) ++ // (fmt, 5) ++ // (fmt) ++ // (true) ++ // (true, 5) ++ // (true, fmt, 5) ++ // (true, fmt) ++ function listWeekdaysImpl(localeSorted, format, index, field) { ++ if (typeof localeSorted === 'boolean') { ++ if (isNumber(format)) { ++ index = format; ++ format = undefined; ++ } ++ ++ format = format || ''; ++ } else { ++ format = localeSorted; ++ index = format; ++ localeSorted = false; ++ ++ if (isNumber(format)) { ++ index = format; ++ format = undefined; ++ } ++ ++ format = format || ''; ++ } ++ ++ var locale = getLocale(), ++ shift = localeSorted ? locale._week.dow : 0, ++ i, ++ out = []; ++ ++ if (index != null) { ++ return get$1(format, (index + shift) % 7, field, 'day'); ++ } ++ ++ for (i = 0; i < 7; i++) { ++ out[i] = get$1(format, (i + shift) % 7, field, 'day'); ++ } ++ return out; ++ } ++ ++ function listMonths(format, index) { ++ return listMonthsImpl(format, index, 'months'); ++ } ++ ++ function listMonthsShort(format, index) { ++ return listMonthsImpl(format, index, 'monthsShort'); ++ } ++ ++ function listWeekdays(localeSorted, format, index) { ++ return listWeekdaysImpl(localeSorted, format, index, 'weekdays'); ++ } ++ ++ function listWeekdaysShort(localeSorted, format, index) { ++ return listWeekdaysImpl(localeSorted, format, index, 'weekdaysShort'); ++ } ++ ++ function listWeekdaysMin(localeSorted, format, index) { ++ return listWeekdaysImpl(localeSorted, format, index, 'weekdaysMin'); ++ } ++ ++ getSetGlobalLocale('en', { ++ eras: [ ++ { ++ since: '0001-01-01', ++ until: +Infinity, ++ offset: 1, ++ name: 'Anno Domini', ++ narrow: 'AD', ++ abbr: 'AD', ++ }, ++ { ++ since: '0000-12-31', ++ until: -Infinity, ++ offset: 1, ++ name: 'Before Christ', ++ narrow: 'BC', ++ abbr: 'BC', ++ }, ++ ], ++ dayOfMonthOrdinalParse: /\d{1,2}(th|st|nd|rd)/, ++ ordinal: function (number) { ++ var b = number % 10, ++ output = ++ toInt((number % 100) / 10) === 1 ++ ? 'th' ++ : b === 1 ++ ? 'st' ++ : b === 2 ++ ? 'nd' ++ : b === 3 ++ ? 'rd' ++ : 'th'; ++ return number + output; ++ }, ++ }); ++ ++ // Side effect imports ++ ++ hooks.lang = deprecate( ++ 'moment.lang is deprecated. Use moment.locale instead.', ++ getSetGlobalLocale ++ ); ++ hooks.langData = deprecate( ++ 'moment.langData is deprecated. Use moment.localeData instead.', ++ getLocale ++ ); ++ ++ var mathAbs = Math.abs; ++ ++ function abs() { ++ var data = this._data; ++ ++ this._milliseconds = mathAbs(this._milliseconds); ++ this._days = mathAbs(this._days); ++ this._months = mathAbs(this._months); ++ ++ data.milliseconds = mathAbs(data.milliseconds); ++ data.seconds = mathAbs(data.seconds); ++ data.minutes = mathAbs(data.minutes); ++ data.hours = mathAbs(data.hours); ++ data.months = mathAbs(data.months); ++ data.years = mathAbs(data.years); ++ ++ return this; ++ } ++ ++ function addSubtract$1(duration, input, value, direction) { ++ var other = createDuration(input, value); ++ ++ duration._milliseconds += direction * other._milliseconds; ++ duration._days += direction * other._days; ++ duration._months += direction * other._months; ++ ++ return duration._bubble(); ++ } ++ ++ // supports only 2.0-style add(1, 's') or add(duration) ++ function add$1(input, value) { ++ return addSubtract$1(this, input, value, 1); ++ } ++ ++ // supports only 2.0-style subtract(1, 's') or subtract(duration) ++ function subtract$1(input, value) { ++ return addSubtract$1(this, input, value, -1); ++ } ++ ++ function absCeil(number) { ++ if (number < 0) { ++ return Math.floor(number); ++ } else { ++ return Math.ceil(number); ++ } ++ } ++ ++ function bubble() { ++ var milliseconds = this._milliseconds, ++ days = this._days, ++ months = this._months, ++ data = this._data, ++ seconds, ++ minutes, ++ hours, ++ years, ++ monthsFromDays; ++ ++ // if we have a mix of positive and negative values, bubble down first ++ // check: https://github.com/moment/moment/issues/2166 ++ if ( ++ !( ++ (milliseconds >= 0 && days >= 0 && months >= 0) || ++ (milliseconds <= 0 && days <= 0 && months <= 0) ++ ) ++ ) { ++ milliseconds += absCeil(monthsToDays(months) + days) * 864e5; ++ days = 0; ++ months = 0; ++ } ++ ++ // The following code bubbles up values, see the tests for ++ // examples of what that means. ++ data.milliseconds = milliseconds % 1000; ++ ++ seconds = absFloor(milliseconds / 1000); ++ data.seconds = seconds % 60; ++ ++ minutes = absFloor(seconds / 60); ++ data.minutes = minutes % 60; ++ ++ hours = absFloor(minutes / 60); ++ data.hours = hours % 24; ++ ++ days += absFloor(hours / 24); ++ ++ // convert days to months ++ monthsFromDays = absFloor(daysToMonths(days)); ++ months += monthsFromDays; ++ days -= absCeil(monthsToDays(monthsFromDays)); ++ ++ // 12 months -> 1 year ++ years = absFloor(months / 12); ++ months %= 12; ++ ++ data.days = days; ++ data.months = months; ++ data.years = years; ++ ++ return this; ++ } ++ ++ function daysToMonths(days) { ++ // 400 years have 146097 days (taking into account leap year rules) ++ // 400 years have 12 months === 4800 ++ return (days * 4800) / 146097; ++ } ++ ++ function monthsToDays(months) { ++ // the reverse of daysToMonths ++ return (months * 146097) / 4800; ++ } ++ ++ function as(units) { ++ if (!this.isValid()) { ++ return NaN; ++ } ++ var days, ++ months, ++ milliseconds = this._milliseconds; ++ ++ units = normalizeUnits(units); ++ ++ if (units === 'month' || units === 'quarter' || units === 'year') { ++ days = this._days + milliseconds / 864e5; ++ months = this._months + daysToMonths(days); ++ switch (units) { ++ case 'month': ++ return months; ++ case 'quarter': ++ return months / 3; ++ case 'year': ++ return months / 12; ++ } ++ } else { ++ // handle milliseconds separately because of floating point math errors (issue #1867) ++ days = this._days + Math.round(monthsToDays(this._months)); ++ switch (units) { ++ case 'week': ++ return days / 7 + milliseconds / 6048e5; ++ case 'day': ++ return days + milliseconds / 864e5; ++ case 'hour': ++ return days * 24 + milliseconds / 36e5; ++ case 'minute': ++ return days * 1440 + milliseconds / 6e4; ++ case 'second': ++ return days * 86400 + milliseconds / 1000; ++ // Math.floor prevents floating point math errors here ++ case 'millisecond': ++ return Math.floor(days * 864e5) + milliseconds; ++ default: ++ throw new Error('Unknown unit ' + units); ++ } ++ } ++ } ++ ++ // TODO: Use this.as('ms')? ++ function valueOf$1() { ++ if (!this.isValid()) { ++ return NaN; ++ } ++ return ( ++ this._milliseconds + ++ this._days * 864e5 + ++ (this._months % 12) * 2592e6 + ++ toInt(this._months / 12) * 31536e6 ++ ); ++ } ++ ++ function makeAs(alias) { ++ return function () { ++ return this.as(alias); ++ }; ++ } ++ ++ var asMilliseconds = makeAs('ms'), ++ asSeconds = makeAs('s'), ++ asMinutes = makeAs('m'), ++ asHours = makeAs('h'), ++ asDays = makeAs('d'), ++ asWeeks = makeAs('w'), ++ asMonths = makeAs('M'), ++ asQuarters = makeAs('Q'), ++ asYears = makeAs('y'); ++ ++ function clone$1() { ++ return createDuration(this); ++ } ++ ++ function get$2(units) { ++ units = normalizeUnits(units); ++ return this.isValid() ? this[units + 's']() : NaN; ++ } ++ ++ function makeGetter(name) { ++ return function () { ++ return this.isValid() ? this._data[name] : NaN; ++ }; ++ } ++ ++ var milliseconds = makeGetter('milliseconds'), ++ seconds = makeGetter('seconds'), ++ minutes = makeGetter('minutes'), ++ hours = makeGetter('hours'), ++ days = makeGetter('days'), ++ months = makeGetter('months'), ++ years = makeGetter('years'); ++ ++ function weeks() { ++ return absFloor(this.days() / 7); ++ } ++ ++ var round = Math.round, ++ thresholds = { ++ ss: 44, // a few seconds to seconds ++ s: 45, // seconds to minute ++ m: 45, // minutes to hour ++ h: 22, // hours to day ++ d: 26, // days to month/week ++ w: null, // weeks to month ++ M: 11, // months to year ++ }; ++ ++ // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize ++ function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { ++ return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); ++ } ++ ++ function relativeTime$1(posNegDuration, withoutSuffix, thresholds, locale) { ++ var duration = createDuration(posNegDuration).abs(), ++ seconds = round(duration.as('s')), ++ minutes = round(duration.as('m')), ++ hours = round(duration.as('h')), ++ days = round(duration.as('d')), ++ months = round(duration.as('M')), ++ weeks = round(duration.as('w')), ++ years = round(duration.as('y')), ++ a = ++ (seconds <= thresholds.ss && ['s', seconds]) || ++ (seconds < thresholds.s && ['ss', seconds]) || ++ (minutes <= 1 && ['m']) || ++ (minutes < thresholds.m && ['mm', minutes]) || ++ (hours <= 1 && ['h']) || ++ (hours < thresholds.h && ['hh', hours]) || ++ (days <= 1 && ['d']) || ++ (days < thresholds.d && ['dd', days]); ++ ++ if (thresholds.w != null) { ++ a = ++ a || ++ (weeks <= 1 && ['w']) || ++ (weeks < thresholds.w && ['ww', weeks]); ++ } ++ a = a || ++ (months <= 1 && ['M']) || ++ (months < thresholds.M && ['MM', months]) || ++ (years <= 1 && ['y']) || ['yy', years]; ++ ++ a[2] = withoutSuffix; ++ a[3] = +posNegDuration > 0; ++ a[4] = locale; ++ return substituteTimeAgo.apply(null, a); ++ } ++ ++ // This function allows you to set the rounding function for relative time strings ++ function getSetRelativeTimeRounding(roundingFunction) { ++ if (roundingFunction === undefined) { ++ return round; ++ } ++ if (typeof roundingFunction === 'function') { ++ round = roundingFunction; ++ return true; ++ } ++ return false; ++ } ++ ++ // This function allows you to set a threshold for relative time strings ++ function getSetRelativeTimeThreshold(threshold, limit) { ++ if (thresholds[threshold] === undefined) { ++ return false; ++ } ++ if (limit === undefined) { ++ return thresholds[threshold]; ++ } ++ thresholds[threshold] = limit; ++ if (threshold === 's') { ++ thresholds.ss = limit - 1; ++ } ++ return true; ++ } ++ ++ function humanize(argWithSuffix, argThresholds) { ++ if (!this.isValid()) { ++ return this.localeData().invalidDate(); ++ } ++ ++ var withSuffix = false, ++ th = thresholds, ++ locale, ++ output; ++ ++ if (typeof argWithSuffix === 'object') { ++ argThresholds = argWithSuffix; ++ argWithSuffix = false; ++ } ++ if (typeof argWithSuffix === 'boolean') { ++ withSuffix = argWithSuffix; ++ } ++ if (typeof argThresholds === 'object') { ++ th = Object.assign({}, thresholds, argThresholds); ++ if (argThresholds.s != null && argThresholds.ss == null) { ++ th.ss = argThresholds.s - 1; ++ } ++ } ++ ++ locale = this.localeData(); ++ output = relativeTime$1(this, !withSuffix, th, locale); ++ ++ if (withSuffix) { ++ output = locale.pastFuture(+this, output); ++ } ++ ++ return locale.postformat(output); ++ } ++ ++ var abs$1 = Math.abs; ++ ++ function sign(x) { ++ return (x > 0) - (x < 0) || +x; ++ } ++ ++ function toISOString$1() { ++ // for ISO strings we do not use the normal bubbling rules: ++ // * milliseconds bubble up until they become hours ++ // * days do not bubble at all ++ // * months bubble up until they become years ++ // This is because there is no context-free conversion between hours and days ++ // (think of clock changes) ++ // and also not between days and months (28-31 days per month) ++ if (!this.isValid()) { ++ return this.localeData().invalidDate(); ++ } ++ ++ var seconds = abs$1(this._milliseconds) / 1000, ++ days = abs$1(this._days), ++ months = abs$1(this._months), ++ minutes, ++ hours, ++ years, ++ s, ++ total = this.asSeconds(), ++ totalSign, ++ ymSign, ++ daysSign, ++ hmsSign; ++ ++ if (!total) { ++ // this is the same as C#'s (Noda) and python (isodate)... ++ // but not other JS (goog.date) ++ return 'P0D'; ++ } ++ ++ // 3600 seconds -> 60 minutes -> 1 hour ++ minutes = absFloor(seconds / 60); ++ hours = absFloor(minutes / 60); ++ seconds %= 60; ++ minutes %= 60; ++ ++ // 12 months -> 1 year ++ years = absFloor(months / 12); ++ months %= 12; ++ ++ // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js ++ s = seconds ? seconds.toFixed(3).replace(/\.?0+$/, '') : ''; ++ ++ totalSign = total < 0 ? '-' : ''; ++ ymSign = sign(this._months) !== sign(total) ? '-' : ''; ++ daysSign = sign(this._days) !== sign(total) ? '-' : ''; ++ hmsSign = sign(this._milliseconds) !== sign(total) ? '-' : ''; ++ ++ return ( ++ totalSign + ++ 'P' + ++ (years ? ymSign + years + 'Y' : '') + ++ (months ? ymSign + months + 'M' : '') + ++ (days ? daysSign + days + 'D' : '') + ++ (hours || minutes || seconds ? 'T' : '') + ++ (hours ? hmsSign + hours + 'H' : '') + ++ (minutes ? hmsSign + minutes + 'M' : '') + ++ (seconds ? hmsSign + s + 'S' : '') ++ ); ++ } ++ ++ var proto$2 = Duration.prototype; ++ ++ proto$2.isValid = isValid$1; ++ proto$2.abs = abs; ++ proto$2.add = add$1; ++ proto$2.subtract = subtract$1; ++ proto$2.as = as; ++ proto$2.asMilliseconds = asMilliseconds; ++ proto$2.asSeconds = asSeconds; ++ proto$2.asMinutes = asMinutes; ++ proto$2.asHours = asHours; ++ proto$2.asDays = asDays; ++ proto$2.asWeeks = asWeeks; ++ proto$2.asMonths = asMonths; ++ proto$2.asQuarters = asQuarters; ++ proto$2.asYears = asYears; ++ proto$2.valueOf = valueOf$1; ++ proto$2._bubble = bubble; ++ proto$2.clone = clone$1; ++ proto$2.get = get$2; ++ proto$2.milliseconds = milliseconds; ++ proto$2.seconds = seconds; ++ proto$2.minutes = minutes; ++ proto$2.hours = hours; ++ proto$2.days = days; ++ proto$2.weeks = weeks; ++ proto$2.months = months; ++ proto$2.years = years; ++ proto$2.humanize = humanize; ++ proto$2.toISOString = toISOString$1; ++ proto$2.toString = toISOString$1; ++ proto$2.toJSON = toISOString$1; ++ proto$2.locale = locale; ++ proto$2.localeData = localeData; ++ ++ proto$2.toIsoString = deprecate( ++ 'toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', ++ toISOString$1 ++ ); ++ proto$2.lang = lang; ++ ++ // FORMATTING ++ ++ addFormatToken('X', 0, 0, 'unix'); ++ addFormatToken('x', 0, 0, 'valueOf'); ++ ++ // PARSING ++ ++ addRegexToken('x', matchSigned); ++ addRegexToken('X', matchTimestamp); ++ addParseToken('X', function (input, array, config) { ++ config._d = new Date(parseFloat(input) * 1000); ++ }); ++ addParseToken('x', function (input, array, config) { ++ config._d = new Date(toInt(input)); ++ }); ++ ++ //! moment.js ++ ++ hooks.version = '2.29.1'; ++ ++ setHookCallback(createLocal); ++ ++ hooks.fn = proto; ++ hooks.min = min; ++ hooks.max = max; ++ hooks.now = now; ++ hooks.utc = createUTC; ++ hooks.unix = createUnix; ++ hooks.months = listMonths; ++ hooks.isDate = isDate; ++ hooks.locale = getSetGlobalLocale; ++ hooks.invalid = createInvalid; ++ hooks.duration = createDuration; ++ hooks.isMoment = isMoment; ++ hooks.weekdays = listWeekdays; ++ hooks.parseZone = createInZone; ++ hooks.localeData = getLocale; ++ hooks.isDuration = isDuration; ++ hooks.monthsShort = listMonthsShort; ++ hooks.weekdaysMin = listWeekdaysMin; ++ hooks.defineLocale = defineLocale; ++ hooks.updateLocale = updateLocale; ++ hooks.locales = listLocales; ++ hooks.weekdaysShort = listWeekdaysShort; ++ hooks.normalizeUnits = normalizeUnits; ++ hooks.relativeTimeRounding = getSetRelativeTimeRounding; ++ hooks.relativeTimeThreshold = getSetRelativeTimeThreshold; ++ hooks.calendarFormat = getCalendarFormat; ++ hooks.prototype = proto; ++ ++ // currently HTML5 input type only supports 24-hour formats ++ hooks.HTML5_FMT = { ++ DATETIME_LOCAL: 'YYYY-MM-DDTHH:mm', // ++ DATETIME_LOCAL_SECONDS: 'YYYY-MM-DDTHH:mm:ss', // ++ DATETIME_LOCAL_MS: 'YYYY-MM-DDTHH:mm:ss.SSS', // ++ DATE: 'YYYY-MM-DD', // ++ TIME: 'HH:mm', // ++ TIME_SECONDS: 'HH:mm:ss', // ++ TIME_MS: 'HH:mm:ss.SSS', // ++ WEEK: 'GGGG-[W]WW', // ++ MONTH: 'YYYY-MM', // ++ }; ++ ++ return hooks; ++ ++}))); +diff --git a/pkg/http-server/assets/webhook-scan-logs.css b/pkg/http-server/assets/webhook-scan-logs.css +new file mode 100644 +index 0000000..5e701fb +--- /dev/null ++++ b/pkg/http-server/assets/webhook-scan-logs.css +@@ -0,0 +1,33 @@ ++ul { ++ list-style: none; ++ margin-left: 16px; ++ padding: 0; ++} ++ ++.jsontree_child-nodes { ++ margin-left: 16px; ++ padding-left: 16px; ++} ++ ++* { ++ box-sizing: border-box; ++} ++ ++.table-sm td, .table-sm th { ++ padding: 10px; ++} ++ ++.review-status.warn { ++ color: #e28d43; ++ font-weight: bold; ++} ++ ++.review-status.rejected { ++ color: #e24343; ++ font-weight: bold; ++} ++ ++.review-status.allowed { ++ color: #43e268; ++ font-weight: bold; ++} +diff --git a/pkg/http-server/assets/webhook-scan-logs.js b/pkg/http-server/assets/webhook-scan-logs.js +new file mode 100644 +index 0000000..8655332 +--- /dev/null ++++ b/pkg/http-server/assets/webhook-scan-logs.js +@@ -0,0 +1,44 @@ ++// Replace all json-objects elements to be a JSON tree ++let jsonElements = document.getElementsByClassName("json-object") ++for (var i = 0; i < jsonElements.length; i++) { ++ let element = jsonElements[i] ++ if (element.innerText.length < 1) { ++ continue ++ } ++ ++ let data = JSON.parse(element.innerText); ++ element.innerText = "" ++ ++ jsonTree.create(data, element); ++} ++ ++// Replace all time-object elements to be in the 'DD/MM/YYYY hh:mm:ss A' format of moment.js ++let timeElements = document.getElementsByClassName("time-object") ++for (var i = 0; i < timeElements.length; i++) { ++ let element = timeElements[i] ++ let elapsedTimeUntilNow = Date.now() - new Date(element.innerText) ++ if (elapsedTimeUntilNow / 1000 < 120) { ++ // In case elapsed less than 2 minutes, show "A few seconds ago" ++ element.innerText = moment(element.innerText).fromNow() ++ } ++ else { ++ element.innerText = moment(element.innerText).format('DD/MM/YYYY hh:mm:ss A') ++ } ++} ++ ++// Change the colors of the review status ++let statusElements = document.getElementsByClassName("review-status") ++for (var i = 0; i < statusElements.length; i++) { ++ let element = statusElements[i] ++ switch (element.innerText) { ++ case "Allowed": ++ element.classList.add("allowed") ++ break ++ case "Rejected": ++ element.classList.add("rejected") ++ break ++ default: ++ element.classList.add("warn") ++ break ++ } ++} +diff --git a/pkg/http-server/constants.go b/pkg/http-server/constants.go +index 2226cf4..cb7e4ea 100644 +--- a/pkg/http-server/constants.go ++++ b/pkg/http-server/constants.go +@@ -20,6 +20,9 @@ const ( + // GatewayDefaultPort - default port at which the http server listens + GatewayDefaultPort = "9010" + ++ // GatewayDefaultPort - default port at which the https server listens ++ TLSGatewayDefaultPort = "9443" ++ + // APIVersion - default api version for REST endpoints + APIVersion = "v1" + ) +diff --git a/pkg/http-server/handler.go b/pkg/http-server/handler.go +index eb86e5a..e165d25 100644 +--- a/pkg/http-server/handler.go ++++ b/pkg/http-server/handler.go +@@ -19,9 +19,12 @@ package httpserver + // APIHandler struct for http api server + type APIHandler struct { + test bool ++ configFile string + } + + // NewAPIHandler returns a new APIHandler{} +-func NewAPIHandler() *APIHandler { +- return &APIHandler{} ++func NewAPIHandler(configFile string) *APIHandler { ++ return &APIHandler{ ++ configFile: configFile, ++ } + } +diff --git a/pkg/http-server/handler_test.go b/pkg/http-server/handler_test.go +index 15c926e..d4d88c1 100644 +--- a/pkg/http-server/handler_test.go ++++ b/pkg/http-server/handler_test.go +@@ -8,8 +8,10 @@ import ( + func TestNewAPIHandler(t *testing.T) { + t.Run("new API gateway", func(t *testing.T) { + var ( +- want = APIHandler{} +- got = NewAPIHandler() ++ want = APIHandler{ ++ configFile: "", ++ } ++ got = NewAPIHandler("") + ) + if !reflect.DeepEqual(*got, want) { + t.Errorf("got: '%v', want: '%v'", *got, want) +diff --git a/pkg/http-server/health_test.go b/pkg/http-server/health_test.go +index 08b92f8..2a447ea 100644 +--- a/pkg/http-server/health_test.go ++++ b/pkg/http-server/health_test.go +@@ -8,7 +8,7 @@ import ( + + func TestHealth(t *testing.T) { + +- handler := NewAPIHandler() ++ handler := NewAPIHandler("") + + t.Run("test health api", func(t *testing.T) { + var ( +diff --git a/pkg/http-server/k8s_testdata/config-deny-category.toml b/pkg/http-server/k8s_testdata/config-deny-category.toml +new file mode 100644 +index 0000000..b3d4204 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/config-deny-category.toml +@@ -0,0 +1,5 @@ ++[k8s-deny-rules] ++ denied-categories = [ ++ "Identity and Access Management", ++ "Network Security", ++ ] +diff --git a/pkg/http-server/k8s_testdata/config-deny-high.toml b/pkg/http-server/k8s_testdata/config-deny-high.toml +new file mode 100644 +index 0000000..5046654 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/config-deny-high.toml +@@ -0,0 +1,5 @@ ++[severity] ++level = "medium" ++ ++[k8s-deny-rules] ++ denied-severity = "high" +diff --git a/pkg/http-server/k8s_testdata/config-deny-non-existing-category.toml b/pkg/http-server/k8s_testdata/config-deny-non-existing-category.toml +new file mode 100644 +index 0000000..d38bc48 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/config-deny-non-existing-category.toml +@@ -0,0 +1,8 @@ ++[severity] ++level = "medium" ++ ++[k8s-deny-rules] ++ denied-categories = [ ++ "Hola", ++ "Invalid", ++ ] +diff --git a/pkg/http-server/k8s_testdata/config-medium-severity.toml b/pkg/http-server/k8s_testdata/config-medium-severity.toml +new file mode 100644 +index 0000000..17b1aed +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/config-medium-severity.toml +@@ -0,0 +1,2 @@ ++[severity] ++level = "medium" +diff --git a/pkg/http-server/k8s_testdata/config-specific-rule.toml b/pkg/http-server/k8s_testdata/config-specific-rule.toml +new file mode 100644 +index 0000000..ad35bd7 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/config-specific-rule.toml +@@ -0,0 +1,5 @@ ++[rules] ++ scan-rules = [ ++ "AWS.S3Bucket.DS.High.1043", ++ "accurics.kubernetes.IAM.107" ++ ] +diff --git a/pkg/http-server/k8s_testdata/empty.json b/pkg/http-server/k8s_testdata/empty.json +new file mode 100644 +index 0000000..e69de29 +diff --git a/pkg/http-server/k8s_testdata/empty_object.json b/pkg/http-server/k8s_testdata/empty_object.json +new file mode 100644 +index 0000000..fb4f3ba +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/empty_object.json +@@ -0,0 +1,7 @@ ++{ ++ "apiVersion": "admission.k8s.io/v1", ++ "kind": "AdmissionReview", ++ "request": { ++ "uid": "705ab4f5-6393-11e8-b7cc-42010a800002" ++ } ++} +diff --git a/pkg/http-server/k8s_testdata/invalid.json b/pkg/http-server/k8s_testdata/invalid.json +new file mode 100644 +index 0000000..f9ff3aa +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/invalid.json +@@ -0,0 +1 @@ ++some invalid tf file +diff --git a/pkg/http-server/k8s_testdata/risky_testconfig.json b/pkg/http-server/k8s_testdata/risky_testconfig.json +new file mode 100644 +index 0000000..3b1f42d +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/risky_testconfig.json +@@ -0,0 +1,27 @@ ++{ ++ "apiVersion":"admission.k8s.io/v1", ++ "kind":"AdmissionReview", ++ "request":{ ++ "uid":"705ab4f5-6393-11e8-b7cc-42010a800002", ++ "object": ++ { ++ "apiVersion": "v1", ++ "kind": "Pod", ++ "metadata": { ++ "name": "root-run-unset" ++ }, ++ "spec": { ++ "containers": [ ++ { ++ "name": "busybox", ++ "image": "busybox", ++ "securityContext": { ++ "allowPrivilegeEscalation": false, ++ "readOnlyRootFilesystem": true ++ } ++ } ++ ] ++ } ++ } ++ } ++} +diff --git a/pkg/http-server/k8s_testdata/testconfig.json b/pkg/http-server/k8s_testdata/testconfig.json +new file mode 100644 +index 0000000..6e3bc1a +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testconfig.json +@@ -0,0 +1,27 @@ ++{ ++ "apiVersion": "admission.k8s.io/v1", ++ "kind": "AdmissionReview", ++ "request": { ++ "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", ++ "object": { ++ "apiVersion": "v1", ++ "kind": "Service", ++ "metadata": { ++ "creationTimestamp": "2021-02-16T19:16:01Z", ++ "labels": { ++ "run": "nginx" ++ }, ++ "name": "nginx", ++ "namespace": "default", ++ "resourceVersion": "17561", ++ "selfLink": "/api/v1/namespaces/default/pods/nginx", ++ "uid": "7a269efe-d951-49b6-a3af-e1a265cb9efe" ++ }, ++ "spec": { ++ "containers": [ ++ ] ++ } ++ }, ++ "operation": "CREATE" ++ } ++} +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json +new file mode 100755 +index 0000000..24409fb +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json +@@ -0,0 +1,21 @@ ++{ ++ "name": "privilegeEscalationCheck", ++ "file": "securityContextCheck.rego", ++ "template_args": { ++ "allowed": "false", ++ "arg1": "cpu", ++ "arg2": "limits", ++ "name": "privilegeEscalationCheck", ++ "not_allowed": "true", ++ "param": "allowPrivilegeEscalation", ++ "param1": "securityContext", ++ "prefix": "", ++ "suffix": "", ++ "value": "true" ++ }, ++ "severity": "HIGH", ++ "description": "Containers Should Not Run with AllowPrivilegeEscalation", ++ "reference_id": "AC-K8-CA-PO-H-0165", ++ "category": "Cloud Assets Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json +new file mode 100755 +index 0000000..d0bff54 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "kubeDashboardEnabled", ++ "file": "kubeDashboardEnabled.rego", ++ "template_args": { ++ "name": "kubeDashboardEnabled", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Ensure Kubernetes Dashboard Is Not Deployed", ++ "reference_id": "AC-K8-DS-PO-M-0176", ++ "category": "Data Security", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json +new file mode 100755 +index 0000000..d8a40cc +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "tillerDeployed", ++ "file": "tillerDeployed.rego", ++ "template_args": { ++ "name": "tillerDeployed", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Ensure That Tiller (Helm V2) Is Not Deployed", ++ "reference_id": "AC-K8-DS-PO-M-0177", ++ "category": "Data Security", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json +new file mode 100755 +index 0000000..d07858d +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "priviledgedContainersEnabled", ++ "file": "priviledgedContainersEnabled.rego", ++ "template_args": { ++ "name": "priviledgedContainersEnabled", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Minimize the admission of privileged containers", ++ "reference_id": "AC-K8-IA-PO-H-0106", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json +new file mode 100755 +index 0000000..71f74c3 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "disallowedSysCalls", ++ "file": "disallowedSysCalls.rego", ++ "template_args": { ++ "name": "disallowedSysCalls", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Allowing the pod to make system level calls provide access to host/node sensitive information", ++ "reference_id": "AC-K8-IA-PO-H-0137", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json +new file mode 100755 +index 0000000..16cfd6d +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "allowedHostPath", ++ "file": "allowedHostPath.rego", ++ "template_args": { ++ "name": "allowedHostPath", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Allowing hostPaths to mount to Pod arise the probability of getting access to the node's filesystem", ++ "reference_id": "AC-K8-IA-PO-H-0138", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json +new file mode 100755 +index 0000000..f7c9d54 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json +@@ -0,0 +1,21 @@ ++{ ++ "name": "runAsNonRootCheck", ++ "file": "securityContextCheck.rego", ++ "template_args": { ++ "allowed": "false", ++ "arg1": "cpu", ++ "arg2": "limits", ++ "name": "runAsNonRootCheck", ++ "not_allowed": "true", ++ "param": "runAsNonRoot", ++ "param1": "securityContext", ++ "prefix": "", ++ "suffix": "", ++ "value": "false" ++ }, ++ "severity": "HIGH", ++ "description": "Minimize Admission of Root Containers", ++ "reference_id": "AC-K8-IA-PO-H-0168", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json +new file mode 100755 +index 0000000..6f9be71 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "autoMountTokenEnabled", ++ "file": "autoMountTokenEnabled.rego", ++ "template_args": { ++ "name": "autoMountTokenEnabled", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Ensure that Service Account Tokens are only mounted where necessary", ++ "reference_id": "AC-K8-IA-PO-M-0105", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json +new file mode 100755 +index 0000000..d7befdd +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "appArmorProfile", ++ "file": "appArmorProfile.rego", ++ "template_args": { ++ "name": "appArmorProfile", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "AppArmor profile not set to default or custom profile will make the container vulnerable to kernel level threats", ++ "reference_id": "AC-K8-IA-PO-M-0135", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json +new file mode 100755 +index 0000000..5a22d3f +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "allowedProcMount", ++ "file": "allowedProcMount.rego", ++ "template_args": { ++ "name": "allowedProcMount", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Unmasking the procMount will allow more information than is necessary to the program running in the containers spawned by k8s", ++ "reference_id": "AC-K8-IA-PO-M-0139", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json +new file mode 100755 +index 0000000..10fad68 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json +@@ -0,0 +1,21 @@ ++{ ++ "name": "readOnlyFileSystem", ++ "file": "securityContextCheck.rego", ++ "template_args": { ++ "allowed": "false", ++ "arg1": "limits", ++ "arg2": "cpu", ++ "name": "readOnlyFileSystem", ++ "not_allowed": "true", ++ "param": "readOnlyRootFilesystem", ++ "param1": "securityContext", ++ "prefix": "", ++ "suffix": "", ++ "value": "false" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container images with readOnlyRootFileSystem set as false mounts the container root file system with write permissions", ++ "reference_id": "AC-K8-IA-PO-M-0140", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json +new file mode 100755 +index 0000000..5293c73 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "secCompProfile", ++ "file": "secCompProfile.rego", ++ "template_args": { ++ "name": "secCompProfile", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Default seccomp profile not enabled will make the container to make non-essential system calls", ++ "reference_id": "AC-K8-IA-PO-M-0141", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json +new file mode 100755 +index 0000000..07843f8 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json +@@ -0,0 +1,22 @@ ++{ ++ "name": "allowedVolumes", ++ "file": "allowedVolumes.rego", ++ "template_args": { ++ "name": "allowedVolumes", ++ "prefix": "", ++ "secure_volumes": [ ++ "configMap", ++ "emptyDir", ++ "projected", ++ "secret", ++ "downwardAPI", ++ "persistentVolumeClaim" ++ ], ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Some volume types mount the host file system paths to the pod or container, thus increasing the chance of escaping the container to access the host", ++ "reference_id": "AC-K8-IA-PO-M-0143", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json +new file mode 100755 +index 0000000..a98195d +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "falseHostPID", ++ "file": "specBoolCheck.rego", ++ "template_args": { ++ "name": "falseHostPID", ++ "param": "hostPID", ++ "prefix": "", ++ "suffix": "", ++ "value": "true" ++ }, ++ "severity": "MEDIUM", ++ "description": "Containers Should Not Share Host Process ID Namespace", ++ "reference_id": "AC-K8-IA-PO-M-0162", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json +new file mode 100755 +index 0000000..11f59e9 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json +@@ -0,0 +1,15 @@ ++{ ++ "name": "netRawCapabilityUsed", ++ "file": "capabilityUsed.rego", ++ "template_args": { ++ "attribute": "requiredDropCapabilities", ++ "name": "netRawCapabilityUsed", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Minimize the admission of containers with the NET_RAW capability", ++ "reference_id": "AC-K8-IA-PS-M-0112", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json +new file mode 100755 +index 0000000..23c8d90 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "secretsAsEnvVariables", ++ "file": "secretsAsEnvVariables.rego", ++ "template_args": { ++ "name": "secretsAsEnvVariables", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Prefer using secrets as files over secrets as environment variables", ++ "reference_id": "AC-K8-NS-PO-H-0117", ++ "category": "Network Security", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json +new file mode 100755 +index 0000000..b211361 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "capSysAdminUsed", ++ "file": "capSysAdminUsed.rego", ++ "template_args": { ++ "name": "capSysAdminUsed", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Do Not Use CAP_SYS_ADMIN Linux Capability", ++ "reference_id": "AC-K8-NS-PO-H-0170", ++ "category": "Network Security", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json +new file mode 100755 +index 0000000..43ba243 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "securityContextUsed", ++ "file": "securityContextUsed.rego", ++ "template_args": { ++ "name": "securityContextUsed", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Apply Security Context to Your Pods and Containers", ++ "reference_id": "AC-K8-NS-PO-M-0122", ++ "category": "Network Security", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json +new file mode 100755 +index 0000000..804a12e +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "imageWithoutDigest", ++ "file": "imageWithoutDigest.rego", ++ "template_args": { ++ "name": "imageWithoutDigest", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Image without digest affects the integrity principle of image security", ++ "reference_id": "AC-K8-NS-PO-M-0133", ++ "category": "Network Security", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json +new file mode 100755 +index 0000000..e96b364 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "falseHostIPC", ++ "file": "specBoolCheck.rego", ++ "template_args": { ++ "name": "falseHostIPC", ++ "param": "hostIPC", ++ "prefix": "", ++ "suffix": "", ++ "value": "true" ++ }, ++ "severity": "MEDIUM", ++ "description": "Containers Should Not Share Host IPC Namespace", ++ "reference_id": "AC-K8-NS-PO-M-0163", ++ "category": "Network Security", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json +new file mode 100755 +index 0000000..5c893ce +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "falseHostNetwork", ++ "file": "specBoolCheck.rego", ++ "template_args": { ++ "name": "falseHostNetwork", ++ "param": "hostNetwork", ++ "prefix": "", ++ "suffix": "", ++ "value": "true" ++ }, ++ "severity": "MEDIUM", ++ "description": "Containers Should Not Share the Host Network Namespace", ++ "reference_id": "AC-K8-NS-PO-M-0164", ++ "category": "Network Security", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json +new file mode 100755 +index 0000000..df493d8 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json +@@ -0,0 +1,17 @@ ++{ ++ "name": "dontConnectDockerSock", ++ "file": "dockerSockCheck.rego", ++ "template_args": { ++ "attrib": "spec.volumes[_].hostPath", ++ "name": "dontConnectDockerSock", ++ "param": "path", ++ "prefix": "", ++ "suffix": "", ++ "value": "/var/run/docker" ++ }, ++ "severity": "MEDIUM", ++ "description": "Restrict Mounting Docker Socket in a Container", ++ "reference_id": "AC-K8-NS-PO-M-0171", ++ "category": "Network Security", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json +new file mode 100755 +index 0000000..2243106 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "containersAsHighUID", ++ "file": "containersAsHighUID.rego", ++ "template_args": { ++ "name": "containersAsHighUID", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Containers Should Run as a High UID to Avoid Host Conflict", ++ "reference_id": "AC-K8-NS-PO-M-0182", ++ "category": "Network Security", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json +new file mode 100755 +index 0000000..6340d31 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json +@@ -0,0 +1,19 @@ ++{ ++ "name": "alwaysPullImages", ++ "file": "commandCheck.rego", ++ "template_args": { ++ "argument": "--enable-admission-plugins", ++ "name": "alwaysPullImages", ++ "negation": "", ++ "optional": "", ++ "param": "AlwaysPullImages", ++ "prefix": "", ++ "presence": "not", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "AlwaysPullImages plugin is not set", ++ "reference_id": "AC-K8-OE-PK-M-0034", ++ "category": "Operational Efficiency", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json +new file mode 100755 +index 0000000..aebef86 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json +@@ -0,0 +1,21 @@ ++{ ++ "name": "CpuRequestsCheck", ++ "file": "securityContextCheck.rego", ++ "template_args": { ++ "allowed": "true", ++ "arg1": "requests", ++ "arg2": "cpu", ++ "name": "CpuRequestsCheck", ++ "not_allowed": "false", ++ "param": "resources", ++ "param1": "resources", ++ "prefix": "", ++ "suffix": "", ++ "value": "false" ++ }, ++ "severity": "Medium", ++ "description": "CPU Request Not Set in config file.", ++ "reference_id": "AC-K8-OE-PK-M-0155", ++ "category": "Operational Efficiency", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json +new file mode 100755 +index 0000000..c74835c +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json +@@ -0,0 +1,21 @@ ++{ ++ "name": "CpulimitsCheck", ++ "file": "securityContextCheck.rego", ++ "template_args": { ++ "allowed": "true", ++ "arg1": "limits", ++ "arg2": "cpu", ++ "name": "CpulimitsCheck", ++ "not_allowed": "false", ++ "param": "limits", ++ "param1": "resources", ++ "prefix": "", ++ "suffix": "", ++ "value": "false" ++ }, ++ "severity": "Medium", ++ "description": "CPU Limits Not Set in config file.", ++ "reference_id": "AC-K8-OE-PK-M-0156", ++ "category": "Operational Efficiency", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json +new file mode 100755 +index 0000000..691b588 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json +@@ -0,0 +1,21 @@ ++{ ++ "name": "MemoryRequestsCheck", ++ "file": "securityContextCheck.rego", ++ "template_args": { ++ "allowed": "true", ++ "arg1": "requests", ++ "arg2": "memory", ++ "name": "MemoryRequestsCheck", ++ "not_allowed": "false", ++ "param": "resources", ++ "param1": "resources", ++ "prefix": "", ++ "suffix": "", ++ "value": "false" ++ }, ++ "severity": "Medium", ++ "description": "Memory Request Not Set in config file.", ++ "reference_id": "AC-K8-OE-PK-M-0157", ++ "category": "Operational Efficiency", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json +new file mode 100755 +index 0000000..7ab678c +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json +@@ -0,0 +1,21 @@ ++{ ++ "name": "MemorylimitsCheck", ++ "file": "securityContextCheck.rego", ++ "template_args": { ++ "allowed": "true", ++ "arg1": "limits", ++ "arg2": "memory", ++ "name": "MemorylimitsCheck", ++ "not_allowed": "false", ++ "param": "limits", ++ "param1": "resources", ++ "prefix": "", ++ "suffix": "", ++ "value": "false" ++ }, ++ "severity": "Medium", ++ "description": "Memory Limits Not Set in config file.", ++ "reference_id": "AC-K8-OE-PK-M-0158", ++ "category": "Operational Efficiency", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json +new file mode 100755 +index 0000000..9ce0938 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "nolivenessProbe", ++ "file": "probeCheck.rego", ++ "template_args": { ++ "argument": "livenessProbe", ++ "argumentTF": "liveness_probe", ++ "name": "nolivenessProbe", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "LOW", ++ "description": "No liveness probe will ensure there is no recovery in case of unexpected errors", ++ "reference_id": "AC-K8-OE-PO-L-0129", ++ "category": "Operational Efficiency", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json +new file mode 100755 +index 0000000..a0e4058 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "noReadinessProbe", ++ "file": "probeCheck.rego", ++ "template_args": { ++ "argument": "readinessProbe", ++ "argumentTF": "readiness_probe", ++ "name": "noReadinessProbe", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "LOW", ++ "description": "No readiness probe will affect automatic recovery in case of unexpected errors", ++ "reference_id": "AC-K8-OE-PO-L-0130", ++ "category": "Operational Efficiency", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json +new file mode 100755 +index 0000000..83eec4e +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "imageWithLatestTag", ++ "file": "imageWithLatestTag.rego", ++ "template_args": { ++ "name": "imageWithLatestTag", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "LOW", ++ "description": "No tag or container image with :Latest tag makes difficult to rollback and track", ++ "reference_id": "AC-K8-OE-PO-L-0134", ++ "category": "Operational Efficiency", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json +new file mode 100755 +index 0000000..6e0c8fd +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "otherNamespace", ++ "file": "otherNamespace.rego", ++ "template_args": { ++ "name": "otherNamespace", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Default Namespace Should Not be Used", ++ "reference_id": "AC-K8-OE-PO-M-0166", ++ "category": "Operational Efficiency", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego +new file mode 100755 +index 0000000..e7d8463 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego +@@ -0,0 +1,107 @@ ++### this policy depends on the parameters specified by the user/client. Here we are considering that no hostPath are allowed### ++package accurics ++ ++#rule for pod ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ vols := pod.config.spec.volumes[_] ++ parameters := {} ++ has_field(vols, "hostPath") ++ allowedPaths := get_allowed_paths(parameters) ++ input_hostpath_violation(allowedPaths, vols) ++} ++ ++#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ vols := kind.config.spec.template.spec.volumes[_] ++ #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } ++ parameters := {} ++ has_field(vols, "hostPath") ++ allowedPaths := get_allowed_paths(parameters) ++ input_hostpath_violation(allowedPaths, vols) ++} ++ ++#rule for cron_job ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ vols := cron_job.config.spec.jobTemplate.spec.template.spec.volumes[_] ++ #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } ++ parameters := {} ++ has_field(vols, "hostPath") ++ allowedPaths := get_allowed_paths(parameters) ++ input_hostpath_violation(allowedPaths, vols) ++} ++ ++#function for all KINDs ++has_field(object, field) = true { ++ object[field] ++} ++ ++#now allowed paths are null, this function will run## ++get_allowed_paths(params) = out { ++ not params.allowedHostPath == "undefined" ++ out = [] ++} ++ ++input_hostpath_violation(allowedPaths, volume) { ++ allowedPaths == [] ++} ++ ++### below functions are for violation when user has specified the hostPath, for testing uncomment the parameter array of objects at top#### ++ ++get_allowed_paths(params) = out { ++ out = params.allowedHostPath ++} ++ ++input_hostpath_violation(allowedPaths, volume) { ++ not input_hostpath_allowed(allowedPaths, volume) ++} ++ ++input_hostpath_allowed(allowedPaths, volume) { ++ allowedHostPath := allowedPaths[_] ++ path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) ++ not allowedHostPath.readOnly == true ++} ++ ++input_hostpath_allowed(allowedPaths, volume) { ++ allowedHostPath := allowedPaths[_] ++ path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) ++ allowedHostPath.readOnly ++ not writeable_input_volume_mounts(volume.name) ++} ++ ++writeable_input_volume_mounts(volume_name) { ++ containers := input.kubernetes_pod[_].config.spec.containers[_] ++ mount := containers.volumeMounts[_] ++ mount.name == volume_name ++ not mount.readOnly ++} ++ ++path_matches(prefix, path) { ++ a := split(trim(prefix, "/"), "/") ++ b := split(trim(path, "/"), "/") ++ prefix_matches(a, b) ++} ++ ++prefix_matches(a, b) { ++ count(a) <= count(b) ++ not any_not_equal_upto(a, b, count(a)) ++} ++ ++any_not_equal_upto(a, b, n) { ++ a[i] != b[i] ++ i < n ++} +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego +new file mode 100755 +index 0000000..d8d6e62 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego +@@ -0,0 +1,126 @@ ++package accurics ++ ++#rule for pod_security_policy ++{{.prefix}}{{.name}}{{.suffix}}[psp.id] { ++ psp := input.kubernetes_pod_security_policy[_] ++ psp.config.spec.allowProcMountTypes != "Default" ++} ++ ++#rule for pod_security_policy terraform ++{{.prefix}}{{.name}}{{.suffix}}[psp.id] { ++ psp := input.kubernetes_pod_security_policy[_] ++ psp.config.spec.allow_proc_mount_types != "Default" ++} ++ ++#rule for pod ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ parameters := {} ++ container := pod.config.spec.containers[_] ++ container.securityContext.procMount ++ allowedProcMount := get_allowed_proc_mount(parameters) ++ not input_proc_mount_type_allowed(allowedProcMount, container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ parameters := {} ++ container := pod.config.spec.initContainers[_] ++ container.securityContext.procMount ++ allowedProcMount := get_allowed_proc_mount(parameters) ++ not input_proc_mount_type_allowed(allowedProcMount, container) ++} ++ ++#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.containers[_] ++ #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } ++ parameters := {} ++ container.securityContext.procMount ++ allowedProcMount := get_allowed_proc_mount(parameters) ++ not input_proc_mount_type_allowed(allowedProcMount, container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.initContainers[_] ++ #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } ++ parameters := {} ++ container.securityContext.procMount ++ allowedProcMount := get_allowed_proc_mount(parameters) ++ not input_proc_mount_type_allowed(allowedProcMount, container) ++} ++ ++#rule for cron_job ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] ++ #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } ++ parameters := {} ++ container.securityContext.procMount ++ allowedProcMount := get_allowed_proc_mount(parameters) ++ not input_proc_mount_type_allowed(allowedProcMount, container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] ++ #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } ++ parameters := {} ++ container.securityContext.procMount ++ allowedProcMount := get_allowed_proc_mount(parameters) ++ not input_proc_mount_type_allowed(allowedProcMount, container) ++} ++ ++###this will get satisfied as no parameters are provided, thus checking with the baseline configuration which is checking that the procmount is default#### ++get_allowed_proc_mount(params) = out { ++ not params.procMount ++ out = "default" ++} ++ ++get_allowed_proc_mount(params) = out { ++ not valid_proc_mount(params.procMount) ++ out = "default" ++} ++ ++get_allowed_proc_mount(params) = out { ++ out = lower(params.procMount) ++} ++ ++valid_proc_mount(str) { ++ lower(str) == "default" ++} ++ ++valid_proc_mount(str) { ++ lower(str) == "unmasked" ++} ++ ++input_proc_mount_type_allowed(allowedProcMount, c) { ++ allowedProcMount == "default" ++ lower(c.securityContext.procMount) == "default" ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego +new file mode 100755 +index 0000000..cef0f21 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego +@@ -0,0 +1,58 @@ ++package accurics ++ ++####fixed the minimum set of allowed volumes, this may change as per the user#### ++ ++#rule for pod_security_policy ++{{.prefix}}{{.name}}{{.suffix}}[psp.id] { ++ psp := input.kubernetes_pod_security_policy[_] ++ secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] ++ volume_field := psp.config.spec.volumes[_] ++ not input_volume_type_allowed(volume_field, secure_volumes) ++} ++ ++#rule for pod ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] ++ volume_fields := {x | pod.config.spec.volumes[_][x]; x != "name"} ++ field := volume_fields[_] ++ not input_volume_type_allowed(field, secure_volumes) ++} ++ ++#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] ++ volume_fields := {x | kind.config.spec.template.spec.volumes[_][x]; x != "name"} ++ field := volume_fields[_] ++ not input_volume_type_allowed(field, secure_volumes) ++} ++ ++#rule for cron_job ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] ++ volume_fields := {x | cron_job.config.spec.jobTemplate.spec.template.spec.volumes[_][x]; x != "name"} ++ field := volume_fields[_] ++ not input_volume_type_allowed(field, secure_volumes) ++} ++ ++input_volume_type_allowed(field, secure_volumes) { ++ secure_volumes[_] == "*" ++} ++ ++input_volume_type_allowed(field, secure_volumes) { ++ field == secure_volumes[_] ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json +new file mode 100755 +index 0000000..3a5ba1b +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_cron_job", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.73", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json +new file mode 100755 +index 0000000..860d3a3 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_daemonset", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.74", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json +new file mode 100755 +index 0000000..a873dc5 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_deployment", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.75", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json +new file mode 100755 +index 0000000..497e12d +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_job", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.76", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json +new file mode 100755 +index 0000000..72f9f69 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_pod", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.77", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json +new file mode 100755 +index 0000000..2ec1d9e +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_replicaset", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.78", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json +new file mode 100755 +index 0000000..0becff4 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_replication_controller", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.79", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json +new file mode 100755 +index 0000000..6c54f99 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_stateful_set", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.80", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json +new file mode 100755 +index 0000000..8e8aebd +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_cron_job", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.81", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json +new file mode 100755 +index 0000000..9d06ffb +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_daemonset", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.82", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json +new file mode 100755 +index 0000000..0259e02 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_deployment", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.83", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json +new file mode 100755 +index 0000000..646842e +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_job", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.84", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json +new file mode 100755 +index 0000000..59f98d1 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_pod", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.85", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json +new file mode 100755 +index 0000000..32ae92f +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_replicaset", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.86", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json +new file mode 100755 +index 0000000..b82fb11 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_replication_controller", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.87", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json +new file mode 100755 +index 0000000..33f7d06 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerHasAllowedCapabilities", ++ "file": "containerHasAllowedCapabilities.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerHasAllowedCapabilities", ++ "prefix": "", ++ "resource_type": "kubernetes_stateful_set", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Pod has extra capabilities allowed", ++ "reference_id": "accurics.kubernetes.IAM.88", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego +new file mode 100644 +index 0000000..bfe42d7 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego +@@ -0,0 +1,119 @@ ++package accurics ++ ++# Checks if any extra capabilities are added ++{{.prefix}}{{.name}}{{.suffix}}[api.id] { ++ {{- template "spec" . }} ++ count(spec.allowedCapabilities) > 0 ++} ++ ++# Note, no TF-equivalent ++ ++{{- if eq .is_init true}} ++ ++{{.prefix}}{{.name}}{{.suffix}}[api.id] { ++ {{- template "initContainersSecurityContext" .}} ++ count(initContainersSecurityContext.capabilities.add) > 0 ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[api.id] { ++ {{- template "initContainersSecurityContextTF" .}} ++ count(initContainersSecurityContextTF.capabilities.add) > 0 ++} ++ ++{{- else }} ++ ++{{.prefix}}{{.name}}{{.suffix}}[api.id] { ++ {{- template "containersSecurityContext" .}} ++ count(containersSecurityContext.capabilities.add) > 0 ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[api.id] { ++ {{- template "containersSecurityContextTF" .}} ++ count(containersSecurityContextTF.capabilities.add) > 0 ++} ++ ++{{- end }} ++ ++ ++################################## ++### Template definitions below ### ++################################## ++{{- define "api" }} ++ api = input.{{.resource_type}}[_] ++{{- end}} ++ ++# resolves path to the spec key ++{{- define "spec" }} ++ {{- template "api" . }} ++ {{- if eq .resource_type "kubernetes_pod" }} ++ spec = api.config.spec ++ {{- else if eq .resource_type "kubernetes_pod_security_policy" }} ++ spec = api.config.spec ++ {{- else if eq .resource_type "kubernetes_cron_job" }} ++ spec = api.config.spec.jobTemplate.spec.template.spec ++ {{- else }} ++ spec = api.config.spec.template.spec ++ {{- end }} ++{{- end }} ++ ++# resolves path to the spec key for terraform-defined k8s resources ++{{- define "specTF" }} ++ {{- template "api" . }} ++ {{- if eq .resource_type "kubernetes_pod" }} ++ specTF = api.config.spec ++ {{- else if eq .resource_type "kubernetes_pod_security_policy" }} ++ specTF = api.config.spec ++ {{- else if eq .resource_type "kubernetes_cron_job" }} ++ specTF = api.config.spec.job_template.spec.template.spec ++ {{- else }} ++ specTF = api.config.spec.template.spec ++ {{- end }} ++{{- end }} ++ ++# resolves path to the containers list ++{{- define "containers" }} ++ {{- template "spec" . }} ++ containers = spec.containers[_] ++{{- end }} ++ ++# resolves path to the containers' security context ++{{- define "containersSecurityContext" }} ++ {{- template "containers" . }} ++ containersSecurityContext = containers.securityContext ++{{- end }} ++ ++# resolves path to the containers list for terraform-defined k8s resources ++{{- define "containersTF" }} ++ {{- template "specTF" . }} ++ containersTF = specTF.containers[_] ++{{- end }} ++ ++# resolves path to the containers' security context for terraform-defined k8s resources ++{{- define "containersSecurityContextTF" }} ++ {{- template "containersTF" . }} ++ containersSecurityContextTF = containersTF.security_context ++{{- end }} ++ ++# resolves path to the initContainers list ++{{- define "initContainers" }} ++ {{- template "spec" . }} ++ initContainers = spec.initContainers[_] ++{{- end }} ++ ++# resolves path to the initContainers' security context ++{{- define "initContainersSecurityContext" }} ++ {{- template "initContainers" . }} ++ initContainersSecurityContext = initContainers.securityContext ++{{- end }} ++ ++# resolves path to the initContainers list for terraform-defined k8s resources ++{{- define "initContainersTF" }} ++ {{- template "specTF" . }} ++ initContainersTF = specTF.init_containers[_] ++{{- end }} ++ ++# resolves path to the initContainers' security context for terraform-defined k8s resources ++{{- define "initContainersSecurityContextTF" }} ++ {{- template "initContainersTF" . }} ++ initContainersSecurityContextTF = initContainersTF.security_context ++{{- end }} +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego +new file mode 100755 +index 0000000..7be8687 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego +@@ -0,0 +1,108 @@ ++package accurics ++ ++#rule for pod security policy, will be valid for terraform pod_security_policy ++{{.prefix}}{{.name}}{{.suffix}}[psp.id] { ++ psp := input.kubernetes_pod_security_policy[_] ++ psp.config.metadata.annotations["apparmor.security.beta.kubernetes.io/defaultProfileName"] != "runtime/default" ++} ++ ++#rule for pod, covers containers ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.containers[_] ++ not input_apparmor_allowed(container.name, pod.config.metadata) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.initContainers[_] ++ not input_apparmor_allowed(container.name, pod.config.metadata) ++} ++ ++#terraform init_containers ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.init_containers[_] ++ not input_apparmor_allowed(container.name, pod.config.metadata) ++} ++ ++##rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.containers[_] ++ not input_apparmor_allowed(container.name, kind.config.spec.template.metadata) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.initContainers[_] ++ not input_apparmor_allowed(container.name, kind.config.spec.template.metadata) ++} ++ ++#terraform init_containers ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.init_containers[_] ++ not input_apparmor_allowed(container.name, kind.config.spec.template.metadata) ++} ++ ++#rule for cron_job, covers containers ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] ++ not input_apparmor_allowed(container.name, cron_job.config.spec.jobTemplate.spec.template.metadata) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] ++ not input_apparmor_allowed(container.name, cron_job.config.spec.jobTemplate.spec.template.metadata) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] ++ not input_apparmor_allowed(container.name, cron_job.config.spec.jobTemplate.spec.template.metadata) ++} ++ ++#function for all Kinds ++input_apparmor_allowed(containerName, metadata) { ++ metadata.annotations[key] == "runtime/default" ++ key == sprintf("container.apparmor.security.beta.kubernetes.io/%v", [containerName]) ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego +new file mode 100755 +index 0000000..248878b +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego +@@ -0,0 +1,33 @@ ++package accurics ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ pod.config.spec.automountServiceAccountToken == true ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_service_account[_] ++ pod.config.automountServiceAccountToken == true ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ pod.config.spec.template.spec.automountServiceAccountToken == true ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ pod.config.spec.jobTemplate.spec.template.spec.automountServiceAccountToken == true ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego +new file mode 100755 +index 0000000..d8d1ae4 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego +@@ -0,0 +1,69 @@ ++package accurics ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.containers[_] ++ container.securityContext.capabilities.add == "-SYS_ADMIN" ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ initcontainer := pod.config.spec.initContainers[_] ++ initcontainer.securityContext.capabilities.add == "-SYS_ADMIN" ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] ++ container.securityContext.capabilities.add == "-SYS_ADMIN" ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] ++ initcontainer.securityContext.capabilities.add == "-SYS_ADMIN" ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ containerCheck(pod.config.spec.template.spec) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ initContainerCheck(pod.config.spec.template.spec) ++} ++ ++initContainerCheck(spec) { ++ container := spec.initContainers[_] ++ container.securityContext.capabilities.add == "-SYS_ADMIN" ++} ++ ++containerCheck(spec) { ++ container := spec.containers[_] ++ container.securityContext.capabilities.add == "-SYS_ADMIN" ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego +new file mode 100755 +index 0000000..9920b38 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego +@@ -0,0 +1,74 @@ ++package accurics ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod_security_policy[_] ++ pod.config.spec.{{.attribute}} != [] ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.containers[_] ++ container.{{.attribute}} != [] ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ initcontainer := pod.config.spec.initContainers[_] ++ initcontainer.{{.attribute}} != [] ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] ++ container.{{.attribute}} != [] ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] ++ initcontainer.{{.attribute}} != [] ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ containerCheck(pod.config.spec.template.spec) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ initContainerCheck(pod.config.spec.template.spec) ++} ++ ++initContainerCheck(spec) { ++ container := spec.initContainers[_] ++ container.{{.attribute}} != [] ++} ++ ++containerCheck(spec) { ++ container := spec.containers[_] ++ container.{{.attribute}} != [] ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego +new file mode 100755 +index 0000000..07c315a +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego +@@ -0,0 +1,19 @@ ++package accurics ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod_kubeapi.id] { ++ pod_kubeapi := input.kubernetes_pod[_] ++ cmds := pod_kubeapi.config.spec.containers[_].command ++ {{.negation}} check(cmds) ++} ++ ++check(cmds) { ++ cmd := cmds[_] ++ startswith(cmd, "{{.argument}}") ++ {{.presence}} contains(cmd, "{{.param}}") ++ {{.optional}} ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ cmds := pod.config.spec.containers[_].imagePullPolicy != "Always" ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json +new file mode 100755 +index 0000000..56545ab +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_cron_job", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.105", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json +new file mode 100755 +index 0000000..4d97801 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_daemonset", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.106", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json +new file mode 100755 +index 0000000..ce27d4b +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_job", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.108", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json +new file mode 100755 +index 0000000..b740200 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_pod", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.109", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json +new file mode 100755 +index 0000000..19489a0 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_replicaset", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.110", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json +new file mode 100755 +index 0000000..d4a8f55 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_replication_controller", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.111", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json +new file mode 100755 +index 0000000..a18dc1c +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_stateful_set", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.112", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json +new file mode 100755 +index 0000000..d1cdd09 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_cron_job", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.113", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json +new file mode 100755 +index 0000000..64b9de3 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_daemonset", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.114", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json +new file mode 100755 +index 0000000..2f573f8 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_deployment", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.115", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json +new file mode 100755 +index 0000000..0f80098 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_job", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.116", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json +new file mode 100755 +index 0000000..9e420b8 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_pod", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.117", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json +new file mode 100755 +index 0000000..17d4116 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_replicaset", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.118", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json +new file mode 100755 +index 0000000..076a492 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_replication_controller", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.119", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json +new file mode 100755 +index 0000000..e5c922b +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerResourcesNotDefined", ++ "file": "containerResourcesNotDefined.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerResourcesNotDefined", ++ "prefix": "", ++ "resource_type": "kubernetes_stateful_set", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Container does not have resource limitations defined", ++ "reference_id": "accurics.kubernetes.IAM.120", ++ "category": "Identity and Access Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego +new file mode 100644 +index 0000000..b073034 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego +@@ -0,0 +1,111 @@ ++package accurics ++ ++{{- if eq .is_init true}} ++ ++{{.prefix}}{{.name}}{{.suffix}}[api.id] { ++ {{- template "initContainers" . }} ++ not initContainers.resources ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[api.id] { ++ {{- template "initContainersTF" . }} ++ not initContainersTF.resources ++} ++ ++{{- else }} ++ ++{{.prefix}}{{.name}}{{.suffix}}[api.id] { ++ {{- template "containers" . }} ++ not containers.resources ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[api.id] { ++ {{- template "containersTF" . }} ++ not containersTF.resources ++} ++ ++{{- end }} ++ ++ ++################################## ++### Template definitions below ### ++################################## ++{{- define "api" }} ++ api = input.{{.resource_type}}[_] ++{{- end}} ++ ++# resolves path to the spec key ++{{- define "spec" }} ++ {{- template "api" . }} ++ {{- if eq .resource_type "kubernetes_pod" }} ++ spec = api.config.spec ++ {{- else if eq .resource_type "kubernetes_pod_security_policy" }} ++ spec = api.config.spec ++ {{- else if eq .resource_type "kubernetes_cron_job" }} ++ spec = api.config.spec.jobTemplate.spec.template.spec ++ {{- else }} ++ spec = api.config.spec.template.spec ++ {{- end }} ++{{- end }} ++ ++# resolves path to the spec key for terraform-defined k8s resources ++{{- define "specTF" }} ++ {{- template "api" . }} ++ {{- if eq .resource_type "kubernetes_pod" }} ++ specTF = api.config.spec ++ {{- else if eq .resource_type "kubernetes_pod_security_policy" }} ++ specTF = api.config.spec ++ {{- else if eq .resource_type "kubernetes_cron_job" }} ++ specTF = api.config.spec.job_template.spec.template.spec ++ {{- else }} ++ specTF = api.config.spec.template.spec ++ {{- end }} ++{{- end }} ++ ++# resolves path to the containers list ++{{- define "containers" }} ++ {{- template "spec" . }} ++ containers = spec.containers[_] ++{{- end }} ++ ++# resolves path to the containers' security context ++{{- define "containersSecurityContext" }} ++ {{- template "containers" . }} ++ containersSecurityContext = containers.securityContext ++{{- end }} ++ ++# resolves path to the containers list for terraform-defined k8s resources ++{{- define "containersTF" }} ++ {{- template "specTF" . }} ++ containersTF = specTF.containers[_] ++{{- end }} ++ ++# resolves path to the containers' security context for terraform-defined k8s resources ++{{- define "containersSecurityContextTF" }} ++ {{- template "containersTF" . }} ++ containersSecurityContextTF = containersTF.security_context ++{{- end }} ++ ++# resolves path to the initContainers list ++{{- define "initContainers" }} ++ {{- template "spec" . }} ++ initContainers = spec.initContainers[_] ++{{- end }} ++ ++# resolves path to the initContainers' security context ++{{- define "initContainersSecurityContext" }} ++ {{- template "initContainers" . }} ++ initContainersSecurityContext = initContainers.securityContext ++{{- end }} ++ ++# resolves path to the initContainers list for terraform-defined k8s resources ++{{- define "initContainersTF" }} ++ {{- template "specTF" . }} ++ initContainersTF = specTF.init_containers[_] ++{{- end }} ++ ++# resolves path to the initContainers' security context for terraform-defined k8s resources ++{{- define "initContainersSecurityContextTF" }} ++ {{- template "initContainersTF" . }} ++ initContainersSecurityContextTF = initContainersTF.security_context ++{{- end }} +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json +new file mode 100755 +index 0000000..7962ef7 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_cron_job", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.57", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json +new file mode 100755 +index 0000000..f8f70ea +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_daemonset", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.58", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json +new file mode 100755 +index 0000000..739c621 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_deployment", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.59", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json +new file mode 100755 +index 0000000..f8a42a8 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_job", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.60", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json +new file mode 100755 +index 0000000..fc7091e +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_pod", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.61", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json +new file mode 100755 +index 0000000..3fc461f +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_replicaset", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.62", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json +new file mode 100755 +index 0000000..59662b3 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_replication_controller", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.63", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json +new file mode 100755 +index 0000000..13cb5dc +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": false, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_stateful_set", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.64", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json +new file mode 100755 +index 0000000..00ec55c +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_cron_job", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.65", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json +new file mode 100755 +index 0000000..0f4a3d5 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_daemonset", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.66", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json +new file mode 100755 +index 0000000..42cd868 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_deployment", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.67", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json +new file mode 100755 +index 0000000..d023923 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_job", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.68", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json +new file mode 100755 +index 0000000..c522de8 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_pod", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.69", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json +new file mode 100755 +index 0000000..45c9cb9 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_replicaset", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.70", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json +new file mode 100755 +index 0000000..4bbbdd2 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_replication_controller", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.71", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json +new file mode 100755 +index 0000000..88fc00e +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json +@@ -0,0 +1,16 @@ ++{ ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "file": "containerUsesSecretsInEnvironmentVar.rego", ++ "template_args": { ++ "is_init": true, ++ "name": "containerUsesSecretsInEnvironmentVar", ++ "prefix": "", ++ "resource_type": "kubernetes_stateful_set", ++ "suffix": "" ++ }, ++ "severity": "HIGH", ++ "description": "Container uses secrets in environment variables", ++ "reference_id": "accurics.kubernetes.EKM.72", ++ "category": "Encryption and Key Management", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego +new file mode 100644 +index 0000000..86c10e3 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego +@@ -0,0 +1,115 @@ ++package accurics ++ ++{{- if eq .is_init true}} ++ ++{{.prefix}}{{.name}}{{.suffix}}[api.id] { ++ {{- template "initContainers" .}} ++ envVars := initContainers.env[_] ++ envVars.valueFrom.secretKeyRef ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[api.id] { ++ {{- template "initContainersTF" .}} ++ envVars := initContainersTF.env[_] ++ envVars.valueFrom.secretKeyRef ++} ++ ++{{- else }} ++ ++{{.prefix}}{{.name}}{{.suffix}}[api.id] { ++ {{- template "containers" .}} ++ envVars := containers.env[_] ++ envVars.valueFrom.secretKeyRef ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[api.id] { ++ {{- template "containersTF" .}} ++ envVars := containersTF.env[_] ++ envVars.valueFrom.secretKeyRef ++} ++ ++{{- end }} ++ ++ ++################################## ++### Template definitions below ### ++################################## ++{{- define "api" }} ++ api = input.{{.resource_type}}[_] ++{{- end}} ++ ++# resolves path to the spec key ++{{- define "spec" }} ++ {{- template "api" . }} ++ {{- if eq .resource_type "kubernetes_pod" }} ++ spec = api.config.spec ++ {{- else if eq .resource_type "kubernetes_pod_security_policy" }} ++ spec = api.config.spec ++ {{- else if eq .resource_type "kubernetes_cron_job" }} ++ spec = api.config.spec.jobTemplate.spec.template.spec ++ {{- else }} ++ spec = api.config.spec.template.spec ++ {{- end }} ++{{- end }} ++ ++# resolves path to the spec key for terraform-defined k8s resources ++{{- define "specTF" }} ++ {{- template "api" . }} ++ {{- if eq .resource_type "kubernetes_pod" }} ++ specTF = api.config.spec ++ {{- else if eq .resource_type "kubernetes_pod_security_policy" }} ++ specTF = api.config.spec ++ {{- else if eq .resource_type "kubernetes_cron_job" }} ++ specTF = api.config.spec.job_template.spec.template.spec ++ {{- else }} ++ specTF = api.config.spec.template.spec ++ {{- end }} ++{{- end }} ++ ++# resolves path to the containers list ++{{- define "containers" }} ++ {{- template "spec" . }} ++ containers = spec.containers[_] ++{{- end }} ++ ++# resolves path to the containers' security context ++{{- define "containersSecurityContext" }} ++ {{- template "containers" . }} ++ containersSecurityContext = containers.securityContext ++{{- end }} ++ ++# resolves path to the containers list for terraform-defined k8s resources ++{{- define "containersTF" }} ++ {{- template "specTF" . }} ++ containersTF = specTF.containers[_] ++{{- end }} ++ ++# resolves path to the containers' security context for terraform-defined k8s resources ++{{- define "containersSecurityContextTF" }} ++ {{- template "containersTF" . }} ++ containersSecurityContextTF = containersTF.security_context ++{{- end }} ++ ++# resolves path to the initContainers list ++{{- define "initContainers" }} ++ {{- template "spec" . }} ++ initContainers = spec.initContainers[_] ++{{- end }} ++ ++# resolves path to the initContainers' security context ++{{- define "initContainersSecurityContext" }} ++ {{- template "initContainers" . }} ++ initContainersSecurityContext = initContainers.securityContext ++{{- end }} ++ ++# resolves path to the initContainers list for terraform-defined k8s resources ++{{- define "initContainersTF" }} ++ {{- template "specTF" . }} ++ initContainersTF = specTF.init_containers[_] ++{{- end }} ++ ++# resolves path to the initContainers' security context for terraform-defined k8s resources ++{{- define "initContainersSecurityContextTF" }} ++ {{- template "initContainersTF" . }} ++ initContainersSecurityContextTF = initContainersTF.security_context ++{{- end }} +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego +new file mode 100755 +index 0000000..6d6e8e6 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego +@@ -0,0 +1,102 @@ ++package accurics ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ pod.config.spec.securityContext.runAsUser < 1000 ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.containers[_] ++ container.securityContext.runAsUser < 1000 ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ initcontainer := pod.config.spec.initContainers[_] ++ initcontainer.securityContext.runAsUser < 1000 ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ pod.config.spec.jobTemplate.spec.template.spec.securityContext.runAsUser < 1000 ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] ++ container.securityContext.runAsUser < 1000 ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] ++ initcontainer.securityContext.runAsUser < 1000 ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod_security_policy[_] ++ ranges := pod.config.spec.runAsUser.ranges[_] ++ ranges.min < 1000 ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ pod.config.spec.template.spec.securityContext.runAsUser < 1000 ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ checkContainer(pod.config.spec.template.spec) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ checkInitContainer(pod.config.spec.template.spec) ++} ++ ++checkInitContainer(spec) { ++ containers := spec.initContainers[_] ++ containers.securityContext.runAsUser < 1000 ++} ++ ++checkContainer(spec) { ++ containers := spec.containers[_] ++ containers.securityContext.runAsUser < 1000 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego +new file mode 100755 +index 0000000..f918cc2 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego +@@ -0,0 +1,51 @@ ++### this pollicy depends on the parameters specified by the user/client. Here we are considering that no kernel level syscalls are allowed### ++package accurics ++ ++#rule for pod ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ forbiddenSysctls = ["kernel.*"] ++ sysctl := pod.config.spec.securityContext.sysctls[_].name ++ forbidden_sysctl(sysctl, forbiddenSysctls) ++} ++ ++##rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ forbiddenSysctls = ["kernel.*"] ++ sysctl := kind.config.spec.template.spec.securityContext.sysctls[_].name ++ forbidden_sysctl(sysctl, forbiddenSysctls) ++} ++ ++#rule for cron_job ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ forbiddenSysctls = ["kernel.*"] ++ sysctl := cron_job.config.spec.jobTemplate.spec.template.spec.securityContext.sysctls[_].name ++ forbidden_sysctl(sysctl, forbiddenSysctls) ++} ++ ++# if all syscalls are forbidden ++forbidden_sysctl(sysctl, arg) { ++ arg[_] == "*" ++} ++ ++forbidden_sysctl(sysctl, arg) { ++ arg[_] == sysctl ++} ++ ++forbidden_sysctl(sysctl, arg) { ++ startswith(sysctl, trim(arg[_], "*")) ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json +new file mode 100644 +index 0000000..10000a6 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json +@@ -0,0 +1,14 @@ ++{ ++ "name": "disAllowedVolumes", ++ "file": "disAllowedVolumes.rego", ++ "template_args": { ++ "name": "disAllowedVolumes", ++ "prefix": "", ++ "suffix": "" ++ }, ++ "severity": "MEDIUM", ++ "description": "Vulnerable to CVE-2020-8555 (affected version of kube-controller-manager: v1.18.0, v1.17.0 - v1.17.4, v1.16.0 - v1.16.8,< v1.15.11", ++ "reference_id": "AC-K8-DS-PO-M-0143", ++ "category": "Data Security", ++ "version": 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego +new file mode 100644 +index 0000000..706bbaf +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego +@@ -0,0 +1,52 @@ ++package accurics ++ ++#rule for pod_security_policy ++{{.prefix}}{{.name}}{{.suffix}}[psp.id] { ++ psp := input.kubernetes_pod_security_policy[_] ++ affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] ++ volume_type := psp.config.spec.volumes[_] ++ volNotAllowed(volume_type, affected_volumes) ++} ++ ++#rule for pod ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] ++ volume_types := {x | pod.config.spec.volumes[_][x]; x != "name"} ++ vol:= volume_types[_] ++ volNotAllowed(vol, affected_volumes) ++} ++ ++#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] ++ volume_types := {x | kind.config.spec.template.spec.volumes[_][x]; x != "name"} ++ vol:= volume_types[_] ++ volNotAllowed(vol, affected_volumes) ++} ++ ++#rule for cron_job ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] ++ volume_types := {x | cron_job.config.spec.jobTemplate.spec.template.spec.volumes[_][x]; x != "name"} ++ vol:= volume_types[_] ++ volNotAllowed(vol, affected_volumes) ++} ++ ++volNotAllowed(field, affected_volumes) { ++ field == affected_volumes[_] ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego +new file mode 100755 +index 0000000..890439e +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego +@@ -0,0 +1,35 @@ ++package accurics ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ vol := pod.config.spec.jobTemplate.spec.template.spec.volumes[_] ++ socketPathCheck(vol.hostPath.path) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ vol := pod.config.spec.volumes[_] ++ socketPathCheck(vol.hostPath.path) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ vol := pod.config.spec.template.spec.volumes[_] ++ socketPathCheck(vol.hostPath.path) ++} ++ ++socketPathCheck(attrib) { ++ contains(attrib, "/var/run/docker") ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego +new file mode 100755 +index 0000000..de75e61 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego +@@ -0,0 +1,196 @@ ++package accurics ++ ++#rule for pod, covers containers, initContainers, terraform, init_containers ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.containers[_] ++ checkForPodLatest(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.containers[_] ++ checkForPodNoTag(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.initContainers[_] ++ checkForPodLatest(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.initContainers[_] ++ checkForPodNoTag(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.init_containers[_] ++ checkForPodLatest(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.init_containers[_] ++ checkForPodNoTag(container) ++} ++ ++#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers, initContainers, terraform init_containers ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.containers[_] ++ checkForPodLatest(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.containers[_] ++ checkForPodNoTag(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.initContainers[_] ++ checkForPodLatest(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.init_containers[_] ++ checkForPodLatest(container) ++ } ++ ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.initContainers[_] ++ checkForPodNoTag(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.init_containers[_] ++ checkForPodNoTag(container) ++} ++ ++#rule for cron_job, covers containers, initContainers, terraform init_containers ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] ++ checkForPodLatest(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] ++ checkForPodNoTag(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] ++ checkForPodLatest(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] ++ checkForPodLatest(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] ++ checkForPodNoTag(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] ++ checkForPodNoTag(container) ++} ++ ++#check function for All KINDs ++checkForPodLatest(arg) { ++ img_split := split(arg.image, ":") ++ tag := img_split[count(img_split) - 1] ++ tag == "latest" ++} ++ ++checkForPodNoTag(argument) { ++ img_split := split(argument.image, ":") ++ count(img_split) == 1 ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego +new file mode 100755 +index 0000000..6cf92e6 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego +@@ -0,0 +1,105 @@ ++package accurics ++ ++#rule for pod, same will satisfy terraform pod, covers containers, initContainers, and terraform init_containers ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.containers[_] ++ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] ++ not all(satisfied) ++} ++ ++#rule for init containers ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.initContainers[_] ++ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] ++ not all(satisfied) ++} ++ ++#rule for terraform init_containers ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.init_containers[_] ++ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] ++ not all(satisfied) ++} ++ ++#rule for deployment, daemonset, job, replica_set, replication_controller, stateful_set covers containers, initContainers, terraform init_containers ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.containers[_] ++ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] ++ not all(satisfied) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.initContainers[_] ++ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] ++ not all(satisfied) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.init_containers[_] ++ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] ++ not all(satisfied) ++} ++ ++#rule for cron_job, covers containers, initContainers, terraform init_containers ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] ++ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] ++ not all(satisfied) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] ++ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] ++ not all(satisfied) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] ++ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] ++ not all(satisfied) ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego +new file mode 100755 +index 0000000..d9746d1 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego +@@ -0,0 +1,6 @@ ++package accurics ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ pod.config.metadata.labels.app == "kubernetes-dashboard" ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego +new file mode 100755 +index 0000000..7ce3427 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego +@@ -0,0 +1,20 @@ ++package accurics ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_cron_job", "undefined"), ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_pod", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ pod.config.metadata.namespace == "default" ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego +new file mode 100755 +index 0000000..148651e +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego +@@ -0,0 +1,11 @@ ++package accurics ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ pod.config.spec.privileged == true ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod_security_policy[_] ++ pod.config.spec.privileged == true ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego +new file mode 100755 +index 0000000..19eaf1a +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego +@@ -0,0 +1,68 @@ ++#liveenessprobe and readinessprobe are not applicable for init containers. ++package accurics ++ ++#rule for pod ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.containers[_] ++ not container["{{.argument}}"] ++} ++ ++#rule for pod terraform ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.containers[_] ++ not container["{{.argumentTF}}"] ++} ++ ++#rule for deployment, daemonset, job, replica_Set, replication_controller, stateful_set ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.containers[_] ++ not container["{{.argument}}"] ++} ++ ++#rule for terraform deployment, daemonset, job, replica_Set, replication_controller, stateful_set ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.containers[_] ++ not container["{{.argumentTF}}"] ++} ++ ++#rule for cronjob ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] ++ not container["{{.argument}}"] ++} ++ ++#rule for terraform cronjob ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] ++ not container["{{.argumentTF}}"] ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego +new file mode 100755 +index 0000000..f4bd21a +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego +@@ -0,0 +1,153 @@ ++package accurics ++ ++#rule for pod, pod_security_policy covers containers ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_pod", "undefined"), ++ object.get(input, "kubernetes_pod_security_policy", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ not input_container_allowed(kind.config.metadata) ++} ++ ++#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ not input_container_allowed(kind.config.spec.template.metadata) ++} ++ ++#rule for cron_job ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ not input_container_allowed(cron_job.config.spec.jobTemplate.spec.template.metadata) ++} ++ ++input_container_allowed(metadata) { ++ metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"] == "runtime/default" ++} ++ ++input_container_allowed(metadata) { ++ metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"] == "docker/default" ++} ++ ++ ####Kubernetes v1.19 or later######## ++ ++#rule for pod covers containers and checks field seccompProfile at container security context which is found at spec.containers. ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.containers[_] ++ not check_seccomp(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.initContainers[_] ++ not check_seccomp(container) ++} ++ ++#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.containers[_] ++ not check_seccomp(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ container := kind.config.spec.template.spec.initContainers[_] ++ not check_seccomp(container) ++} ++ ++#rule for cron_job ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] ++ not check_seccomp(container) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] ++ not check_seccomp(container) ++} ++ ++##rule to check seccompProfile at PodSecurityContext which is found at PodSpec## ++ ++#rule for pod ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ not check_seccomp(pod.config.spec) ++} ++ ++#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers ++{{.prefix}}{{.name}}{{.suffix}}[kind.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ kind := item[_] ++ not check_seccomp(kind.config.spec.template.spec) ++} ++ ++#rule for cron_job ++{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { ++ cron_job := input.kubernetes_cron_job[_] ++ not check_seccomp(cron_job.config.spec.jobTemplate.spec.template.spec) ++} ++ ++#function for all Kinds and scenarios ++check_seccomp(container) { ++ container.securityContext.seccompProfile.type == "RuntimeDefault" ++} ++ ++check_seccomp(container) { ++ container.securityContext.seccompProfile.type == "DockerDefault" ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego +new file mode 100755 +index 0000000..780bb45 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego +@@ -0,0 +1,75 @@ ++package accurics ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ containers := pod.config.spec.containers[_] ++ env := containers.env[_] ++ env.valueFrom != [] ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ initcontainer := pod.config.spec.initContainers[_] ++ env := initcontainer.env[_] ++ env.valueFrom != [] ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ containers := pod.config.spec.jobTemplate.spec.template.spec.containers[_] ++ env := containers.env[_] ++ env.valueFrom != [] ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] ++ env := initcontainer.env[_] ++ env.valueFrom != [] ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ checkContainer(pod.config.spec.template.spec) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ checkInitContainer(pod.config.spec.template.spec) ++} ++ ++checkInitContainer(spec) { ++ containers := spec.initContainers[_] ++ env := containers.env[_] ++ env.valueFrom != [] ++} ++ ++checkContainer(spec) { ++ containers := spec.containers[_] ++ env := containers.env[_] ++ env.valueFrom != [] ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego +new file mode 100755 +index 0000000..f76c539 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego +@@ -0,0 +1,76 @@ ++package accurics ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ checkCorrectAttribute(pod.config.spec.jobTemplate.spec.template.spec) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ checkCorrectAttribute(pod.config.spec.template.spec) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ checkCorrectAttribute(pod.config.spec) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod_security_policy[_] ++ podSecurityCheck(pod.config.spec) ++} ++ ++checkCorrectAttribute(spec) { ++ container := spec.containers[_] ++ containerSecurityCheck(container) ++} ++ ++checkCorrectAttribute(spec) { ++ container := spec.initContainers[_] ++ containerSecurityCheck(container) ++} ++ ++checkCorrectAttribute(spec) { ++ secContext := spec.securityContext ++ podSecurityCheck(secContext) ++} ++ ++containerSecurityCheck(container) { ++ {{.not_allowed}} ++ container.{{.param1}}.{{.param}} == {{.value}} ++} ++ ++containerSecurityCheck(container) { ++ object.get(container, "{{.param1}}", "undefined") == "undefined" ++} ++ ++containerSecurityCheck(container) { ++ not container.{{.param1}}.{{.param}} ++} ++ ++containerSecurityCheck(container) { ++ {{.allowed}} ++ not container.{{.param1}}.{{.arg1}}.{{.arg2}} ++} ++ ++podSecurityCheck(secContext) { ++ {{.not_allowed}} ++ secContext.{{.param}} == {{.value}} ++} ++ ++podSecurityCheck(secContext) { ++ {{.not_allowed}} ++ object.get(secContext, "{{.param}}", "undefined") == "undefined" ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego +new file mode 100755 +index 0000000..5d24387 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego +@@ -0,0 +1,103 @@ ++package accurics ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ container := pod.config.spec.containers[_] ++ not container.securityContext ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ initcontainer := pod.config.spec.initContainers[_] ++ not initcontainer.securityContext ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ not pod.config.spec.securityContext ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] ++ not container.securityContext ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] ++ not initcontainer.securityContext ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ not pod.config.spec.jobTemplate.spec.template.spec.securityContext ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined"), ++ object.get(input, "kubernetes_cron_job", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ checkPod(pod) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined"), ++ object.get(input, "kubernetes_cron_job", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ checkInitContainer(pod.config.spec.template.spec) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined"), ++ object.get(input, "kubernetes_cron_job", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ checkContainer(pod.config.spec.template.spec) ++} ++ ++checkContainer(spec) { ++ containers := spec.containers[_] ++ not containers.securityContext ++} ++ ++checkInitContainer(spec) { ++ containers := spec.initContainers[_] ++ not containers.securityContext ++} ++ ++checkPod(pod) { ++ not pod.config.spec.template.spec.securityContext ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego +new file mode 100755 +index 0000000..5794487 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego +@@ -0,0 +1,36 @@ ++package accurics ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ attribute := pod.config.spec.jobTemplate.spec.template.spec ++ boolCheck(attribute) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ attribute := pod.config.spec ++ boolCheck(attribute) ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ attribute := pod.config.spec.template.spec ++ ++ boolCheck(attribute) ++} ++ ++boolCheck(attribute) { ++ attribute.{{.param}} == {{.value}} ++} +\ No newline at end of file +diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego +new file mode 100755 +index 0000000..f4b90f2 +--- /dev/null ++++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego +@@ -0,0 +1,35 @@ ++package accurics ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_pod[_] ++ pod.config.metadata.labels.app == "helm" ++ pod.config.metadata.labels.name == "tiller" ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ pod := input.kubernetes_cron_job[_] ++ pod.config.spec.jobTemplate.spec.template.metadata.labels.app == "helm" ++ pod.config.spec.jobTemplate.spec.template.metadata.labels.name == "tiller" ++} ++ ++{{.prefix}}{{.name}}{{.suffix}}[pod.id] { ++ item_list := [ ++ object.get(input, "kubernetes_daemonset", "undefined"), ++ object.get(input, "kubernetes_deployment", "undefined"), ++ object.get(input, "kubernetes_job", "undefined"), ++ object.get(input, "kubernetes_replica_set", "undefined"), ++ object.get(input, "kubernetes_replication_controller", "undefined"), ++ object.get(input, "kubernetes_stateful_set", "undefined") ++ ] ++ ++ item = item_list[_] ++ item != "undefined" ++ ++ pod := item[_] ++ checkPod(pod) ++} ++ ++checkPod(pod) { ++ pod.config.spec.template.metadata.labels.app == "helm" ++ pod.config.spec.template.metadata.labels.name == "tiller" ++} +\ No newline at end of file +diff --git a/pkg/http-server/routes.go b/pkg/http-server/routes.go +index 400e990..c7e86ae 100644 +--- a/pkg/http-server/routes.go ++++ b/pkg/http-server/routes.go +@@ -29,13 +29,19 @@ type Route struct { + + // Routes returns a slice of routes of API endpoints to be registered with + // http server +-func (g *APIServer) Routes() []*Route { +- h := NewAPIHandler() ++func (g *APIServer) Routes(configFile string) []*Route { ++ h := NewAPIHandler(configFile) + routes := []*Route{ + {verb: "GET", path: "/health", fn: h.Health}, + {verb: "POST", path: versionedPath("/{iac}/{iacVersion}/{cloud}/local/file/scan"), fn: h.scanFile}, + {verb: "POST", path: versionedPath("/{iac}/{iacVersion}/{cloud}/remote/dir/scan"), fn: h.scanRemoteRepo}, ++ ++ // K8s Webhook Routes ++ {verb: "GET", path: "/k8s/webhooks/{apiKey}/logs", fn: h.getLogs}, ++ {verb: "GET", path: "/k8s/webhooks/{apiKey}/logs/{uid}", fn: h.getLogByUID}, ++ {verb: "POST", path: versionedPath("/k8s/webhooks/{apiKey}/scan"), fn: h.scanIncomingWebhook}, + } ++ + return routes + } + +diff --git a/pkg/http-server/routes_test.go b/pkg/http-server/routes_test.go +index 06d3dad..3e20588 100644 +--- a/pkg/http-server/routes_test.go ++++ b/pkg/http-server/routes_test.go +@@ -8,7 +8,7 @@ func TestRoutes(t *testing.T) { + t.Run("health route check", func(t *testing.T) { + var ( + server = NewAPIServer() +- got = server.Routes() ++ got = server.Routes("") + passed = false + ) + +diff --git a/pkg/http-server/server.go b/pkg/http-server/server.go +index 68529d8..275bafc 100644 +--- a/pkg/http-server/server.go ++++ b/pkg/http-server/server.go +@@ -17,7 +17,8 @@ + package httpserver + + // APIServer struct for http api server +-type APIServer struct{} ++type APIServer struct{ ++} + + // NewAPIServer returns a new APIServer{} + func NewAPIServer() *APIServer { +diff --git a/pkg/http-server/start.go b/pkg/http-server/start.go +index 8075389..aa4c6bb 100644 +--- a/pkg/http-server/start.go ++++ b/pkg/http-server/start.go +@@ -28,19 +28,19 @@ import ( + ) + + // Start initializes api routes and starts http server +-func Start() { ++func Start(configFile string, certFile string, privateKeyFile string) { + // create a new API server + server := NewAPIServer() + + // get all routes +- routes := server.Routes() ++ routes := server.Routes(configFile) + + // register routes and start the http server +- server.start(routes) ++ server.start(routes, certFile, privateKeyFile) + } + + // start http server +-func (g *APIServer) start(routes []*Route) { ++func (g *APIServer) start(routes []*Route, certFile string, privateKeyFile string) { + + var ( + err error +@@ -56,19 +56,36 @@ func (g *APIServer) start(routes []*Route) { + router.Methods(v.verb).Path(v.path).HandlerFunc(v.fn) + } + ++ // Add a route for all static templates / assets. Currently used for the Webhook logs views ++ // go/terrascan/asset is the path where the assets files are located inside the docker container ++ router.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(http.Dir("/go/terrascan/assets")))) ++ ++ var port = GatewayDefaultPort ++ ++ // In case a certificate file is specified, we run the server with a different port ++ if certFile != "" { ++ port = TLSGatewayDefaultPort ++ } ++ + // start http server + server := &http.Server{ +- Addr: ":" + GatewayDefaultPort, ++ Addr: ":" + port, + Handler: router, + } + + go func() { +- err = server.ListenAndServe() ++ var err error ++ if certFile != "" { ++ // In case a certificate file is specified, the server support TLS ++ err = server.ListenAndServeTLS(certFile, privateKeyFile) ++ } else { ++ err = server.ListenAndServe() ++ } + if err != nil && err != http.ErrServerClosed { + logger.Fatal(err) + } + }() +- logger.Infof("http server listening at port %v", GatewayDefaultPort) ++ logger.Infof("http server listening at port %v", port) + + // Wait for interrupt signal to gracefully shutdown the server + quit := make(chan os.Signal, 1) +diff --git a/pkg/http-server/templates/index.html b/pkg/http-server/templates/index.html +new file mode 100644 +index 0000000..43b0bd9 +--- /dev/null ++++ b/pkg/http-server/templates/index.html +@@ -0,0 +1,34 @@ ++ ++ ++ ++ K8s Admission Review Logs ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ {{range .}} ++ ++ ++ ++ ++ ++ ++ {{end}} ++ ++
      TimeStatusRequestReasoning
      {{.CreatedAt}}{{.Status}}{{.Request}}{{.Reasoning}}
      ++ ++ ++ ++ ++ +diff --git a/pkg/http-server/templates/show.html b/pkg/http-server/templates/show.html +new file mode 100644 +index 0000000..7b36330 +--- /dev/null ++++ b/pkg/http-server/templates/show.html +@@ -0,0 +1,40 @@ ++ ++ ++ ++ K8s Admission Review Logs ++ ++ ++ ++ ++ ++
      ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++
      UID{{.UID}}
      Status{{.Status}}
      Request{{.Request}}
      Violations Summary{{.Violations}}
      Deniable Violations{{.DeniableViolations}}
      ++
      ++ ++ ++ ++ ++ +diff --git a/pkg/http-server/webhook-deny-rule-matcher.go b/pkg/http-server/webhook-deny-rule-matcher.go +new file mode 100644 +index 0000000..4becf0b +--- /dev/null ++++ b/pkg/http-server/webhook-deny-rule-matcher.go +@@ -0,0 +1,35 @@ ++package httpserver ++ ++import ( ++ "github.com/accurics/terrascan/pkg/config" ++ "github.com/accurics/terrascan/pkg/results" ++ "github.com/accurics/terrascan/pkg/utils" ++) ++ ++type webhookDenyRuleMatcher struct { ++} ++ ++// This class should check if one of the violations found is relevant for the specified K8s deny rules ++func (g *webhookDenyRuleMatcher) match(violation results.Violation, denyRules config.K8sDenyRules) bool { ++ if &denyRules == nil { ++ return false ++ } ++ ++ // Currently we support: ++ // 1. A minimum severity level ++ // 2. A category list ++ // In case one of the conditions is met, we return true. (We perform an OR between the rules) ++ if len(denyRules.DeniedSeverity) > 0 && utils.CheckSeverity(violation.Severity, denyRules.DeniedSeverity) { ++ return true ++ } ++ ++ if denyRules.Categories != nil { ++ for _, category := range denyRules.Categories { ++ if category == violation.Category { ++ return true ++ } ++ } ++ } ++ ++ return false ++} +diff --git a/pkg/http-server/webhook-deny-rule-matcher_test.go b/pkg/http-server/webhook-deny-rule-matcher_test.go +new file mode 100644 +index 0000000..8f0e165 +--- /dev/null ++++ b/pkg/http-server/webhook-deny-rule-matcher_test.go +@@ -0,0 +1,98 @@ ++package httpserver ++ ++import ( ++ "github.com/accurics/terrascan/pkg/config" ++ "github.com/accurics/terrascan/pkg/results" ++ "testing" ++) ++ ++func TestDenyRuleMatcher(t *testing.T) { ++ testMediumSeverity := "MEDIUM" ++ testCategory := "Identity and Access Management" ++ testRuleName := "My Amazing Rule" ++ ++ table := []struct { ++ name string ++ ruleSeverity string ++ ruleCategory string ++ ruleName string ++ k8sDenyRules config.K8sDenyRules ++ expectedResult bool ++ }{ ++ { ++ name: "no deny rules", ++ ruleSeverity: testMediumSeverity, ++ ruleCategory: testCategory, ++ ruleName: testRuleName, ++ expectedResult: false, ++ }, ++ { ++ name: "matched severity", ++ ruleSeverity: testMediumSeverity, ++ ruleCategory: testCategory, ++ ruleName: testRuleName, ++ k8sDenyRules: config.K8sDenyRules{ DeniedSeverity: testMediumSeverity }, ++ expectedResult: true, ++ }, ++ ++ { ++ name: "lower severity", ++ ruleSeverity: testMediumSeverity, ++ ruleCategory: testCategory, ++ ruleName: testRuleName, ++ k8sDenyRules: config.K8sDenyRules{ DeniedSeverity: "LOW" }, ++ expectedResult: true, ++ }, ++ { ++ name: "higher severity", ++ ruleSeverity: testMediumSeverity, ++ ruleCategory: testCategory, ++ ruleName: testRuleName, ++ k8sDenyRules: config.K8sDenyRules{ DeniedSeverity: "High" }, ++ expectedResult: false, ++ }, ++ { ++ name: "not matching category", ++ ruleSeverity: testMediumSeverity, ++ ruleCategory: testCategory, ++ ruleName: testRuleName, ++ k8sDenyRules: config.K8sDenyRules{ Categories: []string { "WRONG!" } }, ++ expectedResult: false, ++ }, ++ ++ { ++ name: "matching category", ++ ruleSeverity: testMediumSeverity, ++ ruleCategory: testCategory, ++ ruleName: testRuleName, ++ k8sDenyRules: config.K8sDenyRules{ Categories: []string { "WRONG!", testCategory } }, ++ expectedResult: true, ++ }, ++ { ++ name: "incorrect severity by matching category", ++ ruleSeverity: testMediumSeverity, ++ ruleCategory: testCategory, ++ ruleName: testRuleName, ++ k8sDenyRules: config.K8sDenyRules{ Categories: []string { "WRONG!", testCategory }, DeniedSeverity: "HIGH" }, ++ expectedResult: true, ++ }, ++ } ++ ++ var denyRuleMatcher = webhookDenyRuleMatcher{} ++ ++ for _, tt := range table { ++ t.Run(tt.name, func(t *testing.T) { ++ ++ violation := results.Violation{ ++ RuleName: tt.ruleName, ++ Severity: tt.ruleSeverity, ++ Category: tt.ruleCategory, ++ } ++ ++ result := denyRuleMatcher.match(violation, tt.k8sDenyRules) ++ if result != tt.expectedResult { ++ t.Errorf("Expected: %v, Got: %v", tt.expectedResult, result) ++ } ++ }) ++ } ++} +diff --git a/pkg/http-server/webhook-scan-logger.go b/pkg/http-server/webhook-scan-logger.go +new file mode 100644 +index 0000000..cbec83b +--- /dev/null ++++ b/pkg/http-server/webhook-scan-logger.go +@@ -0,0 +1,197 @@ ++package httpserver ++ ++import ( ++ "database/sql" ++ "go.uber.org/zap" ++ "os" ++ "time" ++) ++ ++type WebhookScanLogger struct { ++ test bool ++} ++ ++type webhookScanLog struct { ++ UID string ++ Request string ++ Allowed bool ++ ViolationsSummary string ++ DeniableViolations string ++ CreatedAt time.Time ++} ++ ++// The file name where the DB is stored. Currently we use an SQLite DB ++var dbFileName = "k8s-admission-review-logs.db" ++ ++func (g *WebhookScanLogger) log(webhookScanLog webhookScanLog) error { ++ // Insert a new Log record to the DB ++ ++ db, err := g.getDbHandler() ++ if err != nil { ++ return err ++ } ++ defer db.Close() ++ ++ insertLogSQL := `INSERT INTO logs(uid, request, allowed, violations_summary, deniable_violations, created_at) ++ VALUES (?, ?, ?, ?, ?, ?)` ++ ++ statement, err := db.Prepare(insertLogSQL) ++ if err != nil { ++ zap.S().Errorf("failed preparing SQL statement. error: '%v'", err) ++ return err ++ } ++ _, err = statement.Exec(webhookScanLog.UID, ++ webhookScanLog.Request, ++ webhookScanLog.Allowed, ++ webhookScanLog.ViolationsSummary, ++ webhookScanLog.DeniableViolations, ++ webhookScanLog.CreatedAt) ++ if err != nil { ++ zap.S().Errorf("failed to insert a new log. error: '%v'", err) ++ return err ++ } ++ ++ return nil ++} ++ ++func (g *WebhookScanLogger) fetchLogs() ([]webhookScanLog, error) { ++ // Fetch the entire logs in the DB, ordered by created_at DESC (the most updated will be at the top) ++ ++ db, err := g.getDbHandler() ++ if err != nil { ++ return nil, err ++ } ++ defer db.Close() ++ ++ row, err := db.Query("SELECT * FROM logs ORDER BY created_at DESC") ++ if err != nil { ++ zap.S().Errorf("failed query logs table. error: '%v'", err) ++ return nil, err ++ } ++ ++ var result []webhookScanLog ++ defer row.Close() ++ for row.Next() { ++ var id int ++ var uid string ++ var request string ++ var allowed bool ++ var violationsSummary string ++ var deniableViolations string ++ var createdAt time.Time ++ row.Scan(&id, &uid, &request, &allowed, &violationsSummary, &deniableViolations, &createdAt) ++ ++ result = append(result, webhookScanLog { ++ UID: uid, ++ Request: request, ++ Allowed: allowed, ++ ViolationsSummary: violationsSummary, ++ DeniableViolations: deniableViolations, ++ CreatedAt: createdAt, ++ }) ++ } ++ ++ return result, nil ++} ++ ++func (g *WebhookScanLogger) fetchLogById(logUID string) (*webhookScanLog, error) { ++ // Fetch a specific log by its request UID ++ ++ db, err := g.getDbHandler() ++ if err != nil { ++ return nil, err ++ } ++ defer db.Close() ++ ++ row, err := db.Query("SELECT * FROM logs WHERE uid=?", logUID) ++ if err != nil { ++ zap.S().Errorf("failed query logs table. error: '%v'", err) ++ return nil, err ++ } ++ defer row.Close() ++ ++ for row.Next() { ++ var id int ++ var uid string ++ var request string ++ var allowed bool ++ var violationsSummary string ++ var deniableViolations string ++ var createdAt time.Time ++ row.Scan(&id, &uid, &request, &allowed, &violationsSummary, &deniableViolations, &createdAt) ++ ++ return &webhookScanLog { ++ UID: uid, ++ Request: request, ++ Allowed: allowed, ++ ViolationsSummary: violationsSummary, ++ DeniableViolations: deniableViolations, ++ CreatedAt: createdAt, ++ }, nil ++ } ++ ++ return &webhookScanLog{}, nil ++} ++ ++func (g *WebhookScanLogger) initDBIfNeeded() error { ++ // Check where the SQL file exists. If it does do nothing. Otherwise, create the DB file and the Logs table. ++ if _, err := os.Stat(g.dbFilePath()); os.IsNotExist(err) { ++ file, err := os.Create(g.dbFilePath()) ++ if err != nil { ++ zap.S().Errorf("failed create db file. error: '%v'", err) ++ return err ++ } ++ file.Close() ++ ++ db, err := sql.Open("sqlite3", g.dbFilePath()) ++ if err != nil { ++ zap.S().Errorf("failed to open sql file. error: '%v'", err) ++ return err ++ } ++ defer db.Close() ++ ++ createLogsTableSQL := `CREATE TABLE logs ( ++ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ++ "uid" TEXT UNIQUE, ++ "request" TEXT, ++ "allowed" INTEGER, ++ "violations_summary" TEXT, ++ "deniable_violations" TEXT, ++ "created_at" DATETIME ++ );` ++ statement, err := db.Prepare(createLogsTableSQL) ++ if err != nil { ++ zap.S().Errorf("failed to create logs table. error: '%v'", err) ++ return err ++ } ++ statement.Exec() ++ } ++ ++ return nil ++} ++ ++func (g*WebhookScanLogger) getDbHandler() (*sql.DB, error) { ++ g.initDBIfNeeded() ++ ++ db, err := sql.Open("sqlite3", g.dbFilePath()) ++ if err != nil { ++ zap.S().Errorf("failed to open sql file. error: '%v'", err) ++ } ++ ++ return db, err ++} ++ ++ ++func (g *WebhookScanLogger) dbFilePath() string { ++ if g.test { ++ return "./" + dbFileName ++ } else { ++ // This is where the DB file should be located in the container (It is going to be saved in the host machine volume) ++ return "/data/k8s-admission-review-logs.db" ++ } ++} ++ ++// Used for Tests only - clear the DB file after the tests are done ++func (g *WebhookScanLogger) clearDbFilePath() { ++ os.Remove(g.dbFilePath()) ++} +diff --git a/pkg/http-server/webhook-scan-logger_test.go b/pkg/http-server/webhook-scan-logger_test.go +new file mode 100644 +index 0000000..518c0ae +--- /dev/null ++++ b/pkg/http-server/webhook-scan-logger_test.go +@@ -0,0 +1,77 @@ ++package httpserver ++ ++import ( ++ "testing" ++ "time" ++) ++func TestLogs(t *testing.T) { ++ t.Run("Log a new webhook scan", func(t *testing.T) { ++ var logger = WebhookScanLogger{ ++ test: true, ++ } ++ defer logger.clearDbFilePath() ++ ++ fetchedLogs, err := logger.fetchLogs() ++ if len(fetchedLogs) > 0 { ++ t.Errorf("At the beginning no logs should exist. Got: '%v'", len(fetchedLogs)) ++ } ++ ++ if err != nil { ++ t.Errorf("Got error") ++ } ++ ++ var log = webhookScanLog{ ++ UID: "myUID", ++ Request: "MyRequest", ++ CreatedAt: time.Now(), ++ Allowed: true, ++ DeniableViolations: "MyViolations", ++ ViolationsSummary: "ViolationsSummary", ++ } ++ ++ logger.log(log) ++ ++ fetchedLogs, err = logger.fetchLogs() ++ if err != nil { ++ t.Errorf("Got error") ++ } ++ ++ if len(fetchedLogs) != 1 { ++ t.Errorf("A new log should be returned. Got: '%v' logs", len(fetchedLogs)) ++ } ++ ++ myFetchLog, err := logger.fetchLogById(log.UID) ++ if err != nil { ++ t.Errorf("Got error") ++ } ++ ++ if len(myFetchLog.UID) < 1 { ++ t.Errorf("Log with ID: '%v' is not returned by fetchLogById", log.UID) ++ } ++ ++ if myFetchLog.UID != log.UID { ++ t.Errorf("Wrong UID. Expected '%v', Got: '%v'", log.UID, myFetchLog.UID) ++ ++ } ++ ++ if myFetchLog.Allowed != log.Allowed { ++ t.Errorf("Wrong Allowed. Expected '%v', Got: '%v'", log.Allowed, myFetchLog.Allowed) ++ } ++ ++ if myFetchLog.ViolationsSummary != log.ViolationsSummary { ++ t.Errorf("Wrong ViolationsSummary. Expected '%v', Got: '%v'", log.ViolationsSummary, myFetchLog.ViolationsSummary) ++ } ++ ++ if myFetchLog.Request != log.Request { ++ t.Errorf("Wrong Request. Expected '%v', Got: '%v'", log.Request, myFetchLog.Request) ++ } ++ ++ if myFetchLog.DeniableViolations != log.DeniableViolations { ++ t.Errorf("Wrong DeniableViolations. Expected '%v', Got: '%v'", log.DeniableViolations, myFetchLog.DeniableViolations) ++ } ++ ++ if myFetchLog.CreatedAt.Unix() != log.CreatedAt.Unix() { ++ t.Errorf("Wrong CreatedAt. Expected '%v', Got: '%v'", log.CreatedAt, myFetchLog.CreatedAt) ++ } ++ }) ++} +diff --git a/pkg/http-server/webhook-scan-logs.go b/pkg/http-server/webhook-scan-logs.go +new file mode 100644 +index 0000000..f99e80c +--- /dev/null ++++ b/pkg/http-server/webhook-scan-logs.go +@@ -0,0 +1,242 @@ ++/* ++ Copyright (C) 2020 Accurics, Inc. ++ ++ Licensed under the Apache License, Version 2.0 (the "License"); ++ you may not use this file except in compliance with the License. ++ You may obtain a copy of the License at ++ ++ http://www.apache.org/licenses/LICENSE-2.0 ++ ++ Unless required by applicable law or agreed to in writing, software ++ distributed under the License is distributed on an "AS IS" BASIS, ++ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ See the License for the specific language governing permissions and ++ limitations under the License. ++*/ ++ ++package httpserver ++ ++import ( ++ "encoding/json" ++ "fmt" ++ "github.com/accurics/terrascan/pkg/results" ++ "github.com/gorilla/mux" ++ _ "github.com/mattn/go-sqlite3" ++ "go.uber.org/zap" ++ "html/template" ++ "net/http" ++ "time" ++) ++ ++type webhookDisplayedViolation struct { ++ RuleName string `json:"rule_name"` ++ Category string `json:"category"` ++ Description string `json:"description"` ++ Severity string `json:"severity"` ++} ++ ++type webhookDisplayedReview struct { ++ Request webhookDisplayedRequest `json:"request"` ++} ++ ++type webhookDisplayedRequest struct { ++ Operation string `json:"operation"` ++ Object map[string]interface{} `json:"object"` ++} ++ ++type webhookDisplayedIndexScanLog struct { ++ CreatedAt time.Time ++ LogUrl string ++ Status string ++ Request string ++ Reasoning string ++} ++ ++type webhookDisplayedShowLog struct { ++ CreatedAt time.Time ++ UID string ++ Status string ++ Request string ++ Violations string ++ DeniableViolations string ++} ++ ++func (g *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) { ++ // Return an HTML page including all the logs history ++ ++ params := mux.Vars(r) ++ ++ apiKey := params["apiKey"] ++ if !g.validateAuthorization(apiKey, w) { ++ return ++ } ++ ++ logger := WebhookScanLogger{ ++ test: g.test, ++ } ++ ++ // The templates are saved in the docker in this location ++ t, _ := template.ParseFiles("/go/terrascan/index.html") ++ ++ logs, err := logger.fetchLogs() ++ if err != nil { ++ errMsg := fmt.Sprintf("error reading logs from DB: '%v'", err) ++ zap.S().Error(errMsg) ++ apiErrorResponse(w, errMsg, http.StatusInternalServerError) ++ return ++ } ++ ++ var logsData []webhookDisplayedIndexScanLog ++ for _, log := range logs { ++ logsData = append(logsData, webhookDisplayedIndexScanLog{ ++ CreatedAt: log.CreatedAt, ++ Status: g.getLogStatus(log), ++ LogUrl: g.getLogPath(r.Host, apiKey, log.UID), ++ Reasoning: g.getLogReasoning(log), ++ Request: g.getLogRequest(log), ++ }) ++ } ++ ++ t.Execute(w, logsData) ++} ++ ++func (g *APIHandler) getLogByUID(w http.ResponseWriter, r *http.Request) { ++ // Return an HTML page including the selected log ++ ++ params := mux.Vars(r) ++ ++ if !g.validateAuthorization(params["apiKey"], w) { ++ return ++ } ++ ++ var uid = params["uid"] ++ if len(uid) < 1 { ++ apiErrorResponse(w, "Log UID is missing", http.StatusBadRequest) ++ return ++ } ++ ++ logger := WebhookScanLogger{ ++ test: g.test, ++ } ++ ++ log, err := logger.fetchLogById(uid) ++ if err != nil { ++ errMsg := fmt.Sprintf("error reading logs from DB: '%v'", err) ++ zap.S().Error(errMsg) ++ apiErrorResponse(w, errMsg, http.StatusInternalServerError) ++ return ++ } ++ ++ if len(log.UID) < 1 { ++ apiErrorResponse(w, "Log is not found", http.StatusNotFound) ++ return ++ } ++ ++ displayedScanLog := webhookDisplayedShowLog{ ++ UID: log.UID, ++ CreatedAt: log.CreatedAt, ++ Status: g.getLogStatus(*log), ++ Request: log.Request, ++ Violations: log.ViolationsSummary, ++ DeniableViolations: log.DeniableViolations, ++ } ++ ++ t, _ := template.ParseFiles("/go/terrascan/show.html") ++ ++ t.Execute(w, displayedScanLog) ++} ++ ++func (g *APIHandler) getLogPath(host string, apiKey string, logUID string) string { ++ // Use this as the link to show the a specific log ++ return fmt.Sprintf("https://%v/k8s/webhooks/%v/logs/%v", host, apiKey, logUID) ++} ++ ++func (g *APIHandler)getLogStatus(log webhookScanLog) string { ++ // Calculate a log status: ++ // 1. !Allowed -> Rejected ++ // 2. Allowed -> if there are violations -> Allowed with Warnings. Otherwise -> Allowed ++ if !log.Allowed { ++ return "Rejected" ++ } ++ ++ var violationStore results.ViolationStore ++ err := json.Unmarshal([]byte(log.ViolationsSummary), &violationStore) ++ if err != nil { ++ zap.S().Errorf("Failed to ..") ++ } ++ ++ if len(violationStore.Violations) > 0 { ++ return "Allowed with warnings" ++ } ++ ++ return "Allowed" ++} ++ ++func (g *APIHandler)getLogReasoning(log webhookScanLog) string { ++ // Reasoning: ++ // - In case the request is denied (rejected), show the violations that cause the denial. ++ // - Otherwise, if there are violations, show the full violations list was found ++ // - Otherwise, reasoning is empty ++ ++ var violations []*results.Violation ++ if !log.Allowed { ++ err := json.Unmarshal([]byte(log.DeniableViolations), &violations) ++ if err != nil { ++ zap.S().Errorf("Failed to deserialize deniable violations summary. Error: %v", err.Error()) ++ return "" ++ } ++ } else { ++ var violationStore results.ViolationStore ++ err := json.Unmarshal([]byte(log.ViolationsSummary), &violationStore) ++ if err != nil { ++ zap.S().Errorf("Failed to deserialize violations summary. Error: %v", err.Error()) ++ return "" ++ } ++ ++ violations = violationStore.Violations ++ } ++ ++ var result []webhookDisplayedViolation ++ ++ if len(violations) < 1 { ++ return "" ++ } else { ++ for _, v := range violations { ++ result = append(result, webhookDisplayedViolation{ ++ Category: v.Category, ++ Description: v.Description, ++ RuleName: v.RuleName, ++ Severity: v.Severity, ++ }) ++ } ++ ++ encoded, err := json.Marshal(result) ++ if err != nil { ++ zap.S().Errorf("failed to serialize violations: '%v'", err) ++ return "" ++ } ++ ++ return string(encoded) ++ } ++ ++ return "" ++} ++ ++func (g *APIHandler)getLogRequest(log webhookScanLog) string { ++ var review webhookDisplayedReview ++ ++ err := json.Unmarshal([]byte(log.Request), &review) ++ ++ if err != nil { ++ zap.S().Errorf("Failed to deserialize request. Error: %v", err.Error()) ++ return "{}" ++ } ++ ++ result, err := json.Marshal(review.Request) ++ if err != nil { ++ zap.S().Errorf("Failed to serialize request. Error: %v", err.Error()) ++ return "{}" ++ } ++ ++ return string(result) ++} +diff --git a/pkg/http-server/webhook-scan.go b/pkg/http-server/webhook-scan.go +new file mode 100644 +index 0000000..bca2cc9 +--- /dev/null ++++ b/pkg/http-server/webhook-scan.go +@@ -0,0 +1,297 @@ ++/* ++ Copyright (C) 2020 Accurics, Inc. ++ ++ Licensed under the Apache License, Version 2.0 (the "License"); ++ you may not use this file except in compliance with the License. ++ You may obtain a copy of the License at ++ ++ http://www.apache.org/licenses/LICENSE-2.0 ++ ++ Unless required by applicable law or agreed to in writing, software ++ distributed under the License is distributed on an "AS IS" BASIS, ++ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ See the License for the specific language governing permissions and ++ limitations under the License. ++*/ ++ ++package httpserver ++ ++import ( ++ "encoding/json" ++ "fmt" ++ "github.com/accurics/terrascan/pkg/config" ++ "github.com/accurics/terrascan/pkg/results" ++ "github.com/accurics/terrascan/pkg/runtime" ++ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ++ runtimeK8s "k8s.io/apimachinery/pkg/runtime" ++ ++ "k8s.io/apimachinery/pkg/runtime/serializer" ++ ++ "github.com/gorilla/mux" ++ "go.uber.org/zap" ++ "io/ioutil" ++ v1 "k8s.io/api/admission/v1" ++ "net/http" ++ "os" ++ "time" ++) ++ ++func (g *APIHandler) scanIncomingWebhook(w http.ResponseWriter, r *http.Request) { ++ currentTime := time.Now() ++ ++ params := mux.Vars(r) ++ apiKey := params["apiKey"] ++ ++ // Validate if authorized (API key is specified and matched the server one (saved in an environment variable) ++ if !g.validateAuthorization(apiKey, w) { ++ return ++ } ++ ++ // Read the request into byte array ++ bytesRequestAdmissionReview, err := ioutil.ReadAll(r.Body) ++ if err != nil { ++ msg := fmt.Sprintf("Failed to read admission review: '%v'", err) ++ apiErrorResponse(w, msg, http.StatusBadRequest) ++ return ++ } ++ ++ zap.S().Debugf("scanning configuration webhook request: %+v", string(bytesRequestAdmissionReview)) ++ ++ // Unmarshal the byte array into a v1.AdmissionReview object ++ requestedAdmissionReview, err := g.deserializeAdmissionReviewRequest(bytesRequestAdmissionReview) ++ if err != nil { ++ apiErrorResponse(w, err.Error(), http.StatusBadRequest) ++ return ++ } ++ ++ // In case the object is nil => an operation of DELETE happened, just return 'allow' since there is nothing to check ++ if len(requestedAdmissionReview.Request.Object.Raw) < 1 { ++ g.sendResponseAdmissionReview(w, *requestedAdmissionReview, true, nil, "") ++ return ++ } ++ ++ // Save the object into a temp file for the policy engines ++ tempFile, err := g.writeObjectToTempFile(requestedAdmissionReview.Request.Object.Raw) ++ defer os.Remove(tempFile.Name()) ++ if err != nil { ++ apiErrorResponse(w, err.Error(), http.StatusInternalServerError) ++ return ++ } ++ ++ // Run the policy engines ++ output, err := g.executeEngines(*tempFile) ++ if err != nil { ++ apiErrorResponse(w, err.Error(), http.StatusInternalServerError) ++ return ++ } ++ ++ // Calculate if there are anydeny violations ++ denyViolations, err := g.getDenyViolations(*output) ++ allowed := len(denyViolations) < 1 ++ logPath := g.getLogPath(r.Host, apiKey, string(requestedAdmissionReview.Request.UID)) ++ ++ // Log the request in the DB ++ err = g.logWebhook(*output, string(requestedAdmissionReview.Request.UID), bytesRequestAdmissionReview, denyViolations,currentTime, allowed) ++ if err != nil { ++ apiErrorResponse(w, err.Error(), http.StatusInternalServerError) ++ return ++ } ++ ++ // Send the correct response according to the result ++ g.sendResponseAdmissionReview(w, *requestedAdmissionReview, allowed, output, logPath) ++} ++ ++func (g *APIHandler) validateAuthorization(apiKey string, w http.ResponseWriter) bool { ++ if len(apiKey) < 1 { ++ msg := "apiKey is missing" ++ zap.S().Error(msg) ++ apiErrorResponse(w, msg, http.StatusBadRequest) ++ return false ++ } ++ ++ savedApiKey := os.Getenv("K8S_WEBHOOK_API_KEY") ++ if len(savedApiKey) < 1 { ++ msg := "K8S_WEBHOOK_API_KEY environment variable MUST be declared" ++ zap.S().Error(msg) ++ apiErrorResponse(w, msg, http.StatusInternalServerError) ++ return false ++ } ++ ++ if apiKey != savedApiKey { ++ msg := "Invalid apiKey" ++ zap.S().Error(msg) ++ apiErrorResponse(w, msg, http.StatusUnauthorized) ++ return false ++ } ++ ++ return true ++} ++ ++func (g *APIHandler) getDeniedViolations(violations results.ViolationStore, denyRules config.K8sDenyRules) []*results.Violation { ++ // Check whether one of the violations matches the deny violations configuration ++ ++ var denyViolations []*results.Violation ++ ++ denyRuleMatcher := webhookDenyRuleMatcher{} ++ ++ for _, violation := range violations.Violations { ++ if denyRuleMatcher.match(*violation, denyRules) { ++ denyViolations = append(denyViolations, violation) ++ } ++ } ++ ++ return denyViolations ++} ++ ++func (g *APIHandler) writeObjectToTempFile(objectBytes []byte) (*os.File, error) { ++ tempFile, err := ioutil.TempFile("", "terrascan-*.json") ++ if err != nil { ++ zap.S().Errorf("failed to create temp file: '%v'", err) ++ return nil, err ++ } ++ ++ zap.S().Debugf("created temp config file at '%s'", tempFile.Name()) ++ ++ _, err = tempFile.Write(objectBytes) ++ if err != nil { ++ zap.S().Errorf("failed to write object to temp file: '%v'", err) ++ return nil, err ++ } ++ ++ return tempFile, nil ++} ++ ++func (g *APIHandler) executeEngines(tempFile os.File) (*runtime.Output, error) { ++ var executor *runtime.Executor ++ var err error ++ if g.test { ++ executor, err = runtime.NewExecutor("k8s", "v1", []string { "k8s" }, ++ tempFile.Name(), "", g.configFile, []string{"./k8s_testdata/testpolicies"}, []string{}, []string{}, "") ++ } else { ++ executor, err = runtime.NewExecutor("k8s", "v1", []string { "k8s" }, ++ tempFile.Name(), "", g.configFile, []string{}, []string{}, []string{}, "") ++ } ++ ++ if err != nil { ++ zap.S().Errorf("failed to create runtime executer: '%v'", err) ++ return nil, err ++ } ++ ++ result, err := executor.Execute() ++ if err != nil { ++ zap.S().Error("failed to scan resource object. error: '%v'", err) ++ return nil, err ++ } ++ ++ return &result, nil ++} ++ ++func (g *APIHandler) getDenyViolations(output runtime.Output) ([]*results.Violation, error) { ++ // Calcualte the deny violations according to the configuration specified in the config file ++ configReader, err := config.NewTerrascanConfigReader(g.configFile) ++ if err != nil { ++ zap.S().Errorf("error loading config file: '%v'", err) ++ return nil, err ++ } ++ ++ denyViolations := g.getDeniedViolations(*output.Violations.ViolationStore, configReader.GetK8sDenyRules()) ++ ++ return denyViolations, nil ++} ++ ++func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, ++ requestedAdmissionReview v1.AdmissionReview, ++ allowed bool, ++ output *runtime.Output, ++ logPath string,) { ++ responseAdmissionReview := &v1.AdmissionReview{} ++ responseAdmissionReview.SetGroupVersionKind(requestedAdmissionReview.GroupVersionKind()) ++ ++ responseAdmissionReview.Response = &v1.AdmissionResponse{ ++ UID: requestedAdmissionReview.Request.UID, ++ Allowed: allowed, ++ } ++ ++ if output != nil { ++ // Means we ran the engines and we have results ++ if allowed { ++ if len(output.Violations.ViolationStore.Violations) > 0 { ++ // In case there are no denial violations, just return the log URL as a warning ++ responseAdmissionReview.Response.Warnings = []string{logPath} ++ } ++ } else { ++ // In case the request was denied, return 403 and the log URL as an error message ++ responseAdmissionReview.Response.Result = &metav1.Status{Message: logPath, Code: 403} ++ } ++ } ++ ++ respBytes, err := json.Marshal(responseAdmissionReview) ++ if err != nil { ++ msg := fmt.Sprintf("failed to serialize admission review response: %v", err) ++ zap.S().Error(msg) ++ apiErrorResponse(w, msg, http.StatusInternalServerError) ++ } ++ ++ zap.S().Debugf("Response result: %+v", string(respBytes)) ++ ++ apiResponse(w, string(respBytes), http.StatusOK) ++} ++ ++func (g *APIHandler) logWebhook(output runtime.Output, ++ uid string, ++ bytesAdmissionReview []byte, ++ denyViolations []*results.Violation, ++ currentTime time.Time, ++ allowed bool) error { ++ var deniedViolationsEncoded string ++ ++ if len(denyViolations) < 1 { ++ deniedViolationsEncoded = "" ++ } else { ++ d, _ := json.Marshal(denyViolations) ++ deniedViolationsEncoded = string(d) ++ } ++ ++ encodedViolationsSummary, _ := json.Marshal(output.Violations.ViolationStore) ++ ++ logger := WebhookScanLogger{ ++ test: g.test, ++ } ++ ++ err := logger.log(webhookScanLog { ++ UID: uid, ++ Request: string(bytesAdmissionReview), ++ Allowed: allowed, ++ DeniableViolations: deniedViolationsEncoded, ++ ViolationsSummary: string(encodedViolationsSummary), ++ CreatedAt: currentTime, ++ }) ++ if err != nil { ++ zap.S().Error("error logging scan result: '%v'", err) ++ return err ++ } ++ ++ return nil ++} ++ ++func (g *APIHandler) deserializeAdmissionReviewRequest(bytesAdmissionReview []byte) (*v1.AdmissionReview, error) { ++ var scheme = runtimeK8s.NewScheme() ++ v1.AddToScheme(scheme) ++ ++ var codecs = serializer.NewCodecFactory(scheme) ++ deserializer := codecs.UniversalDeserializer() ++ ++ obj, _, err := deserializer.Decode(bytesAdmissionReview, nil, nil) ++ if err != nil { ++ zap.S().Errorf("Request could not be decoded: %v", err) ++ return nil , err ++ } ++ ++ requestedAdmissionReview, ok := obj.(*v1.AdmissionReview) ++ if !ok { ++ zap.S().Errorf("Failed to deserialize request body to v1.AdmissionReview. Obj: %v", obj) ++ return nil , err ++ } ++ ++ return requestedAdmissionReview, nil ++} +diff --git a/pkg/http-server/webhook-scan_test.go b/pkg/http-server/webhook-scan_test.go +new file mode 100644 +index 0000000..7431359 +--- /dev/null ++++ b/pkg/http-server/webhook-scan_test.go +@@ -0,0 +1,244 @@ ++package httpserver ++ ++import ( ++ "bytes" ++ "encoding/json" ++ "fmt" ++ "github.com/gorilla/mux" ++ "io/ioutil" ++ v1 "k8s.io/api/admission/v1" ++ "net/http" ++ "net/http/httptest" ++ "os" ++ "testing" ++) ++ ++func TestUWebhooks(t *testing.T) { ++ testFilePath := "./k8s_testdata/testconfig.json" ++ testApiKey := "Test-API-KEY" ++ testEnvApiKey := "Test-API-KEY" ++ testConfigFile := "" ++ ++ table := []struct { ++ name string ++ contentRequestPath string ++ apiKey string ++ envApiKey string ++ wantStatus int ++ configFile string ++ warnings bool ++ allowed bool ++ statusCode int32 ++ statusMessage bool ++ }{ ++ { ++ name: "missing api key", ++ contentRequestPath: testFilePath, ++ apiKey: "", ++ envApiKey: testEnvApiKey, ++ wantStatus: http.StatusBadRequest, ++ configFile: testConfigFile, ++ }, ++ { ++ name: "missing K8S_WEBHOOK_API_KEY", ++ contentRequestPath: testFilePath, ++ apiKey: testApiKey, ++ envApiKey: "", ++ wantStatus: http.StatusInternalServerError, ++ configFile: testConfigFile, ++ }, ++ { ++ name: "invalid api key", ++ contentRequestPath: testFilePath, ++ apiKey: testApiKey, ++ envApiKey: "Invalid API KEY", ++ wantStatus: http.StatusUnauthorized, ++ configFile: testConfigFile, ++ }, ++ { ++ name: "invalid api key", ++ contentRequestPath: testFilePath, ++ apiKey: testApiKey, ++ envApiKey: "Invalid API KEY", ++ wantStatus: http.StatusUnauthorized, ++ configFile: testConfigFile, ++ }, ++ { ++ name: "invalid request json content", ++ contentRequestPath: "./k8s_testdata/invalid.json", ++ apiKey: testApiKey, ++ envApiKey: testEnvApiKey, ++ wantStatus: http.StatusBadRequest, ++ configFile: testConfigFile, ++ }, ++ { ++ name: "empty request json content", ++ contentRequestPath: "./k8s_testdata/empty.json", ++ apiKey: testApiKey, ++ envApiKey: testEnvApiKey, ++ wantStatus: http.StatusBadRequest, ++ configFile: testConfigFile, ++ }, ++ { ++ name: "request with empty object", ++ contentRequestPath: "./k8s_testdata/empty_object.json", ++ apiKey: testApiKey, ++ envApiKey: testEnvApiKey, ++ wantStatus: http.StatusOK, ++ configFile: testConfigFile, ++ warnings: false, ++ allowed: true, ++ }, ++ { ++ name: "safe request object", ++ contentRequestPath: testFilePath, ++ apiKey: testApiKey, ++ envApiKey: testEnvApiKey, ++ wantStatus: http.StatusOK, ++ configFile: testConfigFile, ++ warnings: false, ++ allowed: true, ++ }, ++ { ++ name: "risky request object without config", ++ contentRequestPath: "./k8s_testdata/risky_testconfig.json", ++ apiKey: testApiKey, ++ envApiKey: testEnvApiKey, ++ configFile: testConfigFile, ++ warnings: true, ++ allowed: true, ++ wantStatus: http.StatusOK, ++ }, ++ { ++ name: "risky request object with config that make it safe", ++ contentRequestPath: "./k8s_testdata/risky_testconfig.json", ++ apiKey: testApiKey, ++ envApiKey: testEnvApiKey, ++ configFile: "./k8s_testdata/config-specific-rule.toml", ++ warnings: false, ++ allowed: true, ++ wantStatus: http.StatusOK, ++ }, ++ { ++ name: "risky request object with config that just removes some of the violations", ++ contentRequestPath: "./k8s_testdata/risky_testconfig.json", ++ apiKey: testApiKey, ++ envApiKey: testEnvApiKey, ++ configFile: "./k8s_testdata/config-medium-severity.toml", ++ warnings: true, ++ allowed: true, ++ wantStatus: http.StatusOK, ++ }, ++ { ++ name: "risky request object with denied severity", ++ contentRequestPath: "./k8s_testdata/risky_testconfig.json", ++ apiKey: testApiKey, ++ envApiKey: testEnvApiKey, ++ configFile: "./k8s_testdata/config-deny-high.toml", ++ warnings: false, ++ allowed: false, ++ statusCode: 403, ++ statusMessage: true, ++ wantStatus: http.StatusOK, ++ }, ++ { ++ name: "risky request object with denied categories", ++ contentRequestPath: "./k8s_testdata/risky_testconfig.json", ++ apiKey: testApiKey, ++ envApiKey: testEnvApiKey, ++ configFile: "./k8s_testdata/config-deny-category.toml", ++ warnings: false, ++ allowed: false, ++ statusCode: 403, ++ statusMessage: true, ++ wantStatus: http.StatusOK, ++ }, ++ { ++ name: "risky request object with denied categories that does not exist", ++ contentRequestPath: "./k8s_testdata/risky_testconfig.json", ++ apiKey: testApiKey, ++ envApiKey: testEnvApiKey, ++ configFile: "./k8s_testdata/config-deny-non-existing-category.toml", ++ warnings: true, ++ allowed: true, ++ wantStatus: http.StatusOK, ++ }, ++ } ++ ++ for _, tt := range table { ++ t.Run(tt.name, func(t *testing.T) { ++ os.Setenv("K8S_WEBHOOK_API_KEY", tt.envApiKey) ++ ++ // test file to upload ++ path := tt.contentRequestPath ++ jsonFile, err := os.Open(path) ++ if err != nil { ++ t.Error(err) ++ return ++ } ++ defer jsonFile.Close() ++ logger := WebhookScanLogger{ ++ test: true, ++ } ++ defer logger.clearDbFilePath() ++ ++ byteValue, _ := ioutil.ReadAll(jsonFile) ++ ++ var admissionRequest v1.AdmissionReview ++ json.Unmarshal(byteValue, &admissionRequest) ++ ++ var url string ++ if len(tt.apiKey) > 0 { ++ url = fmt.Sprintf("/v1/k8s/webhooks/%v/scan", tt.apiKey) ++ } else { ++ url = fmt.Sprintf("/v1/k8s/webhooks/scan") ++ } ++ ++ req := httptest.NewRequest("POST", url, bytes.NewReader(byteValue)) ++ req.Header.Set("Content-Type", "application/json") ++ req = mux.SetURLVars(req, map[string]string{ ++ "apiKey": tt.apiKey, ++ }) ++ res := httptest.NewRecorder() ++ // new api handler ++ h := &APIHandler{test: true, configFile: tt.configFile } ++ h.scanIncomingWebhook(res, req) ++ ++ if res.Code != tt.wantStatus { ++ t.Errorf("incorrect status code, got: '%v', want: '%v', error: '%v'", res.Code, tt.wantStatus, res.Body) ++ } ++ ++ var response v1.AdmissionReview ++ _ = json.Unmarshal(res.Body.Bytes(), &response) ++ ++ if res.Code == http.StatusOK { ++ if tt.warnings && response.Response.Warnings == nil { ++ t.Errorf("Expected warnings but received None") ++ } ++ ++ if tt.allowed != response.Response.Allowed { ++ t.Errorf("Mismach in allowed. Got: %v, expected: %v", response.Response.Allowed, tt.allowed) ++ } ++ ++ if tt.statusCode != 0 && tt.statusCode != response.Response.Result.Code { ++ t.Errorf("Mismach Statud code Got: %v, expected: %v", response.Response.Result.Code, tt.statusCode) ++ } ++ ++ if tt.warnings || tt.statusMessage { ++ var logPath string ++ if tt.warnings { ++ logPath = response.Response.Warnings[0] ++ } else if tt.statusMessage { ++ logPath = response.Response.Result.Message ++ } ++ ++ expectedLogPath := fmt.Sprintf("https://%v/k8s/webhooks/%v/logs/705ab4f5-6393-11e8-b7cc-42010a800002", req.Host, tt.envApiKey) ++ ++ if logPath != expectedLogPath { ++ t.Errorf("Mismach Log path. Got: %v, expected: %v", logPath, expectedLogPath) ++ } ++ } ++ } ++ }) ++ } ++} +diff --git a/pkg/initialize/run.go b/pkg/initialize/run.go +index cec6f36..30472e0 100644 +--- a/pkg/initialize/run.go ++++ b/pkg/initialize/run.go +@@ -51,7 +51,7 @@ func Run(isScanCmd bool) error { + return err + } + +- zap.S().Debug("intialized successfully") ++ zap.S().Debug("initialized successfully") + return nil + } + +diff --git a/pkg/runtime/executor_test.go b/pkg/runtime/executor_test.go +index 79cb7d5..8cfb576 100644 +--- a/pkg/runtime/executor_test.go ++++ b/pkg/runtime/executor_test.go +@@ -1,12 +1,9 @@ + /* + Copyright (C) 2020 Accurics, Inc. +- + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at +- + http://www.apache.org/licenses/LICENSE-2.0 +- + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +diff --git a/pkg/runtime/testdata/scan-skip-rules-low-severity.toml b/pkg/runtime/testdata/scan-skip-rules-low-severity.toml +index 3f4d71b..5dadbf9 100644 +--- a/pkg/runtime/testdata/scan-skip-rules-low-severity.toml ++++ b/pkg/runtime/testdata/scan-skip-rules-low-severity.toml +@@ -11,4 +11,4 @@ + ] + + [severity] +-level = "low" +\ No newline at end of file ++level = "low" +-- +2.24.3 (Apple Git-128) + diff --git a/Makefile b/Makefile index a9b621d28..15ddf1505 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null) BUILD_FLAGS := -v -ldflags "-w -s" -ENV_SETTINGS := CGO_ENABLED=0 +ENV_SETTINGS := CGO_ENABLED=1 BUILD_DIR = ./bin BINARY_NAME = terrascan diff --git a/build/Dockerfile b/build/Dockerfile index 1258bd9c5..3c5d97ebe 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -3,13 +3,14 @@ FROM golang:alpine AS builder ARG GOOS_VAL=linux ARG GOARCH_VAL=amd64 -ARG CGO_ENABLED_VAL=0 +ARG CGO_ENABLED_VAL=1 WORKDIR $GOPATH/src/terrascan # download go dependencies COPY go.mod go.sum ./ RUN go mod download +RUN apk add -U build-base # copy terrascan source COPY . . @@ -34,6 +35,10 @@ ENV PATH /go/bin:$PATH # copy terrascan binary from build COPY --from=builder /go/bin/terrascan /go/bin/terrascan +# Copy webhooks UI templates & assets +COPY ./pkg/http-server/templates /go/terrascan +COPY ./pkg/http-server/assets /go/terrascan/assets + EXPOSE 9010 ENTRYPOINT ["/go/bin/terrascan"] diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 9210f5c88..6a430a5ca 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -4,3 +4,4 @@ services: image: accurics/terrascan:${TAG:-latest} ports: - 9010:9010 + - 443:9443 diff --git a/docs/getting-started/admission-controller-webhooks-usage.md b/docs/getting-started/admission-controller-webhooks-usage.md new file mode 100644 index 000000000..5db33da16 --- /dev/null +++ b/docs/getting-started/admission-controller-webhooks-usage.md @@ -0,0 +1,108 @@ +# Using Terrascan as a Kubernetes Admission Controller + +## Overview +Terrascan can be integrated with K8s [admissions webhooks](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/). +It can be used as one of the validating webhooks to be used and scan new configurations. + +In this guide, we'll demonstrate how Terrascan can be configured to: +* Scan configuration changes policies when an object is being created or updated +* Allow / reject the request in case a violation is detected + + +## Installation Guide + +### Create an instance +Your Terrascan instance has the following requirements for being able to scan K8s configurations. + +1. Be accessible via HTTPS. Make sure your cloud firewall is configured to allow this. +1. Have a valid SSL certificate for the served domain name. To do that, choose one of our suggested methods: + 1. Use a subdomain of your choosing (e.g dev-terrascan-k8s.accurics.com) and create a valid certificate for this subdomain through your SSL certificate provider. [Let's Encrypt](https://letsencrypt.org/) is a free, simple to use certificate authority you can use. + 1. Use a reverse-proxy to serve SSL requests; for example, use Cloudflare Flexible to get a certificate by a trusted-CA to your [self-signed certificate](https://www.digitalocean.com/community/tutorials/openssl-essentials-working-with-ssl-certificates-private-keys-and-csrs). + 1. Generate a self-signed certificate and have your K8s cluster trust it. To add a trusted CA to ca-pemstore, as demonstrated in [paraspatidar's blog post](https://medium.com/@paraspatidar/add-ssl-tls-certificate-or-pem-file-to-kubernetes-pod-s-trusted-root-ca-store-7bed5cd683d). +1. Use the Terrascan docker as demonstrated in this document, or run it from the sources. + +### Run Terrascan webhook service +Run Terrascan docker image in your server using the following command: + ```bash + sudo docker run -p 443:9443 -v :/data -u root -e K8S_WEBHOOK_API_KEY=> accurics/terrascan server --cert-path /data/cert.pem --key-path /data/key.pem + ``` +`` is a key used for authentication between your K8s environment and the Terrascan server. Generate your preferred key and use it here. + +`` is a directory path in your server where both the certificate and the private key .pem files are stored. +In addition, this directory is used to write save the webhook logs. (An SQLite file) + +You can specify a config file that specifies which policies to use in the scan and which violations should lead to rejection. + +A config file example: ```my_terrscan_config.toml``` + ```bash +[severity] +level = "medium" +[rules] + skip-rules = [ + "accurics.kubernetes.IAM.107" + ] + +[k8s-deny-rules] + denied-categories = [ + "Network Ports Security" + ] + denied-severity = "high" + ``` + +You can specify the following configurations: +* **scan-rules** - one or more rules to scan +* **skip-rules** - one or more rules to skip while scanning +* **severity** - the minimal level of severity of the policies to be scanned + + +* **k8s-deny-rules** - specify the rules that should cause a rejection of the admission request + * **denied-categories** - one or more policy categories that are not allowed in the detected violations + * **denied-severity** - the minimal level of severity that should cause a rejection + +In order to use a configuration file, add it as a command line argument: + +``` -c /data/my_terrscan_config.toml``` + + +### Configure K8s to send webhooks +Configure a new ```ValidatingWebhookConfiguration``` in your Kubernetes environment and specify your Terrascan server endpoint. + +Example: + ```bash + cat </v1/k8s/webhooks//scan + sideEffects: None + admissionReviewVersions: ["v1"] + EOF + ``` + +* You can modify the `rules` that trigger the webhook according to your preferences. +* Update the ```clientConfig``` URL with your terrascan server address and the API key you generated before. + + +### Test your settings +Try to run a new pod / service. For example: +``` Bash + kubectl run mynginx --image=nginx +``` + +Go to ```https:///k8s/webhooks//logs``` and verify your request is logged. diff --git a/go.mod b/go.mod index 40a309b96..90c9c221b 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/iancoleman/strcase v0.1.3 github.com/itchyny/gojq v0.12.1 github.com/mattn/go-isatty v0.0.12 + github.com/mattn/go-sqlite3 v1.12.0 github.com/mitchellh/go-homedir v1.1.0 github.com/onsi/ginkgo v1.12.1 github.com/onsi/gomega v1.10.5 @@ -37,5 +38,7 @@ require ( gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b helm.sh/helm/v3 v3.4.0 honnef.co/go/tools v0.1.3 // indirect + k8s.io/api v0.19.2 + k8s.io/apimachinery v0.19.2 sigs.k8s.io/kustomize/api v0.8.5 ) diff --git a/go.sum b/go.sum index b7c9ff9d6..1b9a34761 100644 --- a/go.sum +++ b/go.sum @@ -632,6 +632,7 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-shellwords v1.0.4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.12.0 h1:u/x3mp++qUxvYfulZ4HKOvVO0JWhk7HtE8lWhbGz/Do= github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= diff --git a/pkg/cli/server.go b/pkg/cli/server.go index 9538b09b7..c5889c12b 100644 --- a/pkg/cli/server.go +++ b/pkg/cli/server.go @@ -21,6 +21,14 @@ import ( "github.com/spf13/cobra" ) +var ( + // CertFile Certificate file path, required in order to enable secure HTTP server + CertFile string + + // PrivateKeyFile Private key file path, required in order to enable secure HTTP server + PrivateKeyFile string +) + var serverCmd = &cobra.Command{ Use: "server", Short: "Run Terrascan as an API server", @@ -35,9 +43,11 @@ Run Terrascan as an API server that inspects incoming IaC (Infrastructure-as-Cod } func server(cmd *cobra.Command, args []string) { - httpserver.Start() + httpserver.Start(ConfigFile, CertFile, PrivateKeyFile) } func init() { + rootCmd.PersistentFlags().StringVarP(&PrivateKeyFile, "key-path", "", "", "private key file path") + rootCmd.PersistentFlags().StringVarP(&CertFile, "cert-path", "", "", "certificate file path") RegisterCommand(rootCmd, serverCmd) } diff --git a/pkg/config/config-reader.go b/pkg/config/config-reader.go index dae0a2f23..bb421a093 100644 --- a/pkg/config/config-reader.go +++ b/pkg/config/config-reader.go @@ -92,3 +92,8 @@ func (r TerrascanConfigReader) getCategory() Category { func (r TerrascanConfigReader) getSeverity() Severity { return r.config.Severity } + +// GetK8sDenyRules will return the k8s deny rules specified in the terrascan config file +func (r TerrascanConfigReader) GetK8sDenyRules() K8sDenyRules { + return r.config.K8sDenyRules +} diff --git a/pkg/config/types.go b/pkg/config/types.go index e71d04b04..a1bc0190e 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -26,6 +26,7 @@ type TerrascanConfig struct { Rules `toml:"rules,omitempty"` Category `toml:"category,omitempty"` Severity `toml:"severity,omitempty"` + K8sDenyRules `toml:"k8s-deny-rules,omitempty"` } // Category defines the categories of violations that you want to be reported @@ -61,3 +62,9 @@ type Rules struct { ScanRules []string `toml:"scan-rules,omitempty"` SkipRules []string `toml:"skip-rules,omitempty"` } + +// K8s deny rules in the terrascan config file +type K8sDenyRules struct { + DeniedSeverity string `toml:"denied-severity,omitempty"` + Categories []string `toml:"denied-categories,omitempty"` +} diff --git a/pkg/http-server/assets/icons.svg b/pkg/http-server/assets/icons.svg new file mode 100644 index 000000000..cc8298ad0 --- /dev/null +++ b/pkg/http-server/assets/icons.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pkg/http-server/assets/jsonTree.css b/pkg/http-server/assets/jsonTree.css new file mode 100644 index 000000000..ad1748407 --- /dev/null +++ b/pkg/http-server/assets/jsonTree.css @@ -0,0 +1,107 @@ +/* + * JSON Tree Viewer + * http://github.com/summerstyle/jsonTreeViewer + * + * Copyright 2017 Vera Lobacheva (http://iamvera.com) + * Released under the MIT license (LICENSE.txt) + */ + +/* Background for the tree. May use for element */ +.jsontree_bg { + background: #FFF; +} + +/* Styles for the container of the tree (e.g. fonts, margins etc.) */ +.jsontree_tree { + /*margin-left: 30px;*/ + font-family: 'PT Mono', monospace; + font-size: 14px; +} + +/* Styles for a list of child nodes */ +.jsontree_child-nodes { + display: none; + margin-left: 35px; + margin-bottom: 5px; + line-height: 2; +} +.jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_child-nodes { + display: block; +} + +/* Styles for labels */ +.jsontree_label-wrapper { + float: left; + margin-right: 8px; +} +.jsontree_label { + font-weight: normal; + vertical-align: top; + color: #000; + position: relative; + padding: 1px; + border-radius: 4px; + cursor: default; +} +.jsontree_node_marked > .jsontree_label-wrapper > .jsontree_label { + background: #fff2aa; +} + +/* Styles for values */ +.jsontree_value-wrapper { + display: block; + /*overflow: hidden;*/ +} +.jsontree_node_complex > .jsontree_value-wrapper { + overflow: inherit; +} +.jsontree_value { + vertical-align: top; + display: inline; +} +.jsontree_value_null { + color: #777; + font-weight: bold; +} +.jsontree_value_string { + color: #025900; + font-weight: bold; +} +.jsontree_value_number { + color: #000E59; + font-weight: bold; +} +.jsontree_value_boolean { + color: #600100; + font-weight: bold; +} + +/* Styles for active elements */ +.jsontree_expand-button { + position: absolute; + top: 3px; + left: -15px; + display: block; + width: 11px; + height: 11px; + background-image: url('icons.svg'); +} +.jsontree_node_expanded > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button { + background-position: 0 -11px; +} +.jsontree_show-more { + cursor: pointer; +} +.jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more { + display: none; +} +.jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button, +.jsontree_node_empty > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more { + display: none !important; +} +.jsontree_node_complex > .jsontree_label-wrapper > .jsontree_label { + cursor: pointer; +} +.jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label { + cursor: default !important; +} diff --git a/pkg/http-server/assets/jsonTree.js b/pkg/http-server/assets/jsonTree.js new file mode 100644 index 000000000..3de131a7d --- /dev/null +++ b/pkg/http-server/assets/jsonTree.js @@ -0,0 +1,819 @@ +/** + * JSON Tree library (a part of jsonTreeViewer) + * http://github.com/summerstyle/jsonTreeViewer + * + * Copyright 2017 Vera Lobacheva (http://iamvera.com) + * Released under the MIT license (LICENSE.txt) + */ + +var jsonTree = (function() { + + /* ---------- Utilities ---------- */ + var utils = { + + /* + * Returns js-"class" of value + * + * @param val {any type} - value + * @returns {string} - for example, "[object Function]" + */ + getClass : function(val) { + return Object.prototype.toString.call(val); + }, + + /** + * Checks for a type of value (for valid JSON data types). + * In other cases - throws an exception + * + * @param val {any type} - the value for new node + * @returns {string} ("object" | "array" | "null" | "boolean" | "number" | "string") + */ + getType : function(val) { + if (val === null) { + return 'null'; + } + + switch (typeof val) { + case 'number': + return 'number'; + + case 'string': + return 'string'; + + case 'boolean': + return 'boolean'; + } + + switch(utils.getClass(val)) { + case '[object Array]': + return 'array'; + + case '[object Object]': + return 'object'; + } + + throw new Error('Bad type: ' + utils.getClass(val)); + }, + + /** + * Applies for each item of list some function + * and checks for last element of the list + * + * @param obj {Object | Array} - a list or a dict with child nodes + * @param func {Function} - the function for each item + */ + forEachNode : function(obj, func) { + var type = utils.getType(obj), + isLast; + + switch (type) { + case 'array': + isLast = obj.length - 1; + + obj.forEach(function(item, i) { + func(i, item, i === isLast); + }); + + break; + + case 'object': + var keys = Object.keys(obj).sort(); + + isLast = keys.length - 1; + + keys.forEach(function(item, i) { + func(item, obj[item], i === isLast); + }); + + break; + } + + }, + + /** + * Implements the kind of an inheritance by + * using parent prototype and + * creating intermediate constructor + * + * @param Child {Function} - a child constructor + * @param Parent {Function} - a parent constructor + */ + inherits : (function() { + var F = function() {}; + + return function(Child, Parent) { + F.prototype = Parent.prototype; + Child.prototype = new F(); + Child.prototype.constructor = Child; + }; + })(), + + /* + * Checks for a valid type of root node* + * + * @param {any type} jsonObj - a value for root node + * @returns {boolean} - true for an object or an array, false otherwise + */ + isValidRoot : function(jsonObj) { + switch (utils.getType(jsonObj)) { + case 'object': + case 'array': + return true; + default: + return false; + } + }, + + /** + * Extends some object + */ + extend : function(targetObj, sourceObj) { + for (var prop in sourceObj) { + if (sourceObj.hasOwnProperty(prop)) { + targetObj[prop] = sourceObj[prop]; + } + } + } + }; + + + /* ---------- Node constructors ---------- */ + + /** + * The factory for creating nodes of defined type. + * + * ~~~ Node ~~~ is a structure element of an onject or an array + * with own label (a key of an object or an index of an array) + * and value of any json data type. The root object or array + * is a node without label. + * {... + * [+] "label": value, + * ...} + * + * Markup: + *
    • + * + * + * + * "label" + * + * : + * + * <(div|span) class="jsontree_value jsontree_value_(object|array|boolean|null|number|string)"> + * ... + * + *
    • + * + * @param label {string} - key name + * @param val {Object | Array | string | number | boolean | null} - a value of node + * @param isLast {boolean} - true if node is last in list of siblings + * + * @return {Node} + */ + function Node(label, val, isLast) { + var nodeType = utils.getType(val); + + if (nodeType in Node.CONSTRUCTORS) { + return new Node.CONSTRUCTORS[nodeType](label, val, isLast); + } else { + throw new Error('Bad type: ' + utils.getClass(val)); + } + } + + Node.CONSTRUCTORS = { + 'boolean' : NodeBoolean, + 'number' : NodeNumber, + 'string' : NodeString, + 'null' : NodeNull, + 'object' : NodeObject, + 'array' : NodeArray + }; + + + /* + * The constructor for simple types (string, number, boolean, null) + * {... + * [+] "label": value, + * ...} + * value = string || number || boolean || null + * + * Markup: + *
    • + * + * "age" + * : + * + * 25 + * , + *
    • + * + * @abstract + * @param label {string} - key name + * @param val {string | number | boolean | null} - a value of simple types + * @param isLast {boolean} - true if node is last in list of parent childNodes + */ + function _NodeSimple(label, val, isLast) { + if (this.constructor === _NodeSimple) { + throw new Error('This is abstract class'); + } + + var self = this, + el = document.createElement('li'), + labelEl, + template = function(label, val) { + var str = '\ + \ + "' + + label + + '" : \ + \ + \ + ' + + val + + '' + + (!isLast ? ',' : '') + + ''; + + return str; + }; + + self.label = label; + self.isComplex = false; + + el.classList.add('jsontree_node'); + el.innerHTML = template(label, val); + + self.el = el; + + labelEl = el.querySelector('.jsontree_label'); + + labelEl.addEventListener('click', function(e) { + if (e.altKey) { + self.toggleMarked(); + return; + } + + if (e.shiftKey) { + document.getSelection().removeAllRanges(); + alert(self.getJSONPath()); + return; + } + }, false); + } + + _NodeSimple.prototype = { + constructor : _NodeSimple, + + /** + * Mark node + */ + mark : function() { + this.el.classList.add('jsontree_node_marked'); + }, + + /** + * Unmark node + */ + unmark : function() { + this.el.classList.remove('jsontree_node_marked'); + }, + + /** + * Mark or unmark node + */ + toggleMarked : function() { + this.el.classList.toggle('jsontree_node_marked'); + }, + + /** + * Expands parent node of this node + * + * @param isRecursive {boolean} - if true, expands all parent nodes + * (from node to root) + */ + expandParent : function(isRecursive) { + if (!this.parent) { + return; + } + + this.parent.expand(); + this.parent.expandParent(isRecursive); + }, + + /** + * Returns JSON-path of this + * + * @param isInDotNotation {boolean} - kind of notation for returned json-path + * (by default, in bracket notation) + * @returns {string} + */ + getJSONPath : function(isInDotNotation) { + if (this.isRoot) { + return "$"; + } + + var currentPath; + + if (this.parent.type === 'array') { + currentPath = "[" + this.label + "]"; + } else { + currentPath = isInDotNotation ? "." + this.label : "['" + this.label + "']"; + } + + return this.parent.getJSONPath(isInDotNotation) + currentPath; + } + }; + + + /* + * The constructor for boolean values + * {... + * [+] "label": boolean, + * ...} + * boolean = true || false + * + * @constructor + * @param label {string} - key name + * @param val {boolean} - value of boolean type, true or false + * @param isLast {boolean} - true if node is last in list of parent childNodes + */ + function NodeBoolean(label, val, isLast) { + this.type = "boolean"; + + _NodeSimple.call(this, label, val, isLast); + } + utils.inherits(NodeBoolean,_NodeSimple); + + + /* + * The constructor for number values + * {... + * [+] "label": number, + * ...} + * number = 123 + * + * @constructor + * @param label {string} - key name + * @param val {number} - value of number type, for example 123 + * @param isLast {boolean} - true if node is last in list of parent childNodes + */ + function NodeNumber(label, val, isLast) { + this.type = "number"; + + _NodeSimple.call(this, label, val, isLast); + } + utils.inherits(NodeNumber,_NodeSimple); + + + /* + * The constructor for string values + * {... + * [+] "label": string, + * ...} + * string = "abc" + * + * @constructor + * @param label {string} - key name + * @param val {string} - value of string type, for example "abc" + * @param isLast {boolean} - true if node is last in list of parent childNodes + */ + function NodeString(label, val, isLast) { + this.type = "string"; + + _NodeSimple.call(this, label, '"' + val + '"', isLast); + } + utils.inherits(NodeString,_NodeSimple); + + + /* + * The constructor for null values + * {... + * [+] "label": null, + * ...} + * + * @constructor + * @param label {string} - key name + * @param val {null} - value (only null) + * @param isLast {boolean} - true if node is last in list of parent childNodes + */ + function NodeNull(label, val, isLast) { + this.type = "null"; + + _NodeSimple.call(this, label, val, isLast); + } + utils.inherits(NodeNull,_NodeSimple); + + + /* + * The constructor for complex types (object, array) + * {... + * [+] "label": value, + * ...} + * value = object || array + * + * Markup: + *
    • + * + * + * + * "label" + * + * : + * + *
      + * { + *
        + * } + * , + *
      + *
    • + * + * @abstract + * @param label {string} - key name + * @param val {Object | Array} - a value of complex types, object or array + * @param isLast {boolean} - true if node is last in list of parent childNodes + */ + function _NodeComplex(label, val, isLast) { + if (this.constructor === _NodeComplex) { + throw new Error('This is abstract class'); + } + + var self = this, + el = document.createElement('li'), + template = function(label, sym) { + var comma = (!isLast) ? ',' : '', + str = '\ +
      \ +
      \ + ' + sym[0] + '\ + \ +
        \ + ' + sym[1] + '' + + '
        ' + comma + + '
        '; + + if (label !== null) { + str = '\ + \ + ' + + '' + + '"' + label + + '" : \ + ' + str; + } + + return str; + }, + childNodesUl, + labelEl, + moreContentEl, + childNodes = []; + + self.label = label; + self.isComplex = true; + + el.classList.add('jsontree_node'); + el.classList.add('jsontree_node_complex'); + el.innerHTML = template(label, self.sym); + + childNodesUl = el.querySelector('.jsontree_child-nodes'); + + if (label !== null) { + labelEl = el.querySelector('.jsontree_label'); + moreContentEl = el.querySelector('.jsontree_show-more'); + + labelEl.addEventListener('click', function(e) { + if (e.altKey) { + self.toggleMarked(); + return; + } + + if (e.shiftKey) { + document.getSelection().removeAllRanges(); + alert(self.getJSONPath()); + return; + } + + self.toggle(e.ctrlKey || e.metaKey); + }, false); + + moreContentEl.addEventListener('click', function(e) { + self.toggle(e.ctrlKey || e.metaKey); + }, false); + + self.isRoot = false; + } else { + self.isRoot = true; + self.parent = null; + + el.classList.add('jsontree_node_expanded'); + } + + self.el = el; + self.childNodes = childNodes; + self.childNodesUl = childNodesUl; + + utils.forEachNode(val, function(label, node, isLast) { + self.addChild(new Node(label, node, isLast)); + }); + + self.isEmpty = !Boolean(childNodes.length); + if (self.isEmpty) { + el.classList.add('jsontree_node_empty'); + } + } + + utils.inherits(_NodeComplex, _NodeSimple); + + utils.extend(_NodeComplex.prototype, { + constructor : _NodeComplex, + + /* + * Add child node to list of child nodes + * + * @param child {Node} - child node + */ + addChild : function(child) { + this.childNodes.push(child); + this.childNodesUl.appendChild(child.el); + child.parent = this; + }, + + /* + * Expands this list of node child nodes + * + * @param isRecursive {boolean} - if true, expands all child nodes + */ + expand : function(isRecursive){ + if (this.isEmpty) { + return; + } + + if (!this.isRoot) { + this.el.classList.add('jsontree_node_expanded'); + } + + if (isRecursive) { + this.childNodes.forEach(function(item, i) { + if (item.isComplex) { + item.expand(isRecursive); + } + }); + } + }, + + /* + * Collapses this list of node child nodes + * + * @param isRecursive {boolean} - if true, collapses all child nodes + */ + collapse : function(isRecursive) { + if (this.isEmpty) { + return; + } + + if (!this.isRoot) { + this.el.classList.remove('jsontree_node_expanded'); + } + + if (isRecursive) { + this.childNodes.forEach(function(item, i) { + if (item.isComplex) { + item.collapse(isRecursive); + } + }); + } + }, + + /* + * Expands collapsed or collapses expanded node + * + * @param {boolean} isRecursive - Expand all child nodes if this node is expanded + * and collapse it otherwise + */ + toggle : function(isRecursive) { + if (this.isEmpty) { + return; + } + + this.el.classList.toggle('jsontree_node_expanded'); + + if (isRecursive) { + var isExpanded = this.el.classList.contains('jsontree_node_expanded'); + + this.childNodes.forEach(function(item, i) { + if (item.isComplex) { + item[isExpanded ? 'expand' : 'collapse'](isRecursive); + } + }); + } + }, + + /** + * Find child nodes that match some conditions and handle it + * + * @param {Function} matcher + * @param {Function} handler + * @param {boolean} isRecursive + */ + findChildren : function(matcher, handler, isRecursive) { + if (this.isEmpty) { + return; + } + + this.childNodes.forEach(function(item, i) { + if (matcher(item)) { + handler(item); + } + + if (item.isComplex && isRecursive) { + item.findChildren(matcher, handler, isRecursive); + } + }); + } + }); + + + /* + * The constructor for object values + * {... + * [+] "label": object, + * ...} + * object = {"abc": "def"} + * + * @constructor + * @param label {string} - key name + * @param val {Object} - value of object type, {"abc": "def"} + * @param isLast {boolean} - true if node is last in list of siblings + */ + function NodeObject(label, val, isLast) { + this.sym = ['{', '}']; + this.type = "object"; + + _NodeComplex.call(this, label, val, isLast); + } + utils.inherits(NodeObject,_NodeComplex); + + + /* + * The constructor for array values + * {... + * [+] "label": array, + * ...} + * array = [1,2,3] + * + * @constructor + * @param label {string} - key name + * @param val {Array} - value of array type, [1,2,3] + * @param isLast {boolean} - true if node is last in list of siblings + */ + function NodeArray(label, val, isLast) { + this.sym = ['[', ']']; + this.type = "array"; + + _NodeComplex.call(this, label, val, isLast); + } + utils.inherits(NodeArray, _NodeComplex); + + + /* ---------- The tree constructor ---------- */ + + /* + * The constructor for json tree. + * It contains only one Node (Array or Object), without property name. + * CSS-styles of .tree define main tree styles like font-family, + * font-size and own margins. + * + * Markup: + *
          + * {Node} + *
        + * + * @constructor + * @param jsonObj {Object | Array} - data for tree + * @param domEl {DOMElement} - DOM-element, wrapper for tree + */ + function Tree(jsonObj, domEl) { + this.wrapper = document.createElement('ul'); + this.wrapper.className = 'jsontree_tree clearfix'; + + this.rootNode = null; + + this.sourceJSONObj = jsonObj; + + this.loadData(jsonObj); + this.appendTo(domEl); + } + + Tree.prototype = { + constructor : Tree, + + /** + * Fill new data in current json tree + * + * @param {Object | Array} jsonObj - json-data + */ + loadData : function(jsonObj) { + if (!utils.isValidRoot(jsonObj)) { + alert('The root should be an object or an array'); + return; + } + + this.sourceJSONObj = jsonObj; + + this.rootNode = new Node(null, jsonObj, 'last'); + this.wrapper.innerHTML = ''; + this.wrapper.appendChild(this.rootNode.el); + }, + + /** + * Appends tree to DOM-element (or move it to new place) + * + * @param {DOMElement} domEl + */ + appendTo : function(domEl) { + domEl.appendChild(this.wrapper); + }, + + /** + * Expands all tree nodes (objects or arrays) recursively + * + * @param {Function} filterFunc - 'true' if this node should be expanded + */ + expand : function(filterFunc) { + if (this.rootNode.isComplex) { + if (typeof filterFunc == 'function') { + this.rootNode.childNodes.forEach(function(item, i) { + if (item.isComplex && filterFunc(item)) { + item.expand(); + } + }); + } else { + this.rootNode.expand('recursive'); + } + } + }, + + /** + * Collapses all tree nodes (objects or arrays) recursively + */ + collapse : function() { + if (typeof this.rootNode.collapse === 'function') { + this.rootNode.collapse('recursive'); + } + }, + + /** + * Returns the source json-string (pretty-printed) + * + * @param {boolean} isPrettyPrinted - 'true' for pretty-printed string + * @returns {string} - for exemple, '{"a":2,"b":3}' + */ + toSourceJSON : function(isPrettyPrinted) { + if (!isPrettyPrinted) { + return JSON.stringify(this.sourceJSONObj); + } + + var DELIMETER = "[%^$#$%^%]", + jsonStr = JSON.stringify(this.sourceJSONObj, null, DELIMETER); + + jsonStr = jsonStr.split("\n").join("
        "); + jsonStr = jsonStr.split(DELIMETER).join("    "); + + return jsonStr; + }, + + /** + * Find all nodes that match some conditions and handle it + */ + findAndHandle : function(matcher, handler) { + this.rootNode.findChildren(matcher, handler, 'isRecursive'); + }, + + /** + * Unmark all nodes + */ + unmarkAll : function() { + this.rootNode.findChildren(function(node) { + return true; + }, function(node) { + node.unmark(); + }, 'isRecursive'); + } + }; + + + /* ---------- Public methods ---------- */ + return { + /** + * Creates new tree by data and appends it to the DOM-element + * + * @param jsonObj {Object | Array} - json-data + * @param domEl {DOMElement} - the wrapper element + * @returns {Tree} + */ + create : function(jsonObj, domEl) { + return new Tree(jsonObj, domEl); + } + }; +})(); diff --git a/pkg/http-server/assets/moment.js b/pkg/http-server/assets/moment.js new file mode 100644 index 000000000..43bb38035 --- /dev/null +++ b/pkg/http-server/assets/moment.js @@ -0,0 +1,5670 @@ +//! moment.js +//! version : 2.29.1 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com + +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + global.moment = factory() +}(this, (function () { 'use strict'; + + var hookCallback; + + function hooks() { + return hookCallback.apply(null, arguments); + } + + // This is done to register the method called with moment() + // without creating circular dependencies. + function setHookCallback(callback) { + hookCallback = callback; + } + + function isArray(input) { + return ( + input instanceof Array || + Object.prototype.toString.call(input) === '[object Array]' + ); + } + + function isObject(input) { + // IE8 will treat undefined and null as object if it wasn't for + // input != null + return ( + input != null && + Object.prototype.toString.call(input) === '[object Object]' + ); + } + + function hasOwnProp(a, b) { + return Object.prototype.hasOwnProperty.call(a, b); + } + + function isObjectEmpty(obj) { + if (Object.getOwnPropertyNames) { + return Object.getOwnPropertyNames(obj).length === 0; + } else { + var k; + for (k in obj) { + if (hasOwnProp(obj, k)) { + return false; + } + } + return true; + } + } + + function isUndefined(input) { + return input === void 0; + } + + function isNumber(input) { + return ( + typeof input === 'number' || + Object.prototype.toString.call(input) === '[object Number]' + ); + } + + function isDate(input) { + return ( + input instanceof Date || + Object.prototype.toString.call(input) === '[object Date]' + ); + } + + function map(arr, fn) { + var res = [], + i; + for (i = 0; i < arr.length; ++i) { + res.push(fn(arr[i], i)); + } + return res; + } + + function extend(a, b) { + for (var i in b) { + if (hasOwnProp(b, i)) { + a[i] = b[i]; + } + } + + if (hasOwnProp(b, 'toString')) { + a.toString = b.toString; + } + + if (hasOwnProp(b, 'valueOf')) { + a.valueOf = b.valueOf; + } + + return a; + } + + function createUTC(input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, true).utc(); + } + + function defaultParsingFlags() { + // We need to deep clone this object. + return { + empty: false, + unusedTokens: [], + unusedInput: [], + overflow: -2, + charsLeftOver: 0, + nullInput: false, + invalidEra: null, + invalidMonth: null, + invalidFormat: false, + userInvalidated: false, + iso: false, + parsedDateParts: [], + era: null, + meridiem: null, + rfc2822: false, + weekdayMismatch: false, + }; + } + + function getParsingFlags(m) { + if (m._pf == null) { + m._pf = defaultParsingFlags(); + } + return m._pf; + } + + var some; + if (Array.prototype.some) { + some = Array.prototype.some; + } else { + some = function (fun) { + var t = Object(this), + len = t.length >>> 0, + i; + + for (i = 0; i < len; i++) { + if (i in t && fun.call(this, t[i], i, t)) { + return true; + } + } + + return false; + }; + } + + function isValid(m) { + if (m._isValid == null) { + var flags = getParsingFlags(m), + parsedParts = some.call(flags.parsedDateParts, function (i) { + return i != null; + }), + isNowValid = + !isNaN(m._d.getTime()) && + flags.overflow < 0 && + !flags.empty && + !flags.invalidEra && + !flags.invalidMonth && + !flags.invalidWeekday && + !flags.weekdayMismatch && + !flags.nullInput && + !flags.invalidFormat && + !flags.userInvalidated && + (!flags.meridiem || (flags.meridiem && parsedParts)); + + if (m._strict) { + isNowValid = + isNowValid && + flags.charsLeftOver === 0 && + flags.unusedTokens.length === 0 && + flags.bigHour === undefined; + } + + if (Object.isFrozen == null || !Object.isFrozen(m)) { + m._isValid = isNowValid; + } else { + return isNowValid; + } + } + return m._isValid; + } + + function createInvalid(flags) { + var m = createUTC(NaN); + if (flags != null) { + extend(getParsingFlags(m), flags); + } else { + getParsingFlags(m).userInvalidated = true; + } + + return m; + } + + // Plugins that add properties should also add the key here (null value), + // so we can properly clone ourselves. + var momentProperties = (hooks.momentProperties = []), + updateInProgress = false; + + function copyConfig(to, from) { + var i, prop, val; + + if (!isUndefined(from._isAMomentObject)) { + to._isAMomentObject = from._isAMomentObject; + } + if (!isUndefined(from._i)) { + to._i = from._i; + } + if (!isUndefined(from._f)) { + to._f = from._f; + } + if (!isUndefined(from._l)) { + to._l = from._l; + } + if (!isUndefined(from._strict)) { + to._strict = from._strict; + } + if (!isUndefined(from._tzm)) { + to._tzm = from._tzm; + } + if (!isUndefined(from._isUTC)) { + to._isUTC = from._isUTC; + } + if (!isUndefined(from._offset)) { + to._offset = from._offset; + } + if (!isUndefined(from._pf)) { + to._pf = getParsingFlags(from); + } + if (!isUndefined(from._locale)) { + to._locale = from._locale; + } + + if (momentProperties.length > 0) { + for (i = 0; i < momentProperties.length; i++) { + prop = momentProperties[i]; + val = from[prop]; + if (!isUndefined(val)) { + to[prop] = val; + } + } + } + + return to; + } + + // Moment prototype object + function Moment(config) { + copyConfig(this, config); + this._d = new Date(config._d != null ? config._d.getTime() : NaN); + if (!this.isValid()) { + this._d = new Date(NaN); + } + // Prevent infinite loop in case updateOffset creates new moment + // objects. + if (updateInProgress === false) { + updateInProgress = true; + hooks.updateOffset(this); + updateInProgress = false; + } + } + + function isMoment(obj) { + return ( + obj instanceof Moment || (obj != null && obj._isAMomentObject != null) + ); + } + + function warn(msg) { + if ( + hooks.suppressDeprecationWarnings === false && + typeof console !== 'undefined' && + console.warn + ) { + console.warn('Deprecation warning: ' + msg); + } + } + + function deprecate(msg, fn) { + var firstTime = true; + + return extend(function () { + if (hooks.deprecationHandler != null) { + hooks.deprecationHandler(null, msg); + } + if (firstTime) { + var args = [], + arg, + i, + key; + for (i = 0; i < arguments.length; i++) { + arg = ''; + if (typeof arguments[i] === 'object') { + arg += '\n[' + i + '] '; + for (key in arguments[0]) { + if (hasOwnProp(arguments[0], key)) { + arg += key + ': ' + arguments[0][key] + ', '; + } + } + arg = arg.slice(0, -2); // Remove trailing comma and space + } else { + arg = arguments[i]; + } + args.push(arg); + } + warn( + msg + + '\nArguments: ' + + Array.prototype.slice.call(args).join('') + + '\n' + + new Error().stack + ); + firstTime = false; + } + return fn.apply(this, arguments); + }, fn); + } + + var deprecations = {}; + + function deprecateSimple(name, msg) { + if (hooks.deprecationHandler != null) { + hooks.deprecationHandler(name, msg); + } + if (!deprecations[name]) { + warn(msg); + deprecations[name] = true; + } + } + + hooks.suppressDeprecationWarnings = false; + hooks.deprecationHandler = null; + + function isFunction(input) { + return ( + (typeof Function !== 'undefined' && input instanceof Function) || + Object.prototype.toString.call(input) === '[object Function]' + ); + } + + function set(config) { + var prop, i; + for (i in config) { + if (hasOwnProp(config, i)) { + prop = config[i]; + if (isFunction(prop)) { + this[i] = prop; + } else { + this['_' + i] = prop; + } + } + } + this._config = config; + // Lenient ordinal parsing accepts just a number in addition to + // number + (possibly) stuff coming from _dayOfMonthOrdinalParse. + // TODO: Remove "ordinalParse" fallback in next major release. + this._dayOfMonthOrdinalParseLenient = new RegExp( + (this._dayOfMonthOrdinalParse.source || this._ordinalParse.source) + + '|' + + /\d{1,2}/.source + ); + } + + function mergeConfigs(parentConfig, childConfig) { + var res = extend({}, parentConfig), + prop; + for (prop in childConfig) { + if (hasOwnProp(childConfig, prop)) { + if (isObject(parentConfig[prop]) && isObject(childConfig[prop])) { + res[prop] = {}; + extend(res[prop], parentConfig[prop]); + extend(res[prop], childConfig[prop]); + } else if (childConfig[prop] != null) { + res[prop] = childConfig[prop]; + } else { + delete res[prop]; + } + } + } + for (prop in parentConfig) { + if ( + hasOwnProp(parentConfig, prop) && + !hasOwnProp(childConfig, prop) && + isObject(parentConfig[prop]) + ) { + // make sure changes to properties don't modify parent config + res[prop] = extend({}, res[prop]); + } + } + return res; + } + + function Locale(config) { + if (config != null) { + this.set(config); + } + } + + var keys; + + if (Object.keys) { + keys = Object.keys; + } else { + keys = function (obj) { + var i, + res = []; + for (i in obj) { + if (hasOwnProp(obj, i)) { + res.push(i); + } + } + return res; + }; + } + + var defaultCalendar = { + sameDay: '[Today at] LT', + nextDay: '[Tomorrow at] LT', + nextWeek: 'dddd [at] LT', + lastDay: '[Yesterday at] LT', + lastWeek: '[Last] dddd [at] LT', + sameElse: 'L', + }; + + function calendar(key, mom, now) { + var output = this._calendar[key] || this._calendar['sameElse']; + return isFunction(output) ? output.call(mom, now) : output; + } + + function zeroFill(number, targetLength, forceSign) { + var absNumber = '' + Math.abs(number), + zerosToFill = targetLength - absNumber.length, + sign = number >= 0; + return ( + (sign ? (forceSign ? '+' : '') : '-') + + Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + + absNumber + ); + } + + var formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g, + localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, + formatFunctions = {}, + formatTokenFunctions = {}; + + // token: 'M' + // padded: ['MM', 2] + // ordinal: 'Mo' + // callback: function () { this.month() + 1 } + function addFormatToken(token, padded, ordinal, callback) { + var func = callback; + if (typeof callback === 'string') { + func = function () { + return this[callback](); + }; + } + if (token) { + formatTokenFunctions[token] = func; + } + if (padded) { + formatTokenFunctions[padded[0]] = function () { + return zeroFill(func.apply(this, arguments), padded[1], padded[2]); + }; + } + if (ordinal) { + formatTokenFunctions[ordinal] = function () { + return this.localeData().ordinal( + func.apply(this, arguments), + token + ); + }; + } + } + + function removeFormattingTokens(input) { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ''); + } + return input.replace(/\\/g, ''); + } + + function makeFormatFunction(format) { + var array = format.match(formattingTokens), + i, + length; + + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } + } + + return function (mom) { + var output = '', + i; + for (i = 0; i < length; i++) { + output += isFunction(array[i]) + ? array[i].call(mom, format) + : array[i]; + } + return output; + }; + } + + // format date using native date object + function formatMoment(m, format) { + if (!m.isValid()) { + return m.localeData().invalidDate(); + } + + format = expandFormat(format, m.localeData()); + formatFunctions[format] = + formatFunctions[format] || makeFormatFunction(format); + + return formatFunctions[format](m); + } + + function expandFormat(format, locale) { + var i = 5; + + function replaceLongDateFormatTokens(input) { + return locale.longDateFormat(input) || input; + } + + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace( + localFormattingTokens, + replaceLongDateFormatTokens + ); + localFormattingTokens.lastIndex = 0; + i -= 1; + } + + return format; + } + + var defaultLongDateFormat = { + LTS: 'h:mm:ss A', + LT: 'h:mm A', + L: 'MM/DD/YYYY', + LL: 'MMMM D, YYYY', + LLL: 'MMMM D, YYYY h:mm A', + LLLL: 'dddd, MMMM D, YYYY h:mm A', + }; + + function longDateFormat(key) { + var format = this._longDateFormat[key], + formatUpper = this._longDateFormat[key.toUpperCase()]; + + if (format || !formatUpper) { + return format; + } + + this._longDateFormat[key] = formatUpper + .match(formattingTokens) + .map(function (tok) { + if ( + tok === 'MMMM' || + tok === 'MM' || + tok === 'DD' || + tok === 'dddd' + ) { + return tok.slice(1); + } + return tok; + }) + .join(''); + + return this._longDateFormat[key]; + } + + var defaultInvalidDate = 'Invalid date'; + + function invalidDate() { + return this._invalidDate; + } + + var defaultOrdinal = '%d', + defaultDayOfMonthOrdinalParse = /\d{1,2}/; + + function ordinal(number) { + return this._ordinal.replace('%d', number); + } + + var defaultRelativeTime = { + future: 'in %s', + past: '%s ago', + s: 'a few seconds', + ss: '%d seconds', + m: 'a minute', + mm: '%d minutes', + h: 'an hour', + hh: '%d hours', + d: 'a day', + dd: '%d days', + w: 'a week', + ww: '%d weeks', + M: 'a month', + MM: '%d months', + y: 'a year', + yy: '%d years', + }; + + function relativeTime(number, withoutSuffix, string, isFuture) { + var output = this._relativeTime[string]; + return isFunction(output) + ? output(number, withoutSuffix, string, isFuture) + : output.replace(/%d/i, number); + } + + function pastFuture(diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return isFunction(format) ? format(output) : format.replace(/%s/i, output); + } + + var aliases = {}; + + function addUnitAlias(unit, shorthand) { + var lowerCase = unit.toLowerCase(); + aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; + } + + function normalizeUnits(units) { + return typeof units === 'string' + ? aliases[units] || aliases[units.toLowerCase()] + : undefined; + } + + function normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; + + for (prop in inputObject) { + if (hasOwnProp(inputObject, prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } + } + + return normalizedInput; + } + + var priorities = {}; + + function addUnitPriority(unit, priority) { + priorities[unit] = priority; + } + + function getPrioritizedUnits(unitsObj) { + var units = [], + u; + for (u in unitsObj) { + if (hasOwnProp(unitsObj, u)) { + units.push({ unit: u, priority: priorities[u] }); + } + } + units.sort(function (a, b) { + return a.priority - b.priority; + }); + return units; + } + + function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + } + + function absFloor(number) { + if (number < 0) { + // -0 -> 0 + return Math.ceil(number) || 0; + } else { + return Math.floor(number); + } + } + + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; + + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + value = absFloor(coercedNumber); + } + + return value; + } + + function makeGetSet(unit, keepTime) { + return function (value) { + if (value != null) { + set$1(this, unit, value); + hooks.updateOffset(this, keepTime); + return this; + } else { + return get(this, unit); + } + }; + } + + function get(mom, unit) { + return mom.isValid() + ? mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]() + : NaN; + } + + function set$1(mom, unit, value) { + if (mom.isValid() && !isNaN(value)) { + if ( + unit === 'FullYear' && + isLeapYear(mom.year()) && + mom.month() === 1 && + mom.date() === 29 + ) { + value = toInt(value); + mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit]( + value, + mom.month(), + daysInMonth(value, mom.month()) + ); + } else { + mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + } + } + } + + // MOMENTS + + function stringGet(units) { + units = normalizeUnits(units); + if (isFunction(this[units])) { + return this[units](); + } + return this; + } + + function stringSet(units, value) { + if (typeof units === 'object') { + units = normalizeObjectUnits(units); + var prioritized = getPrioritizedUnits(units), + i; + for (i = 0; i < prioritized.length; i++) { + this[prioritized[i].unit](units[prioritized[i].unit]); + } + } else { + units = normalizeUnits(units); + if (isFunction(this[units])) { + return this[units](value); + } + } + return this; + } + + var match1 = /\d/, // 0 - 9 + match2 = /\d\d/, // 00 - 99 + match3 = /\d{3}/, // 000 - 999 + match4 = /\d{4}/, // 0000 - 9999 + match6 = /[+-]?\d{6}/, // -999999 - 999999 + match1to2 = /\d\d?/, // 0 - 99 + match3to4 = /\d\d\d\d?/, // 999 - 9999 + match5to6 = /\d\d\d\d\d\d?/, // 99999 - 999999 + match1to3 = /\d{1,3}/, // 0 - 999 + match1to4 = /\d{1,4}/, // 0 - 9999 + match1to6 = /[+-]?\d{1,6}/, // -999999 - 999999 + matchUnsigned = /\d+/, // 0 - inf + matchSigned = /[+-]?\d+/, // -inf - inf + matchOffset = /Z|[+-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z + matchShortOffset = /Z|[+-]\d\d(?::?\d\d)?/gi, // +00 -00 +00:00 -00:00 +0000 -0000 or Z + matchTimestamp = /[+-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123 + // any word (or two) characters or numbers including two/three word month in arabic. + // includes scottish gaelic two word and hyphenated months + matchWord = /[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i, + regexes; + + regexes = {}; + + function addRegexToken(token, regex, strictRegex) { + regexes[token] = isFunction(regex) + ? regex + : function (isStrict, localeData) { + return isStrict && strictRegex ? strictRegex : regex; + }; + } + + function getParseRegexForToken(token, config) { + if (!hasOwnProp(regexes, token)) { + return new RegExp(unescapeFormat(token)); + } + + return regexes[token](config._strict, config._locale); + } + + // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + function unescapeFormat(s) { + return regexEscape( + s + .replace('\\', '') + .replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function ( + matched, + p1, + p2, + p3, + p4 + ) { + return p1 || p2 || p3 || p4; + }) + ); + } + + function regexEscape(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } + + var tokens = {}; + + function addParseToken(token, callback) { + var i, + func = callback; + if (typeof token === 'string') { + token = [token]; + } + if (isNumber(callback)) { + func = function (input, array) { + array[callback] = toInt(input); + }; + } + for (i = 0; i < token.length; i++) { + tokens[token[i]] = func; + } + } + + function addWeekParseToken(token, callback) { + addParseToken(token, function (input, array, config, token) { + config._w = config._w || {}; + callback(input, config._w, config, token); + }); + } + + function addTimeToArrayFromToken(token, input, config) { + if (input != null && hasOwnProp(tokens, token)) { + tokens[token](input, config._a, config, token); + } + } + + var YEAR = 0, + MONTH = 1, + DATE = 2, + HOUR = 3, + MINUTE = 4, + SECOND = 5, + MILLISECOND = 6, + WEEK = 7, + WEEKDAY = 8; + + function mod(n, x) { + return ((n % x) + x) % x; + } + + var indexOf; + + if (Array.prototype.indexOf) { + indexOf = Array.prototype.indexOf; + } else { + indexOf = function (o) { + // I know + var i; + for (i = 0; i < this.length; ++i) { + if (this[i] === o) { + return i; + } + } + return -1; + }; + } + + function daysInMonth(year, month) { + if (isNaN(year) || isNaN(month)) { + return NaN; + } + var modMonth = mod(month, 12); + year += (month - modMonth) / 12; + return modMonth === 1 + ? isLeapYear(year) + ? 29 + : 28 + : 31 - ((modMonth % 7) % 2); + } + + // FORMATTING + + addFormatToken('M', ['MM', 2], 'Mo', function () { + return this.month() + 1; + }); + + addFormatToken('MMM', 0, 0, function (format) { + return this.localeData().monthsShort(this, format); + }); + + addFormatToken('MMMM', 0, 0, function (format) { + return this.localeData().months(this, format); + }); + + // ALIASES + + addUnitAlias('month', 'M'); + + // PRIORITY + + addUnitPriority('month', 8); + + // PARSING + + addRegexToken('M', match1to2); + addRegexToken('MM', match1to2, match2); + addRegexToken('MMM', function (isStrict, locale) { + return locale.monthsShortRegex(isStrict); + }); + addRegexToken('MMMM', function (isStrict, locale) { + return locale.monthsRegex(isStrict); + }); + + addParseToken(['M', 'MM'], function (input, array) { + array[MONTH] = toInt(input) - 1; + }); + + addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { + var month = config._locale.monthsParse(input, token, config._strict); + // if we didn't find a month name, mark the date as invalid. + if (month != null) { + array[MONTH] = month; + } else { + getParsingFlags(config).invalidMonth = input; + } + }); + + // LOCALES + + var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split( + '_' + ), + defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split( + '_' + ), + MONTHS_IN_FORMAT = /D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/, + defaultMonthsShortRegex = matchWord, + defaultMonthsRegex = matchWord; + + function localeMonths(m, format) { + if (!m) { + return isArray(this._months) + ? this._months + : this._months['standalone']; + } + return isArray(this._months) + ? this._months[m.month()] + : this._months[ + (this._months.isFormat || MONTHS_IN_FORMAT).test(format) + ? 'format' + : 'standalone' + ][m.month()]; + } + + function localeMonthsShort(m, format) { + if (!m) { + return isArray(this._monthsShort) + ? this._monthsShort + : this._monthsShort['standalone']; + } + return isArray(this._monthsShort) + ? this._monthsShort[m.month()] + : this._monthsShort[ + MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone' + ][m.month()]; + } + + function handleStrictParse(monthName, format, strict) { + var i, + ii, + mom, + llc = monthName.toLocaleLowerCase(); + if (!this._monthsParse) { + // this is not used + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + for (i = 0; i < 12; ++i) { + mom = createUTC([2000, i]); + this._shortMonthsParse[i] = this.monthsShort( + mom, + '' + ).toLocaleLowerCase(); + this._longMonthsParse[i] = this.months(mom, '').toLocaleLowerCase(); + } + } + + if (strict) { + if (format === 'MMM') { + ii = indexOf.call(this._shortMonthsParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._longMonthsParse, llc); + return ii !== -1 ? ii : null; + } + } else { + if (format === 'MMM') { + ii = indexOf.call(this._shortMonthsParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._longMonthsParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._longMonthsParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._shortMonthsParse, llc); + return ii !== -1 ? ii : null; + } + } + } + + function localeMonthsParse(monthName, format, strict) { + var i, mom, regex; + + if (this._monthsParseExact) { + return handleStrictParse.call(this, monthName, format, strict); + } + + if (!this._monthsParse) { + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + } + + // TODO: add sorting + // Sorting makes sure if one month (or abbr) is a prefix of another + // see sorting in computeMonthsParse + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = createUTC([2000, i]); + if (strict && !this._longMonthsParse[i]) { + this._longMonthsParse[i] = new RegExp( + '^' + this.months(mom, '').replace('.', '') + '$', + 'i' + ); + this._shortMonthsParse[i] = new RegExp( + '^' + this.monthsShort(mom, '').replace('.', '') + '$', + 'i' + ); + } + if (!strict && !this._monthsParse[i]) { + regex = + '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); + this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if ( + strict && + format === 'MMMM' && + this._longMonthsParse[i].test(monthName) + ) { + return i; + } else if ( + strict && + format === 'MMM' && + this._shortMonthsParse[i].test(monthName) + ) { + return i; + } else if (!strict && this._monthsParse[i].test(monthName)) { + return i; + } + } + } + + // MOMENTS + + function setMonth(mom, value) { + var dayOfMonth; + + if (!mom.isValid()) { + // No op + return mom; + } + + if (typeof value === 'string') { + if (/^\d+$/.test(value)) { + value = toInt(value); + } else { + value = mom.localeData().monthsParse(value); + // TODO: Another silent failure? + if (!isNumber(value)) { + return mom; + } + } + } + + dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); + mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); + return mom; + } + + function getSetMonth(value) { + if (value != null) { + setMonth(this, value); + hooks.updateOffset(this, true); + return this; + } else { + return get(this, 'Month'); + } + } + + function getDaysInMonth() { + return daysInMonth(this.year(), this.month()); + } + + function monthsShortRegex(isStrict) { + if (this._monthsParseExact) { + if (!hasOwnProp(this, '_monthsRegex')) { + computeMonthsParse.call(this); + } + if (isStrict) { + return this._monthsShortStrictRegex; + } else { + return this._monthsShortRegex; + } + } else { + if (!hasOwnProp(this, '_monthsShortRegex')) { + this._monthsShortRegex = defaultMonthsShortRegex; + } + return this._monthsShortStrictRegex && isStrict + ? this._monthsShortStrictRegex + : this._monthsShortRegex; + } + } + + function monthsRegex(isStrict) { + if (this._monthsParseExact) { + if (!hasOwnProp(this, '_monthsRegex')) { + computeMonthsParse.call(this); + } + if (isStrict) { + return this._monthsStrictRegex; + } else { + return this._monthsRegex; + } + } else { + if (!hasOwnProp(this, '_monthsRegex')) { + this._monthsRegex = defaultMonthsRegex; + } + return this._monthsStrictRegex && isStrict + ? this._monthsStrictRegex + : this._monthsRegex; + } + } + + function computeMonthsParse() { + function cmpLenRev(a, b) { + return b.length - a.length; + } + + var shortPieces = [], + longPieces = [], + mixedPieces = [], + i, + mom; + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = createUTC([2000, i]); + shortPieces.push(this.monthsShort(mom, '')); + longPieces.push(this.months(mom, '')); + mixedPieces.push(this.months(mom, '')); + mixedPieces.push(this.monthsShort(mom, '')); + } + // Sorting makes sure if one month (or abbr) is a prefix of another it + // will match the longer piece. + shortPieces.sort(cmpLenRev); + longPieces.sort(cmpLenRev); + mixedPieces.sort(cmpLenRev); + for (i = 0; i < 12; i++) { + shortPieces[i] = regexEscape(shortPieces[i]); + longPieces[i] = regexEscape(longPieces[i]); + } + for (i = 0; i < 24; i++) { + mixedPieces[i] = regexEscape(mixedPieces[i]); + } + + this._monthsRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); + this._monthsShortRegex = this._monthsRegex; + this._monthsStrictRegex = new RegExp( + '^(' + longPieces.join('|') + ')', + 'i' + ); + this._monthsShortStrictRegex = new RegExp( + '^(' + shortPieces.join('|') + ')', + 'i' + ); + } + + // FORMATTING + + addFormatToken('Y', 0, 0, function () { + var y = this.year(); + return y <= 9999 ? zeroFill(y, 4) : '+' + y; + }); + + addFormatToken(0, ['YY', 2], 0, function () { + return this.year() % 100; + }); + + addFormatToken(0, ['YYYY', 4], 0, 'year'); + addFormatToken(0, ['YYYYY', 5], 0, 'year'); + addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); + + // ALIASES + + addUnitAlias('year', 'y'); + + // PRIORITIES + + addUnitPriority('year', 1); + + // PARSING + + addRegexToken('Y', matchSigned); + addRegexToken('YY', match1to2, match2); + addRegexToken('YYYY', match1to4, match4); + addRegexToken('YYYYY', match1to6, match6); + addRegexToken('YYYYYY', match1to6, match6); + + addParseToken(['YYYYY', 'YYYYYY'], YEAR); + addParseToken('YYYY', function (input, array) { + array[YEAR] = + input.length === 2 ? hooks.parseTwoDigitYear(input) : toInt(input); + }); + addParseToken('YY', function (input, array) { + array[YEAR] = hooks.parseTwoDigitYear(input); + }); + addParseToken('Y', function (input, array) { + array[YEAR] = parseInt(input, 10); + }); + + // HELPERS + + function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; + } + + // HOOKS + + hooks.parseTwoDigitYear = function (input) { + return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); + }; + + // MOMENTS + + var getSetYear = makeGetSet('FullYear', true); + + function getIsLeapYear() { + return isLeapYear(this.year()); + } + + function createDate(y, m, d, h, M, s, ms) { + // can't just apply() to create a date: + // https://stackoverflow.com/q/181348 + var date; + // the date constructor remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0) { + // preserve leap years using a full 400 year cycle, then reset + date = new Date(y + 400, m, d, h, M, s, ms); + if (isFinite(date.getFullYear())) { + date.setFullYear(y); + } + } else { + date = new Date(y, m, d, h, M, s, ms); + } + + return date; + } + + function createUTCDate(y) { + var date, args; + // the Date.UTC function remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0) { + args = Array.prototype.slice.call(arguments); + // preserve leap years using a full 400 year cycle, then reset + args[0] = y + 400; + date = new Date(Date.UTC.apply(null, args)); + if (isFinite(date.getUTCFullYear())) { + date.setUTCFullYear(y); + } + } else { + date = new Date(Date.UTC.apply(null, arguments)); + } + + return date; + } + + // start-of-first-week - start-of-year + function firstWeekOffset(year, dow, doy) { + var // first-week day -- which january is always in the first week (4 for iso, 1 for other) + fwd = 7 + dow - doy, + // first-week day local weekday -- which local weekday is fwd + fwdlw = (7 + createUTCDate(year, 0, fwd).getUTCDay() - dow) % 7; + + return -fwdlw + fwd - 1; + } + + // https://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday + function dayOfYearFromWeeks(year, week, weekday, dow, doy) { + var localWeekday = (7 + weekday - dow) % 7, + weekOffset = firstWeekOffset(year, dow, doy), + dayOfYear = 1 + 7 * (week - 1) + localWeekday + weekOffset, + resYear, + resDayOfYear; + + if (dayOfYear <= 0) { + resYear = year - 1; + resDayOfYear = daysInYear(resYear) + dayOfYear; + } else if (dayOfYear > daysInYear(year)) { + resYear = year + 1; + resDayOfYear = dayOfYear - daysInYear(year); + } else { + resYear = year; + resDayOfYear = dayOfYear; + } + + return { + year: resYear, + dayOfYear: resDayOfYear, + }; + } + + function weekOfYear(mom, dow, doy) { + var weekOffset = firstWeekOffset(mom.year(), dow, doy), + week = Math.floor((mom.dayOfYear() - weekOffset - 1) / 7) + 1, + resWeek, + resYear; + + if (week < 1) { + resYear = mom.year() - 1; + resWeek = week + weeksInYear(resYear, dow, doy); + } else if (week > weeksInYear(mom.year(), dow, doy)) { + resWeek = week - weeksInYear(mom.year(), dow, doy); + resYear = mom.year() + 1; + } else { + resYear = mom.year(); + resWeek = week; + } + + return { + week: resWeek, + year: resYear, + }; + } + + function weeksInYear(year, dow, doy) { + var weekOffset = firstWeekOffset(year, dow, doy), + weekOffsetNext = firstWeekOffset(year + 1, dow, doy); + return (daysInYear(year) - weekOffset + weekOffsetNext) / 7; + } + + // FORMATTING + + addFormatToken('w', ['ww', 2], 'wo', 'week'); + addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); + + // ALIASES + + addUnitAlias('week', 'w'); + addUnitAlias('isoWeek', 'W'); + + // PRIORITIES + + addUnitPriority('week', 5); + addUnitPriority('isoWeek', 5); + + // PARSING + + addRegexToken('w', match1to2); + addRegexToken('ww', match1to2, match2); + addRegexToken('W', match1to2); + addRegexToken('WW', match1to2, match2); + + addWeekParseToken(['w', 'ww', 'W', 'WW'], function ( + input, + week, + config, + token + ) { + week[token.substr(0, 1)] = toInt(input); + }); + + // HELPERS + + // LOCALES + + function localeWeek(mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; + } + + var defaultLocaleWeek = { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 6th is the first week of the year. + }; + + function localeFirstDayOfWeek() { + return this._week.dow; + } + + function localeFirstDayOfYear() { + return this._week.doy; + } + + // MOMENTS + + function getSetWeek(input) { + var week = this.localeData().week(this); + return input == null ? week : this.add((input - week) * 7, 'd'); + } + + function getSetISOWeek(input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add((input - week) * 7, 'd'); + } + + // FORMATTING + + addFormatToken('d', 0, 'do', 'day'); + + addFormatToken('dd', 0, 0, function (format) { + return this.localeData().weekdaysMin(this, format); + }); + + addFormatToken('ddd', 0, 0, function (format) { + return this.localeData().weekdaysShort(this, format); + }); + + addFormatToken('dddd', 0, 0, function (format) { + return this.localeData().weekdays(this, format); + }); + + addFormatToken('e', 0, 0, 'weekday'); + addFormatToken('E', 0, 0, 'isoWeekday'); + + // ALIASES + + addUnitAlias('day', 'd'); + addUnitAlias('weekday', 'e'); + addUnitAlias('isoWeekday', 'E'); + + // PRIORITY + addUnitPriority('day', 11); + addUnitPriority('weekday', 11); + addUnitPriority('isoWeekday', 11); + + // PARSING + + addRegexToken('d', match1to2); + addRegexToken('e', match1to2); + addRegexToken('E', match1to2); + addRegexToken('dd', function (isStrict, locale) { + return locale.weekdaysMinRegex(isStrict); + }); + addRegexToken('ddd', function (isStrict, locale) { + return locale.weekdaysShortRegex(isStrict); + }); + addRegexToken('dddd', function (isStrict, locale) { + return locale.weekdaysRegex(isStrict); + }); + + addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config, token) { + var weekday = config._locale.weekdaysParse(input, token, config._strict); + // if we didn't get a weekday name, mark the date as invalid + if (weekday != null) { + week.d = weekday; + } else { + getParsingFlags(config).invalidWeekday = input; + } + }); + + addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { + week[token] = toInt(input); + }); + + // HELPERS + + function parseWeekday(input, locale) { + if (typeof input !== 'string') { + return input; + } + + if (!isNaN(input)) { + return parseInt(input, 10); + } + + input = locale.weekdaysParse(input); + if (typeof input === 'number') { + return input; + } + + return null; + } + + function parseIsoWeekday(input, locale) { + if (typeof input === 'string') { + return locale.weekdaysParse(input) % 7 || 7; + } + return isNaN(input) ? null : input; + } + + // LOCALES + function shiftWeekdays(ws, n) { + return ws.slice(n, 7).concat(ws.slice(0, n)); + } + + var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split( + '_' + ), + defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), + defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), + defaultWeekdaysRegex = matchWord, + defaultWeekdaysShortRegex = matchWord, + defaultWeekdaysMinRegex = matchWord; + + function localeWeekdays(m, format) { + var weekdays = isArray(this._weekdays) + ? this._weekdays + : this._weekdays[ + m && m !== true && this._weekdays.isFormat.test(format) + ? 'format' + : 'standalone' + ]; + return m === true + ? shiftWeekdays(weekdays, this._week.dow) + : m + ? weekdays[m.day()] + : weekdays; + } + + function localeWeekdaysShort(m) { + return m === true + ? shiftWeekdays(this._weekdaysShort, this._week.dow) + : m + ? this._weekdaysShort[m.day()] + : this._weekdaysShort; + } + + function localeWeekdaysMin(m) { + return m === true + ? shiftWeekdays(this._weekdaysMin, this._week.dow) + : m + ? this._weekdaysMin[m.day()] + : this._weekdaysMin; + } + + function handleStrictParse$1(weekdayName, format, strict) { + var i, + ii, + mom, + llc = weekdayName.toLocaleLowerCase(); + if (!this._weekdaysParse) { + this._weekdaysParse = []; + this._shortWeekdaysParse = []; + this._minWeekdaysParse = []; + + for (i = 0; i < 7; ++i) { + mom = createUTC([2000, 1]).day(i); + this._minWeekdaysParse[i] = this.weekdaysMin( + mom, + '' + ).toLocaleLowerCase(); + this._shortWeekdaysParse[i] = this.weekdaysShort( + mom, + '' + ).toLocaleLowerCase(); + this._weekdaysParse[i] = this.weekdays(mom, '').toLocaleLowerCase(); + } + } + + if (strict) { + if (format === 'dddd') { + ii = indexOf.call(this._weekdaysParse, llc); + return ii !== -1 ? ii : null; + } else if (format === 'ddd') { + ii = indexOf.call(this._shortWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._minWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } + } else { + if (format === 'dddd') { + ii = indexOf.call(this._weekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._shortWeekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._minWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } else if (format === 'ddd') { + ii = indexOf.call(this._shortWeekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._weekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._minWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._minWeekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._weekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._shortWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } + } + } + + function localeWeekdaysParse(weekdayName, format, strict) { + var i, mom, regex; + + if (this._weekdaysParseExact) { + return handleStrictParse$1.call(this, weekdayName, format, strict); + } + + if (!this._weekdaysParse) { + this._weekdaysParse = []; + this._minWeekdaysParse = []; + this._shortWeekdaysParse = []; + this._fullWeekdaysParse = []; + } + + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + + mom = createUTC([2000, 1]).day(i); + if (strict && !this._fullWeekdaysParse[i]) { + this._fullWeekdaysParse[i] = new RegExp( + '^' + this.weekdays(mom, '').replace('.', '\\.?') + '$', + 'i' + ); + this._shortWeekdaysParse[i] = new RegExp( + '^' + this.weekdaysShort(mom, '').replace('.', '\\.?') + '$', + 'i' + ); + this._minWeekdaysParse[i] = new RegExp( + '^' + this.weekdaysMin(mom, '').replace('.', '\\.?') + '$', + 'i' + ); + } + if (!this._weekdaysParse[i]) { + regex = + '^' + + this.weekdays(mom, '') + + '|^' + + this.weekdaysShort(mom, '') + + '|^' + + this.weekdaysMin(mom, ''); + this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if ( + strict && + format === 'dddd' && + this._fullWeekdaysParse[i].test(weekdayName) + ) { + return i; + } else if ( + strict && + format === 'ddd' && + this._shortWeekdaysParse[i].test(weekdayName) + ) { + return i; + } else if ( + strict && + format === 'dd' && + this._minWeekdaysParse[i].test(weekdayName) + ) { + return i; + } else if (!strict && this._weekdaysParse[i].test(weekdayName)) { + return i; + } + } + } + + // MOMENTS + + function getSetDayOfWeek(input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + if (input != null) { + input = parseWeekday(input, this.localeData()); + return this.add(input - day, 'd'); + } else { + return day; + } + } + + function getSetLocaleDayOfWeek(input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; + return input == null ? weekday : this.add(input - weekday, 'd'); + } + + function getSetISODayOfWeek(input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + + // behaves the same as moment#day except + // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) + // as a setter, sunday should belong to the previous week. + + if (input != null) { + var weekday = parseIsoWeekday(input, this.localeData()); + return this.day(this.day() % 7 ? weekday : weekday - 7); + } else { + return this.day() || 7; + } + } + + function weekdaysRegex(isStrict) { + if (this._weekdaysParseExact) { + if (!hasOwnProp(this, '_weekdaysRegex')) { + computeWeekdaysParse.call(this); + } + if (isStrict) { + return this._weekdaysStrictRegex; + } else { + return this._weekdaysRegex; + } + } else { + if (!hasOwnProp(this, '_weekdaysRegex')) { + this._weekdaysRegex = defaultWeekdaysRegex; + } + return this._weekdaysStrictRegex && isStrict + ? this._weekdaysStrictRegex + : this._weekdaysRegex; + } + } + + function weekdaysShortRegex(isStrict) { + if (this._weekdaysParseExact) { + if (!hasOwnProp(this, '_weekdaysRegex')) { + computeWeekdaysParse.call(this); + } + if (isStrict) { + return this._weekdaysShortStrictRegex; + } else { + return this._weekdaysShortRegex; + } + } else { + if (!hasOwnProp(this, '_weekdaysShortRegex')) { + this._weekdaysShortRegex = defaultWeekdaysShortRegex; + } + return this._weekdaysShortStrictRegex && isStrict + ? this._weekdaysShortStrictRegex + : this._weekdaysShortRegex; + } + } + + function weekdaysMinRegex(isStrict) { + if (this._weekdaysParseExact) { + if (!hasOwnProp(this, '_weekdaysRegex')) { + computeWeekdaysParse.call(this); + } + if (isStrict) { + return this._weekdaysMinStrictRegex; + } else { + return this._weekdaysMinRegex; + } + } else { + if (!hasOwnProp(this, '_weekdaysMinRegex')) { + this._weekdaysMinRegex = defaultWeekdaysMinRegex; + } + return this._weekdaysMinStrictRegex && isStrict + ? this._weekdaysMinStrictRegex + : this._weekdaysMinRegex; + } + } + + function computeWeekdaysParse() { + function cmpLenRev(a, b) { + return b.length - a.length; + } + + var minPieces = [], + shortPieces = [], + longPieces = [], + mixedPieces = [], + i, + mom, + minp, + shortp, + longp; + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + mom = createUTC([2000, 1]).day(i); + minp = regexEscape(this.weekdaysMin(mom, '')); + shortp = regexEscape(this.weekdaysShort(mom, '')); + longp = regexEscape(this.weekdays(mom, '')); + minPieces.push(minp); + shortPieces.push(shortp); + longPieces.push(longp); + mixedPieces.push(minp); + mixedPieces.push(shortp); + mixedPieces.push(longp); + } + // Sorting makes sure if one weekday (or abbr) is a prefix of another it + // will match the longer piece. + minPieces.sort(cmpLenRev); + shortPieces.sort(cmpLenRev); + longPieces.sort(cmpLenRev); + mixedPieces.sort(cmpLenRev); + + this._weekdaysRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); + this._weekdaysShortRegex = this._weekdaysRegex; + this._weekdaysMinRegex = this._weekdaysRegex; + + this._weekdaysStrictRegex = new RegExp( + '^(' + longPieces.join('|') + ')', + 'i' + ); + this._weekdaysShortStrictRegex = new RegExp( + '^(' + shortPieces.join('|') + ')', + 'i' + ); + this._weekdaysMinStrictRegex = new RegExp( + '^(' + minPieces.join('|') + ')', + 'i' + ); + } + + // FORMATTING + + function hFormat() { + return this.hours() % 12 || 12; + } + + function kFormat() { + return this.hours() || 24; + } + + addFormatToken('H', ['HH', 2], 0, 'hour'); + addFormatToken('h', ['hh', 2], 0, hFormat); + addFormatToken('k', ['kk', 2], 0, kFormat); + + addFormatToken('hmm', 0, 0, function () { + return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2); + }); + + addFormatToken('hmmss', 0, 0, function () { + return ( + '' + + hFormat.apply(this) + + zeroFill(this.minutes(), 2) + + zeroFill(this.seconds(), 2) + ); + }); + + addFormatToken('Hmm', 0, 0, function () { + return '' + this.hours() + zeroFill(this.minutes(), 2); + }); + + addFormatToken('Hmmss', 0, 0, function () { + return ( + '' + + this.hours() + + zeroFill(this.minutes(), 2) + + zeroFill(this.seconds(), 2) + ); + }); + + function meridiem(token, lowercase) { + addFormatToken(token, 0, 0, function () { + return this.localeData().meridiem( + this.hours(), + this.minutes(), + lowercase + ); + }); + } + + meridiem('a', true); + meridiem('A', false); + + // ALIASES + + addUnitAlias('hour', 'h'); + + // PRIORITY + addUnitPriority('hour', 13); + + // PARSING + + function matchMeridiem(isStrict, locale) { + return locale._meridiemParse; + } + + addRegexToken('a', matchMeridiem); + addRegexToken('A', matchMeridiem); + addRegexToken('H', match1to2); + addRegexToken('h', match1to2); + addRegexToken('k', match1to2); + addRegexToken('HH', match1to2, match2); + addRegexToken('hh', match1to2, match2); + addRegexToken('kk', match1to2, match2); + + addRegexToken('hmm', match3to4); + addRegexToken('hmmss', match5to6); + addRegexToken('Hmm', match3to4); + addRegexToken('Hmmss', match5to6); + + addParseToken(['H', 'HH'], HOUR); + addParseToken(['k', 'kk'], function (input, array, config) { + var kInput = toInt(input); + array[HOUR] = kInput === 24 ? 0 : kInput; + }); + addParseToken(['a', 'A'], function (input, array, config) { + config._isPm = config._locale.isPM(input); + config._meridiem = input; + }); + addParseToken(['h', 'hh'], function (input, array, config) { + array[HOUR] = toInt(input); + getParsingFlags(config).bigHour = true; + }); + addParseToken('hmm', function (input, array, config) { + var pos = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos)); + array[MINUTE] = toInt(input.substr(pos)); + getParsingFlags(config).bigHour = true; + }); + addParseToken('hmmss', function (input, array, config) { + var pos1 = input.length - 4, + pos2 = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos1)); + array[MINUTE] = toInt(input.substr(pos1, 2)); + array[SECOND] = toInt(input.substr(pos2)); + getParsingFlags(config).bigHour = true; + }); + addParseToken('Hmm', function (input, array, config) { + var pos = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos)); + array[MINUTE] = toInt(input.substr(pos)); + }); + addParseToken('Hmmss', function (input, array, config) { + var pos1 = input.length - 4, + pos2 = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos1)); + array[MINUTE] = toInt(input.substr(pos1, 2)); + array[SECOND] = toInt(input.substr(pos2)); + }); + + // LOCALES + + function localeIsPM(input) { + // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays + // Using charAt should be more compatible. + return (input + '').toLowerCase().charAt(0) === 'p'; + } + + var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i, + // Setting the hour should keep the time, because the user explicitly + // specified which hour they want. So trying to maintain the same hour (in + // a new timezone) makes sense. Adding/subtracting hours does not follow + // this rule. + getSetHour = makeGetSet('Hours', true); + + function localeMeridiem(hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + } + + var baseConfig = { + calendar: defaultCalendar, + longDateFormat: defaultLongDateFormat, + invalidDate: defaultInvalidDate, + ordinal: defaultOrdinal, + dayOfMonthOrdinalParse: defaultDayOfMonthOrdinalParse, + relativeTime: defaultRelativeTime, + + months: defaultLocaleMonths, + monthsShort: defaultLocaleMonthsShort, + + week: defaultLocaleWeek, + + weekdays: defaultLocaleWeekdays, + weekdaysMin: defaultLocaleWeekdaysMin, + weekdaysShort: defaultLocaleWeekdaysShort, + + meridiemParse: defaultLocaleMeridiemParse, + }; + + // internal storage for locale config files + var locales = {}, + localeFamilies = {}, + globalLocale; + + function commonPrefix(arr1, arr2) { + var i, + minl = Math.min(arr1.length, arr2.length); + for (i = 0; i < minl; i += 1) { + if (arr1[i] !== arr2[i]) { + return i; + } + } + return minl; + } + + function normalizeLocale(key) { + return key ? key.toLowerCase().replace('_', '-') : key; + } + + // pick the locale from the array + // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each + // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root + function chooseLocale(names) { + var i = 0, + j, + next, + locale, + split; + + while (i < names.length) { + split = normalizeLocale(names[i]).split('-'); + j = split.length; + next = normalizeLocale(names[i + 1]); + next = next ? next.split('-') : null; + while (j > 0) { + locale = loadLocale(split.slice(0, j).join('-')); + if (locale) { + return locale; + } + if ( + next && + next.length >= j && + commonPrefix(split, next) >= j - 1 + ) { + //the next array item is better than a shallower substring of this one + break; + } + j--; + } + i++; + } + return globalLocale; + } + + function loadLocale(name) { + var oldLocale = null, + aliasedRequire; + // TODO: Find a better way to register and load all the locales in Node + if ( + locales[name] === undefined && + typeof module !== 'undefined' && + module && + module.exports + ) { + try { + oldLocale = globalLocale._abbr; + aliasedRequire = require; + aliasedRequire('./locale/' + name); + getSetGlobalLocale(oldLocale); + } catch (e) { + // mark as not found to avoid repeating expensive file require call causing high CPU + // when trying to find en-US, en_US, en-us for every format call + locales[name] = null; // null means not found + } + } + return locales[name]; + } + + // This function will load locale and then set the global locale. If + // no arguments are passed in, it will simply return the current global + // locale key. + function getSetGlobalLocale(key, values) { + var data; + if (key) { + if (isUndefined(values)) { + data = getLocale(key); + } else { + data = defineLocale(key, values); + } + + if (data) { + // moment.duration._locale = moment._locale = data; + globalLocale = data; + } else { + if (typeof console !== 'undefined' && console.warn) { + //warn user if arguments are passed but the locale could not be set + console.warn( + 'Locale ' + key + ' not found. Did you forget to load it?' + ); + } + } + } + + return globalLocale._abbr; + } + + function defineLocale(name, config) { + if (config !== null) { + var locale, + parentConfig = baseConfig; + config.abbr = name; + if (locales[name] != null) { + deprecateSimple( + 'defineLocaleOverride', + 'use moment.updateLocale(localeName, config) to change ' + + 'an existing locale. moment.defineLocale(localeName, ' + + 'config) should only be used for creating a new locale ' + + 'See http://momentjs.com/guides/#/warnings/define-locale/ for more info.' + ); + parentConfig = locales[name]._config; + } else if (config.parentLocale != null) { + if (locales[config.parentLocale] != null) { + parentConfig = locales[config.parentLocale]._config; + } else { + locale = loadLocale(config.parentLocale); + if (locale != null) { + parentConfig = locale._config; + } else { + if (!localeFamilies[config.parentLocale]) { + localeFamilies[config.parentLocale] = []; + } + localeFamilies[config.parentLocale].push({ + name: name, + config: config, + }); + return null; + } + } + } + locales[name] = new Locale(mergeConfigs(parentConfig, config)); + + if (localeFamilies[name]) { + localeFamilies[name].forEach(function (x) { + defineLocale(x.name, x.config); + }); + } + + // backwards compat for now: also set the locale + // make sure we set the locale AFTER all child locales have been + // created, so we won't end up with the child locale set. + getSetGlobalLocale(name); + + return locales[name]; + } else { + // useful for testing + delete locales[name]; + return null; + } + } + + function updateLocale(name, config) { + if (config != null) { + var locale, + tmpLocale, + parentConfig = baseConfig; + + if (locales[name] != null && locales[name].parentLocale != null) { + // Update existing child locale in-place to avoid memory-leaks + locales[name].set(mergeConfigs(locales[name]._config, config)); + } else { + // MERGE + tmpLocale = loadLocale(name); + if (tmpLocale != null) { + parentConfig = tmpLocale._config; + } + config = mergeConfigs(parentConfig, config); + if (tmpLocale == null) { + // updateLocale is called for creating a new locale + // Set abbr so it will have a name (getters return + // undefined otherwise). + config.abbr = name; + } + locale = new Locale(config); + locale.parentLocale = locales[name]; + locales[name] = locale; + } + + // backwards compat for now: also set the locale + getSetGlobalLocale(name); + } else { + // pass null for config to unupdate, useful for tests + if (locales[name] != null) { + if (locales[name].parentLocale != null) { + locales[name] = locales[name].parentLocale; + if (name === getSetGlobalLocale()) { + getSetGlobalLocale(name); + } + } else if (locales[name] != null) { + delete locales[name]; + } + } + } + return locales[name]; + } + + // returns locale data + function getLocale(key) { + var locale; + + if (key && key._locale && key._locale._abbr) { + key = key._locale._abbr; + } + + if (!key) { + return globalLocale; + } + + if (!isArray(key)) { + //short-circuit everything else + locale = loadLocale(key); + if (locale) { + return locale; + } + key = [key]; + } + + return chooseLocale(key); + } + + function listLocales() { + return keys(locales); + } + + function checkOverflow(m) { + var overflow, + a = m._a; + + if (a && getParsingFlags(m).overflow === -2) { + overflow = + a[MONTH] < 0 || a[MONTH] > 11 + ? MONTH + : a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) + ? DATE + : a[HOUR] < 0 || + a[HOUR] > 24 || + (a[HOUR] === 24 && + (a[MINUTE] !== 0 || + a[SECOND] !== 0 || + a[MILLISECOND] !== 0)) + ? HOUR + : a[MINUTE] < 0 || a[MINUTE] > 59 + ? MINUTE + : a[SECOND] < 0 || a[SECOND] > 59 + ? SECOND + : a[MILLISECOND] < 0 || a[MILLISECOND] > 999 + ? MILLISECOND + : -1; + + if ( + getParsingFlags(m)._overflowDayOfYear && + (overflow < YEAR || overflow > DATE) + ) { + overflow = DATE; + } + if (getParsingFlags(m)._overflowWeeks && overflow === -1) { + overflow = WEEK; + } + if (getParsingFlags(m)._overflowWeekday && overflow === -1) { + overflow = WEEKDAY; + } + + getParsingFlags(m).overflow = overflow; + } + + return m; + } + + // iso 8601 regex + // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00) + var extendedIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/, + basicIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d|))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/, + tzRegex = /Z|[+-]\d\d(?::?\d\d)?/, + isoDates = [ + ['YYYYYY-MM-DD', /[+-]\d{6}-\d\d-\d\d/], + ['YYYY-MM-DD', /\d{4}-\d\d-\d\d/], + ['GGGG-[W]WW-E', /\d{4}-W\d\d-\d/], + ['GGGG-[W]WW', /\d{4}-W\d\d/, false], + ['YYYY-DDD', /\d{4}-\d{3}/], + ['YYYY-MM', /\d{4}-\d\d/, false], + ['YYYYYYMMDD', /[+-]\d{10}/], + ['YYYYMMDD', /\d{8}/], + ['GGGG[W]WWE', /\d{4}W\d{3}/], + ['GGGG[W]WW', /\d{4}W\d{2}/, false], + ['YYYYDDD', /\d{7}/], + ['YYYYMM', /\d{6}/, false], + ['YYYY', /\d{4}/, false], + ], + // iso time formats and regexes + isoTimes = [ + ['HH:mm:ss.SSSS', /\d\d:\d\d:\d\d\.\d+/], + ['HH:mm:ss,SSSS', /\d\d:\d\d:\d\d,\d+/], + ['HH:mm:ss', /\d\d:\d\d:\d\d/], + ['HH:mm', /\d\d:\d\d/], + ['HHmmss.SSSS', /\d\d\d\d\d\d\.\d+/], + ['HHmmss,SSSS', /\d\d\d\d\d\d,\d+/], + ['HHmmss', /\d\d\d\d\d\d/], + ['HHmm', /\d\d\d\d/], + ['HH', /\d\d/], + ], + aspNetJsonRegex = /^\/?Date\((-?\d+)/i, + // RFC 2822 regex: For details see https://tools.ietf.org/html/rfc2822#section-3.3 + rfc2822 = /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/, + obsOffsets = { + UT: 0, + GMT: 0, + EDT: -4 * 60, + EST: -5 * 60, + CDT: -5 * 60, + CST: -6 * 60, + MDT: -6 * 60, + MST: -7 * 60, + PDT: -7 * 60, + PST: -8 * 60, + }; + + // date from iso format + function configFromISO(config) { + var i, + l, + string = config._i, + match = extendedIsoRegex.exec(string) || basicIsoRegex.exec(string), + allowTime, + dateFormat, + timeFormat, + tzFormat; + + if (match) { + getParsingFlags(config).iso = true; + + for (i = 0, l = isoDates.length; i < l; i++) { + if (isoDates[i][1].exec(match[1])) { + dateFormat = isoDates[i][0]; + allowTime = isoDates[i][2] !== false; + break; + } + } + if (dateFormat == null) { + config._isValid = false; + return; + } + if (match[3]) { + for (i = 0, l = isoTimes.length; i < l; i++) { + if (isoTimes[i][1].exec(match[3])) { + // match[2] should be 'T' or space + timeFormat = (match[2] || ' ') + isoTimes[i][0]; + break; + } + } + if (timeFormat == null) { + config._isValid = false; + return; + } + } + if (!allowTime && timeFormat != null) { + config._isValid = false; + return; + } + if (match[4]) { + if (tzRegex.exec(match[4])) { + tzFormat = 'Z'; + } else { + config._isValid = false; + return; + } + } + config._f = dateFormat + (timeFormat || '') + (tzFormat || ''); + configFromStringAndFormat(config); + } else { + config._isValid = false; + } + } + + function extractFromRFC2822Strings( + yearStr, + monthStr, + dayStr, + hourStr, + minuteStr, + secondStr + ) { + var result = [ + untruncateYear(yearStr), + defaultLocaleMonthsShort.indexOf(monthStr), + parseInt(dayStr, 10), + parseInt(hourStr, 10), + parseInt(minuteStr, 10), + ]; + + if (secondStr) { + result.push(parseInt(secondStr, 10)); + } + + return result; + } + + function untruncateYear(yearStr) { + var year = parseInt(yearStr, 10); + if (year <= 49) { + return 2000 + year; + } else if (year <= 999) { + return 1900 + year; + } + return year; + } + + function preprocessRFC2822(s) { + // Remove comments and folding whitespace and replace multiple-spaces with a single space + return s + .replace(/\([^)]*\)|[\n\t]/g, ' ') + .replace(/(\s\s+)/g, ' ') + .replace(/^\s\s*/, '') + .replace(/\s\s*$/, ''); + } + + function checkWeekday(weekdayStr, parsedInput, config) { + if (weekdayStr) { + // TODO: Replace the vanilla JS Date object with an independent day-of-week check. + var weekdayProvided = defaultLocaleWeekdaysShort.indexOf(weekdayStr), + weekdayActual = new Date( + parsedInput[0], + parsedInput[1], + parsedInput[2] + ).getDay(); + if (weekdayProvided !== weekdayActual) { + getParsingFlags(config).weekdayMismatch = true; + config._isValid = false; + return false; + } + } + return true; + } + + function calculateOffset(obsOffset, militaryOffset, numOffset) { + if (obsOffset) { + return obsOffsets[obsOffset]; + } else if (militaryOffset) { + // the only allowed military tz is Z + return 0; + } else { + var hm = parseInt(numOffset, 10), + m = hm % 100, + h = (hm - m) / 100; + return h * 60 + m; + } + } + + // date and time from ref 2822 format + function configFromRFC2822(config) { + var match = rfc2822.exec(preprocessRFC2822(config._i)), + parsedArray; + if (match) { + parsedArray = extractFromRFC2822Strings( + match[4], + match[3], + match[2], + match[5], + match[6], + match[7] + ); + if (!checkWeekday(match[1], parsedArray, config)) { + return; + } + + config._a = parsedArray; + config._tzm = calculateOffset(match[8], match[9], match[10]); + + config._d = createUTCDate.apply(null, config._a); + config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); + + getParsingFlags(config).rfc2822 = true; + } else { + config._isValid = false; + } + } + + // date from 1) ASP.NET, 2) ISO, 3) RFC 2822 formats, or 4) optional fallback if parsing isn't strict + function configFromString(config) { + var matched = aspNetJsonRegex.exec(config._i); + if (matched !== null) { + config._d = new Date(+matched[1]); + return; + } + + configFromISO(config); + if (config._isValid === false) { + delete config._isValid; + } else { + return; + } + + configFromRFC2822(config); + if (config._isValid === false) { + delete config._isValid; + } else { + return; + } + + if (config._strict) { + config._isValid = false; + } else { + // Final attempt, use Input Fallback + hooks.createFromInputFallback(config); + } + } + + hooks.createFromInputFallback = deprecate( + 'value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), ' + + 'which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are ' + + 'discouraged. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.', + function (config) { + config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); + } + ); + + // Pick the first defined of two or three arguments. + function defaults(a, b, c) { + if (a != null) { + return a; + } + if (b != null) { + return b; + } + return c; + } + + function currentDateArray(config) { + // hooks is actually the exported moment object + var nowValue = new Date(hooks.now()); + if (config._useUTC) { + return [ + nowValue.getUTCFullYear(), + nowValue.getUTCMonth(), + nowValue.getUTCDate(), + ]; + } + return [nowValue.getFullYear(), nowValue.getMonth(), nowValue.getDate()]; + } + + // convert an array to a date. + // the array should mirror the parameters below + // note: all values past the year are optional and will default to the lowest possible value. + // [year, month, day , hour, minute, second, millisecond] + function configFromArray(config) { + var i, + date, + input = [], + currentDate, + expectedWeekday, + yearToUse; + + if (config._d) { + return; + } + + currentDate = currentDateArray(config); + + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + dayOfYearFromWeekInfo(config); + } + + //if the day of the year is set, figure out what it is + if (config._dayOfYear != null) { + yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); + + if ( + config._dayOfYear > daysInYear(yearToUse) || + config._dayOfYear === 0 + ) { + getParsingFlags(config)._overflowDayOfYear = true; + } + + date = createUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); + } + + // Default to current date. + // * if no year, month, day of month are given, default to today + // * if day of month is given, default month and year + // * if month is given, default only year + // * if year is given, don't default anything + for (i = 0; i < 3 && config._a[i] == null; ++i) { + config._a[i] = input[i] = currentDate[i]; + } + + // Zero out whatever was not defaulted, including time + for (; i < 7; i++) { + config._a[i] = input[i] = + config._a[i] == null ? (i === 2 ? 1 : 0) : config._a[i]; + } + + // Check for 24:00:00.000 + if ( + config._a[HOUR] === 24 && + config._a[MINUTE] === 0 && + config._a[SECOND] === 0 && + config._a[MILLISECOND] === 0 + ) { + config._nextDay = true; + config._a[HOUR] = 0; + } + + config._d = (config._useUTC ? createUTCDate : createDate).apply( + null, + input + ); + expectedWeekday = config._useUTC + ? config._d.getUTCDay() + : config._d.getDay(); + + // Apply timezone offset from input. The actual utcOffset can be changed + // with parseZone. + if (config._tzm != null) { + config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); + } + + if (config._nextDay) { + config._a[HOUR] = 24; + } + + // check for mismatching day of week + if ( + config._w && + typeof config._w.d !== 'undefined' && + config._w.d !== expectedWeekday + ) { + getParsingFlags(config).weekdayMismatch = true; + } + } + + function dayOfYearFromWeekInfo(config) { + var w, weekYear, week, weekday, dow, doy, temp, weekdayOverflow, curWeek; + + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + dow = 1; + doy = 4; + + // TODO: We need to take the current isoWeekYear, but that depends on + // how we interpret now (local, utc, fixed offset). So create + // a now version of current config (take local/utc/offset flags, and + // create now). + weekYear = defaults( + w.GG, + config._a[YEAR], + weekOfYear(createLocal(), 1, 4).year + ); + week = defaults(w.W, 1); + weekday = defaults(w.E, 1); + if (weekday < 1 || weekday > 7) { + weekdayOverflow = true; + } + } else { + dow = config._locale._week.dow; + doy = config._locale._week.doy; + + curWeek = weekOfYear(createLocal(), dow, doy); + + weekYear = defaults(w.gg, config._a[YEAR], curWeek.year); + + // Default to current week. + week = defaults(w.w, curWeek.week); + + if (w.d != null) { + // weekday -- low day numbers are considered next week + weekday = w.d; + if (weekday < 0 || weekday > 6) { + weekdayOverflow = true; + } + } else if (w.e != null) { + // local weekday -- counting starts from beginning of week + weekday = w.e + dow; + if (w.e < 0 || w.e > 6) { + weekdayOverflow = true; + } + } else { + // default to beginning of week + weekday = dow; + } + } + if (week < 1 || week > weeksInYear(weekYear, dow, doy)) { + getParsingFlags(config)._overflowWeeks = true; + } else if (weekdayOverflow != null) { + getParsingFlags(config)._overflowWeekday = true; + } else { + temp = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy); + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; + } + } + + // constant that refers to the ISO standard + hooks.ISO_8601 = function () {}; + + // constant that refers to the RFC 2822 form + hooks.RFC_2822 = function () {}; + + // date from string and format string + function configFromStringAndFormat(config) { + // TODO: Move this to another part of the creation flow to prevent circular deps + if (config._f === hooks.ISO_8601) { + configFromISO(config); + return; + } + if (config._f === hooks.RFC_2822) { + configFromRFC2822(config); + return; + } + config._a = []; + getParsingFlags(config).empty = true; + + // This array is used to make a Date, either with `new Date` or `Date.UTC` + var string = '' + config._i, + i, + parsedInput, + tokens, + token, + skipped, + stringLength = string.length, + totalParsedInputLength = 0, + era; + + tokens = + expandFormat(config._f, config._locale).match(formattingTokens) || []; + + for (i = 0; i < tokens.length; i++) { + token = tokens[i]; + parsedInput = (string.match(getParseRegexForToken(token, config)) || + [])[0]; + if (parsedInput) { + skipped = string.substr(0, string.indexOf(parsedInput)); + if (skipped.length > 0) { + getParsingFlags(config).unusedInput.push(skipped); + } + string = string.slice( + string.indexOf(parsedInput) + parsedInput.length + ); + totalParsedInputLength += parsedInput.length; + } + // don't parse if it's not a known token + if (formatTokenFunctions[token]) { + if (parsedInput) { + getParsingFlags(config).empty = false; + } else { + getParsingFlags(config).unusedTokens.push(token); + } + addTimeToArrayFromToken(token, parsedInput, config); + } else if (config._strict && !parsedInput) { + getParsingFlags(config).unusedTokens.push(token); + } + } + + // add remaining unparsed input length to the string + getParsingFlags(config).charsLeftOver = + stringLength - totalParsedInputLength; + if (string.length > 0) { + getParsingFlags(config).unusedInput.push(string); + } + + // clear _12h flag if hour is <= 12 + if ( + config._a[HOUR] <= 12 && + getParsingFlags(config).bigHour === true && + config._a[HOUR] > 0 + ) { + getParsingFlags(config).bigHour = undefined; + } + + getParsingFlags(config).parsedDateParts = config._a.slice(0); + getParsingFlags(config).meridiem = config._meridiem; + // handle meridiem + config._a[HOUR] = meridiemFixWrap( + config._locale, + config._a[HOUR], + config._meridiem + ); + + // handle era + era = getParsingFlags(config).era; + if (era !== null) { + config._a[YEAR] = config._locale.erasConvertYear(era, config._a[YEAR]); + } + + configFromArray(config); + checkOverflow(config); + } + + function meridiemFixWrap(locale, hour, meridiem) { + var isPm; + + if (meridiem == null) { + // nothing to do + return hour; + } + if (locale.meridiemHour != null) { + return locale.meridiemHour(hour, meridiem); + } else if (locale.isPM != null) { + // Fallback + isPm = locale.isPM(meridiem); + if (isPm && hour < 12) { + hour += 12; + } + if (!isPm && hour === 12) { + hour = 0; + } + return hour; + } else { + // this is not supposed to happen + return hour; + } + } + + // date from string and array of format strings + function configFromStringAndArray(config) { + var tempConfig, + bestMoment, + scoreToBeat, + i, + currentScore, + validFormatFound, + bestFormatIsValid = false; + + if (config._f.length === 0) { + getParsingFlags(config).invalidFormat = true; + config._d = new Date(NaN); + return; + } + + for (i = 0; i < config._f.length; i++) { + currentScore = 0; + validFormatFound = false; + tempConfig = copyConfig({}, config); + if (config._useUTC != null) { + tempConfig._useUTC = config._useUTC; + } + tempConfig._f = config._f[i]; + configFromStringAndFormat(tempConfig); + + if (isValid(tempConfig)) { + validFormatFound = true; + } + + // if there is any input that was not parsed add a penalty for that format + currentScore += getParsingFlags(tempConfig).charsLeftOver; + + //or tokens + currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; + + getParsingFlags(tempConfig).score = currentScore; + + if (!bestFormatIsValid) { + if ( + scoreToBeat == null || + currentScore < scoreToBeat || + validFormatFound + ) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + if (validFormatFound) { + bestFormatIsValid = true; + } + } + } else { + if (currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + } + } + } + + extend(config, bestMoment || tempConfig); + } + + function configFromObject(config) { + if (config._d) { + return; + } + + var i = normalizeObjectUnits(config._i), + dayOrDate = i.day === undefined ? i.date : i.day; + config._a = map( + [i.year, i.month, dayOrDate, i.hour, i.minute, i.second, i.millisecond], + function (obj) { + return obj && parseInt(obj, 10); + } + ); + + configFromArray(config); + } + + function createFromConfig(config) { + var res = new Moment(checkOverflow(prepareConfig(config))); + if (res._nextDay) { + // Adding is smart enough around DST + res.add(1, 'd'); + res._nextDay = undefined; + } + + return res; + } + + function prepareConfig(config) { + var input = config._i, + format = config._f; + + config._locale = config._locale || getLocale(config._l); + + if (input === null || (format === undefined && input === '')) { + return createInvalid({ nullInput: true }); + } + + if (typeof input === 'string') { + config._i = input = config._locale.preparse(input); + } + + if (isMoment(input)) { + return new Moment(checkOverflow(input)); + } else if (isDate(input)) { + config._d = input; + } else if (isArray(format)) { + configFromStringAndArray(config); + } else if (format) { + configFromStringAndFormat(config); + } else { + configFromInput(config); + } + + if (!isValid(config)) { + config._d = null; + } + + return config; + } + + function configFromInput(config) { + var input = config._i; + if (isUndefined(input)) { + config._d = new Date(hooks.now()); + } else if (isDate(input)) { + config._d = new Date(input.valueOf()); + } else if (typeof input === 'string') { + configFromString(config); + } else if (isArray(input)) { + config._a = map(input.slice(0), function (obj) { + return parseInt(obj, 10); + }); + configFromArray(config); + } else if (isObject(input)) { + configFromObject(config); + } else if (isNumber(input)) { + // from milliseconds + config._d = new Date(input); + } else { + hooks.createFromInputFallback(config); + } + } + + function createLocalOrUTC(input, format, locale, strict, isUTC) { + var c = {}; + + if (format === true || format === false) { + strict = format; + format = undefined; + } + + if (locale === true || locale === false) { + strict = locale; + locale = undefined; + } + + if ( + (isObject(input) && isObjectEmpty(input)) || + (isArray(input) && input.length === 0) + ) { + input = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c._isAMomentObject = true; + c._useUTC = c._isUTC = isUTC; + c._l = locale; + c._i = input; + c._f = format; + c._strict = strict; + + return createFromConfig(c); + } + + function createLocal(input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, false); + } + + var prototypeMin = deprecate( + 'moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/', + function () { + var other = createLocal.apply(null, arguments); + if (this.isValid() && other.isValid()) { + return other < this ? this : other; + } else { + return createInvalid(); + } + } + ), + prototypeMax = deprecate( + 'moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/', + function () { + var other = createLocal.apply(null, arguments); + if (this.isValid() && other.isValid()) { + return other > this ? this : other; + } else { + return createInvalid(); + } + } + ); + + // Pick a moment m from moments so that m[fn](other) is true for all + // other. This relies on the function fn to be transitive. + // + // moments should either be an array of moment objects or an array, whose + // first element is an array of moment objects. + function pickBy(fn, moments) { + var res, i; + if (moments.length === 1 && isArray(moments[0])) { + moments = moments[0]; + } + if (!moments.length) { + return createLocal(); + } + res = moments[0]; + for (i = 1; i < moments.length; ++i) { + if (!moments[i].isValid() || moments[i][fn](res)) { + res = moments[i]; + } + } + return res; + } + + // TODO: Use [].sort instead? + function min() { + var args = [].slice.call(arguments, 0); + + return pickBy('isBefore', args); + } + + function max() { + var args = [].slice.call(arguments, 0); + + return pickBy('isAfter', args); + } + + var now = function () { + return Date.now ? Date.now() : +new Date(); + }; + + var ordering = [ + 'year', + 'quarter', + 'month', + 'week', + 'day', + 'hour', + 'minute', + 'second', + 'millisecond', + ]; + + function isDurationValid(m) { + var key, + unitHasDecimal = false, + i; + for (key in m) { + if ( + hasOwnProp(m, key) && + !( + indexOf.call(ordering, key) !== -1 && + (m[key] == null || !isNaN(m[key])) + ) + ) { + return false; + } + } + + for (i = 0; i < ordering.length; ++i) { + if (m[ordering[i]]) { + if (unitHasDecimal) { + return false; // only allow non-integers for smallest unit + } + if (parseFloat(m[ordering[i]]) !== toInt(m[ordering[i]])) { + unitHasDecimal = true; + } + } + } + + return true; + } + + function isValid$1() { + return this._isValid; + } + + function createInvalid$1() { + return createDuration(NaN); + } + + function Duration(duration) { + var normalizedInput = normalizeObjectUnits(duration), + years = normalizedInput.year || 0, + quarters = normalizedInput.quarter || 0, + months = normalizedInput.month || 0, + weeks = normalizedInput.week || normalizedInput.isoWeek || 0, + days = normalizedInput.day || 0, + hours = normalizedInput.hour || 0, + minutes = normalizedInput.minute || 0, + seconds = normalizedInput.second || 0, + milliseconds = normalizedInput.millisecond || 0; + + this._isValid = isDurationValid(normalizedInput); + + // representation for dateAddRemove + this._milliseconds = + +milliseconds + + seconds * 1e3 + // 1000 + minutes * 6e4 + // 1000 * 60 + hours * 1000 * 60 * 60; //using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978 + // Because of dateAddRemove treats 24 hours as different from a + // day when working around DST, we need to store them separately + this._days = +days + weeks * 7; + // It is impossible to translate months into days without knowing + // which months you are are talking about, so we have to store + // it separately. + this._months = +months + quarters * 3 + years * 12; + + this._data = {}; + + this._locale = getLocale(); + + this._bubble(); + } + + function isDuration(obj) { + return obj instanceof Duration; + } + + function absRound(number) { + if (number < 0) { + return Math.round(-1 * number) * -1; + } else { + return Math.round(number); + } + } + + // compare two arrays, return the number of differences + function compareArrays(array1, array2, dontConvert) { + var len = Math.min(array1.length, array2.length), + lengthDiff = Math.abs(array1.length - array2.length), + diffs = 0, + i; + for (i = 0; i < len; i++) { + if ( + (dontConvert && array1[i] !== array2[i]) || + (!dontConvert && toInt(array1[i]) !== toInt(array2[i])) + ) { + diffs++; + } + } + return diffs + lengthDiff; + } + + // FORMATTING + + function offset(token, separator) { + addFormatToken(token, 0, 0, function () { + var offset = this.utcOffset(), + sign = '+'; + if (offset < 0) { + offset = -offset; + sign = '-'; + } + return ( + sign + + zeroFill(~~(offset / 60), 2) + + separator + + zeroFill(~~offset % 60, 2) + ); + }); + } + + offset('Z', ':'); + offset('ZZ', ''); + + // PARSING + + addRegexToken('Z', matchShortOffset); + addRegexToken('ZZ', matchShortOffset); + addParseToken(['Z', 'ZZ'], function (input, array, config) { + config._useUTC = true; + config._tzm = offsetFromString(matchShortOffset, input); + }); + + // HELPERS + + // timezone chunker + // '+10:00' > ['10', '00'] + // '-1530' > ['-15', '30'] + var chunkOffset = /([\+\-]|\d\d)/gi; + + function offsetFromString(matcher, string) { + var matches = (string || '').match(matcher), + chunk, + parts, + minutes; + + if (matches === null) { + return null; + } + + chunk = matches[matches.length - 1] || []; + parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; + minutes = +(parts[1] * 60) + toInt(parts[2]); + + return minutes === 0 ? 0 : parts[0] === '+' ? minutes : -minutes; + } + + // Return a moment from input, that is local/utc/zone equivalent to model. + function cloneWithOffset(input, model) { + var res, diff; + if (model._isUTC) { + res = model.clone(); + diff = + (isMoment(input) || isDate(input) + ? input.valueOf() + : createLocal(input).valueOf()) - res.valueOf(); + // Use low-level api, because this fn is low-level api. + res._d.setTime(res._d.valueOf() + diff); + hooks.updateOffset(res, false); + return res; + } else { + return createLocal(input).local(); + } + } + + function getDateOffset(m) { + // On Firefox.24 Date#getTimezoneOffset returns a floating point. + // https://github.com/moment/moment/pull/1871 + return -Math.round(m._d.getTimezoneOffset()); + } + + // HOOKS + + // This function will be called whenever a moment is mutated. + // It is intended to keep the offset in sync with the timezone. + hooks.updateOffset = function () {}; + + // MOMENTS + + // keepLocalTime = true means only change the timezone, without + // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> + // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset + // +0200, so we adjust the time as needed, to be valid. + // + // Keeping the time actually adds/subtracts (one hour) + // from the actual represented time. That is why we call updateOffset + // a second time. In case it wants us to change the offset again + // _changeInProgress == true case, then we have to adjust, because + // there is no such time in the given timezone. + function getSetOffset(input, keepLocalTime, keepMinutes) { + var offset = this._offset || 0, + localAdjust; + if (!this.isValid()) { + return input != null ? this : NaN; + } + if (input != null) { + if (typeof input === 'string') { + input = offsetFromString(matchShortOffset, input); + if (input === null) { + return this; + } + } else if (Math.abs(input) < 16 && !keepMinutes) { + input = input * 60; + } + if (!this._isUTC && keepLocalTime) { + localAdjust = getDateOffset(this); + } + this._offset = input; + this._isUTC = true; + if (localAdjust != null) { + this.add(localAdjust, 'm'); + } + if (offset !== input) { + if (!keepLocalTime || this._changeInProgress) { + addSubtract( + this, + createDuration(input - offset, 'm'), + 1, + false + ); + } else if (!this._changeInProgress) { + this._changeInProgress = true; + hooks.updateOffset(this, true); + this._changeInProgress = null; + } + } + return this; + } else { + return this._isUTC ? offset : getDateOffset(this); + } + } + + function getSetZone(input, keepLocalTime) { + if (input != null) { + if (typeof input !== 'string') { + input = -input; + } + + this.utcOffset(input, keepLocalTime); + + return this; + } else { + return -this.utcOffset(); + } + } + + function setOffsetToUTC(keepLocalTime) { + return this.utcOffset(0, keepLocalTime); + } + + function setOffsetToLocal(keepLocalTime) { + if (this._isUTC) { + this.utcOffset(0, keepLocalTime); + this._isUTC = false; + + if (keepLocalTime) { + this.subtract(getDateOffset(this), 'm'); + } + } + return this; + } + + function setOffsetToParsedOffset() { + if (this._tzm != null) { + this.utcOffset(this._tzm, false, true); + } else if (typeof this._i === 'string') { + var tZone = offsetFromString(matchOffset, this._i); + if (tZone != null) { + this.utcOffset(tZone); + } else { + this.utcOffset(0, true); + } + } + return this; + } + + function hasAlignedHourOffset(input) { + if (!this.isValid()) { + return false; + } + input = input ? createLocal(input).utcOffset() : 0; + + return (this.utcOffset() - input) % 60 === 0; + } + + function isDaylightSavingTime() { + return ( + this.utcOffset() > this.clone().month(0).utcOffset() || + this.utcOffset() > this.clone().month(5).utcOffset() + ); + } + + function isDaylightSavingTimeShifted() { + if (!isUndefined(this._isDSTShifted)) { + return this._isDSTShifted; + } + + var c = {}, + other; + + copyConfig(c, this); + c = prepareConfig(c); + + if (c._a) { + other = c._isUTC ? createUTC(c._a) : createLocal(c._a); + this._isDSTShifted = + this.isValid() && compareArrays(c._a, other.toArray()) > 0; + } else { + this._isDSTShifted = false; + } + + return this._isDSTShifted; + } + + function isLocal() { + return this.isValid() ? !this._isUTC : false; + } + + function isUtcOffset() { + return this.isValid() ? this._isUTC : false; + } + + function isUtc() { + return this.isValid() ? this._isUTC && this._offset === 0 : false; + } + + // ASP.NET json date format regex + var aspNetRegex = /^(-|\+)?(?:(\d*)[. ])?(\d+):(\d+)(?::(\d+)(\.\d*)?)?$/, + // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html + // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere + // and further modified to allow for strings containing both week and day + isoRegex = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/; + + function createDuration(input, key) { + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + diffRes; + + if (isDuration(input)) { + duration = { + ms: input._milliseconds, + d: input._days, + M: input._months, + }; + } else if (isNumber(input) || !isNaN(+input)) { + duration = {}; + if (key) { + duration[key] = +input; + } else { + duration.milliseconds = +input; + } + } else if ((match = aspNetRegex.exec(input))) { + sign = match[1] === '-' ? -1 : 1; + duration = { + y: 0, + d: toInt(match[DATE]) * sign, + h: toInt(match[HOUR]) * sign, + m: toInt(match[MINUTE]) * sign, + s: toInt(match[SECOND]) * sign, + ms: toInt(absRound(match[MILLISECOND] * 1000)) * sign, // the millisecond decimal point is included in the match + }; + } else if ((match = isoRegex.exec(input))) { + sign = match[1] === '-' ? -1 : 1; + duration = { + y: parseIso(match[2], sign), + M: parseIso(match[3], sign), + w: parseIso(match[4], sign), + d: parseIso(match[5], sign), + h: parseIso(match[6], sign), + m: parseIso(match[7], sign), + s: parseIso(match[8], sign), + }; + } else if (duration == null) { + // checks for null or undefined + duration = {}; + } else if ( + typeof duration === 'object' && + ('from' in duration || 'to' in duration) + ) { + diffRes = momentsDifference( + createLocal(duration.from), + createLocal(duration.to) + ); + + duration = {}; + duration.ms = diffRes.milliseconds; + duration.M = diffRes.months; + } + + ret = new Duration(duration); + + if (isDuration(input) && hasOwnProp(input, '_locale')) { + ret._locale = input._locale; + } + + if (isDuration(input) && hasOwnProp(input, '_isValid')) { + ret._isValid = input._isValid; + } + + return ret; + } + + createDuration.fn = Duration.prototype; + createDuration.invalid = createInvalid$1; + + function parseIso(inp, sign) { + // We'd normally use ~~inp for this, but unfortunately it also + // converts floats to ints. + // inp may be undefined, so careful calling replace on it. + var res = inp && parseFloat(inp.replace(',', '.')); + // apply sign while we're at it + return (isNaN(res) ? 0 : res) * sign; + } + + function positiveMomentsDifference(base, other) { + var res = {}; + + res.months = + other.month() - base.month() + (other.year() - base.year()) * 12; + if (base.clone().add(res.months, 'M').isAfter(other)) { + --res.months; + } + + res.milliseconds = +other - +base.clone().add(res.months, 'M'); + + return res; + } + + function momentsDifference(base, other) { + var res; + if (!(base.isValid() && other.isValid())) { + return { milliseconds: 0, months: 0 }; + } + + other = cloneWithOffset(other, base); + if (base.isBefore(other)) { + res = positiveMomentsDifference(base, other); + } else { + res = positiveMomentsDifference(other, base); + res.milliseconds = -res.milliseconds; + res.months = -res.months; + } + + return res; + } + + // TODO: remove 'name' arg after deprecation is removed + function createAdder(direction, name) { + return function (val, period) { + var dur, tmp; + //invert the arguments, but complain about it + if (period !== null && !isNaN(+period)) { + deprecateSimple( + name, + 'moment().' + + name + + '(period, number) is deprecated. Please use moment().' + + name + + '(number, period). ' + + 'See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info.' + ); + tmp = val; + val = period; + period = tmp; + } + + dur = createDuration(val, period); + addSubtract(this, dur, direction); + return this; + }; + } + + function addSubtract(mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = absRound(duration._days), + months = absRound(duration._months); + + if (!mom.isValid()) { + // No op + return; + } + + updateOffset = updateOffset == null ? true : updateOffset; + + if (months) { + setMonth(mom, get(mom, 'Month') + months * isAdding); + } + if (days) { + set$1(mom, 'Date', get(mom, 'Date') + days * isAdding); + } + if (milliseconds) { + mom._d.setTime(mom._d.valueOf() + milliseconds * isAdding); + } + if (updateOffset) { + hooks.updateOffset(mom, days || months); + } + } + + var add = createAdder(1, 'add'), + subtract = createAdder(-1, 'subtract'); + + function isString(input) { + return typeof input === 'string' || input instanceof String; + } + + // type MomentInput = Moment | Date | string | number | (number | string)[] | MomentInputObject | void; // null | undefined + function isMomentInput(input) { + return ( + isMoment(input) || + isDate(input) || + isString(input) || + isNumber(input) || + isNumberOrStringArray(input) || + isMomentInputObject(input) || + input === null || + input === undefined + ); + } + + function isMomentInputObject(input) { + var objectTest = isObject(input) && !isObjectEmpty(input), + propertyTest = false, + properties = [ + 'years', + 'year', + 'y', + 'months', + 'month', + 'M', + 'days', + 'day', + 'd', + 'dates', + 'date', + 'D', + 'hours', + 'hour', + 'h', + 'minutes', + 'minute', + 'm', + 'seconds', + 'second', + 's', + 'milliseconds', + 'millisecond', + 'ms', + ], + i, + property; + + for (i = 0; i < properties.length; i += 1) { + property = properties[i]; + propertyTest = propertyTest || hasOwnProp(input, property); + } + + return objectTest && propertyTest; + } + + function isNumberOrStringArray(input) { + var arrayTest = isArray(input), + dataTypeTest = false; + if (arrayTest) { + dataTypeTest = + input.filter(function (item) { + return !isNumber(item) && isString(input); + }).length === 0; + } + return arrayTest && dataTypeTest; + } + + function isCalendarSpec(input) { + var objectTest = isObject(input) && !isObjectEmpty(input), + propertyTest = false, + properties = [ + 'sameDay', + 'nextDay', + 'lastDay', + 'nextWeek', + 'lastWeek', + 'sameElse', + ], + i, + property; + + for (i = 0; i < properties.length; i += 1) { + property = properties[i]; + propertyTest = propertyTest || hasOwnProp(input, property); + } + + return objectTest && propertyTest; + } + + function getCalendarFormat(myMoment, now) { + var diff = myMoment.diff(now, 'days', true); + return diff < -6 + ? 'sameElse' + : diff < -1 + ? 'lastWeek' + : diff < 0 + ? 'lastDay' + : diff < 1 + ? 'sameDay' + : diff < 2 + ? 'nextDay' + : diff < 7 + ? 'nextWeek' + : 'sameElse'; + } + + function calendar$1(time, formats) { + // Support for single parameter, formats only overload to the calendar function + if (arguments.length === 1) { + if (!arguments[0]) { + time = undefined; + formats = undefined; + } else if (isMomentInput(arguments[0])) { + time = arguments[0]; + formats = undefined; + } else if (isCalendarSpec(arguments[0])) { + formats = arguments[0]; + time = undefined; + } + } + // We want to compare the start of today, vs this. + // Getting start-of-today depends on whether we're local/utc/offset or not. + var now = time || createLocal(), + sod = cloneWithOffset(now, this).startOf('day'), + format = hooks.calendarFormat(this, sod) || 'sameElse', + output = + formats && + (isFunction(formats[format]) + ? formats[format].call(this, now) + : formats[format]); + + return this.format( + output || this.localeData().calendar(format, this, createLocal(now)) + ); + } + + function clone() { + return new Moment(this); + } + + function isAfter(input, units) { + var localInput = isMoment(input) ? input : createLocal(input); + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(units) || 'millisecond'; + if (units === 'millisecond') { + return this.valueOf() > localInput.valueOf(); + } else { + return localInput.valueOf() < this.clone().startOf(units).valueOf(); + } + } + + function isBefore(input, units) { + var localInput = isMoment(input) ? input : createLocal(input); + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(units) || 'millisecond'; + if (units === 'millisecond') { + return this.valueOf() < localInput.valueOf(); + } else { + return this.clone().endOf(units).valueOf() < localInput.valueOf(); + } + } + + function isBetween(from, to, units, inclusivity) { + var localFrom = isMoment(from) ? from : createLocal(from), + localTo = isMoment(to) ? to : createLocal(to); + if (!(this.isValid() && localFrom.isValid() && localTo.isValid())) { + return false; + } + inclusivity = inclusivity || '()'; + return ( + (inclusivity[0] === '(' + ? this.isAfter(localFrom, units) + : !this.isBefore(localFrom, units)) && + (inclusivity[1] === ')' + ? this.isBefore(localTo, units) + : !this.isAfter(localTo, units)) + ); + } + + function isSame(input, units) { + var localInput = isMoment(input) ? input : createLocal(input), + inputMs; + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(units) || 'millisecond'; + if (units === 'millisecond') { + return this.valueOf() === localInput.valueOf(); + } else { + inputMs = localInput.valueOf(); + return ( + this.clone().startOf(units).valueOf() <= inputMs && + inputMs <= this.clone().endOf(units).valueOf() + ); + } + } + + function isSameOrAfter(input, units) { + return this.isSame(input, units) || this.isAfter(input, units); + } + + function isSameOrBefore(input, units) { + return this.isSame(input, units) || this.isBefore(input, units); + } + + function diff(input, units, asFloat) { + var that, zoneDelta, output; + + if (!this.isValid()) { + return NaN; + } + + that = cloneWithOffset(input, this); + + if (!that.isValid()) { + return NaN; + } + + zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4; + + units = normalizeUnits(units); + + switch (units) { + case 'year': + output = monthDiff(this, that) / 12; + break; + case 'month': + output = monthDiff(this, that); + break; + case 'quarter': + output = monthDiff(this, that) / 3; + break; + case 'second': + output = (this - that) / 1e3; + break; // 1000 + case 'minute': + output = (this - that) / 6e4; + break; // 1000 * 60 + case 'hour': + output = (this - that) / 36e5; + break; // 1000 * 60 * 60 + case 'day': + output = (this - that - zoneDelta) / 864e5; + break; // 1000 * 60 * 60 * 24, negate dst + case 'week': + output = (this - that - zoneDelta) / 6048e5; + break; // 1000 * 60 * 60 * 24 * 7, negate dst + default: + output = this - that; + } + + return asFloat ? output : absFloor(output); + } + + function monthDiff(a, b) { + if (a.date() < b.date()) { + // end-of-month calculations work correct when the start month has more + // days than the end month. + return -monthDiff(b, a); + } + // difference in months + var wholeMonthDiff = (b.year() - a.year()) * 12 + (b.month() - a.month()), + // b is in (anchor - 1 month, anchor + 1 month) + anchor = a.clone().add(wholeMonthDiff, 'months'), + anchor2, + adjust; + + if (b - anchor < 0) { + anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor - anchor2); + } else { + anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor2 - anchor); + } + + //check for negative zero, return zero if negative zero + return -(wholeMonthDiff + adjust) || 0; + } + + hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; + hooks.defaultFormatUtc = 'YYYY-MM-DDTHH:mm:ss[Z]'; + + function toString() { + return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); + } + + function toISOString(keepOffset) { + if (!this.isValid()) { + return null; + } + var utc = keepOffset !== true, + m = utc ? this.clone().utc() : this; + if (m.year() < 0 || m.year() > 9999) { + return formatMoment( + m, + utc + ? 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]' + : 'YYYYYY-MM-DD[T]HH:mm:ss.SSSZ' + ); + } + if (isFunction(Date.prototype.toISOString)) { + // native implementation is ~50x faster, use it when we can + if (utc) { + return this.toDate().toISOString(); + } else { + return new Date(this.valueOf() + this.utcOffset() * 60 * 1000) + .toISOString() + .replace('Z', formatMoment(m, 'Z')); + } + } + return formatMoment( + m, + utc ? 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]' : 'YYYY-MM-DD[T]HH:mm:ss.SSSZ' + ); + } + + /** + * Return a human readable representation of a moment that can + * also be evaluated to get a new moment which is the same + * + * @link https://nodejs.org/dist/latest/docs/api/util.html#util_custom_inspect_function_on_objects + */ + function inspect() { + if (!this.isValid()) { + return 'moment.invalid(/* ' + this._i + ' */)'; + } + var func = 'moment', + zone = '', + prefix, + year, + datetime, + suffix; + if (!this.isLocal()) { + func = this.utcOffset() === 0 ? 'moment.utc' : 'moment.parseZone'; + zone = 'Z'; + } + prefix = '[' + func + '("]'; + year = 0 <= this.year() && this.year() <= 9999 ? 'YYYY' : 'YYYYYY'; + datetime = '-MM-DD[T]HH:mm:ss.SSS'; + suffix = zone + '[")]'; + + return this.format(prefix + year + datetime + suffix); + } + + function format(inputString) { + if (!inputString) { + inputString = this.isUtc() + ? hooks.defaultFormatUtc + : hooks.defaultFormat; + } + var output = formatMoment(this, inputString); + return this.localeData().postformat(output); + } + + function from(time, withoutSuffix) { + if ( + this.isValid() && + ((isMoment(time) && time.isValid()) || createLocal(time).isValid()) + ) { + return createDuration({ to: this, from: time }) + .locale(this.locale()) + .humanize(!withoutSuffix); + } else { + return this.localeData().invalidDate(); + } + } + + function fromNow(withoutSuffix) { + return this.from(createLocal(), withoutSuffix); + } + + function to(time, withoutSuffix) { + if ( + this.isValid() && + ((isMoment(time) && time.isValid()) || createLocal(time).isValid()) + ) { + return createDuration({ from: this, to: time }) + .locale(this.locale()) + .humanize(!withoutSuffix); + } else { + return this.localeData().invalidDate(); + } + } + + function toNow(withoutSuffix) { + return this.to(createLocal(), withoutSuffix); + } + + // If passed a locale key, it will set the locale for this + // instance. Otherwise, it will return the locale configuration + // variables for this instance. + function locale(key) { + var newLocaleData; + + if (key === undefined) { + return this._locale._abbr; + } else { + newLocaleData = getLocale(key); + if (newLocaleData != null) { + this._locale = newLocaleData; + } + return this; + } + } + + var lang = deprecate( + 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', + function (key) { + if (key === undefined) { + return this.localeData(); + } else { + return this.locale(key); + } + } + ); + + function localeData() { + return this._locale; + } + + var MS_PER_SECOND = 1000, + MS_PER_MINUTE = 60 * MS_PER_SECOND, + MS_PER_HOUR = 60 * MS_PER_MINUTE, + MS_PER_400_YEARS = (365 * 400 + 97) * 24 * MS_PER_HOUR; + + // actual modulo - handles negative numbers (for dates before 1970): + function mod$1(dividend, divisor) { + return ((dividend % divisor) + divisor) % divisor; + } + + function localStartOfDate(y, m, d) { + // the date constructor remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0) { + // preserve leap years using a full 400 year cycle, then reset + return new Date(y + 400, m, d) - MS_PER_400_YEARS; + } else { + return new Date(y, m, d).valueOf(); + } + } + + function utcStartOfDate(y, m, d) { + // Date.UTC remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0) { + // preserve leap years using a full 400 year cycle, then reset + return Date.UTC(y + 400, m, d) - MS_PER_400_YEARS; + } else { + return Date.UTC(y, m, d); + } + } + + function startOf(units) { + var time, startOfDate; + units = normalizeUnits(units); + if (units === undefined || units === 'millisecond' || !this.isValid()) { + return this; + } + + startOfDate = this._isUTC ? utcStartOfDate : localStartOfDate; + + switch (units) { + case 'year': + time = startOfDate(this.year(), 0, 1); + break; + case 'quarter': + time = startOfDate( + this.year(), + this.month() - (this.month() % 3), + 1 + ); + break; + case 'month': + time = startOfDate(this.year(), this.month(), 1); + break; + case 'week': + time = startOfDate( + this.year(), + this.month(), + this.date() - this.weekday() + ); + break; + case 'isoWeek': + time = startOfDate( + this.year(), + this.month(), + this.date() - (this.isoWeekday() - 1) + ); + break; + case 'day': + case 'date': + time = startOfDate(this.year(), this.month(), this.date()); + break; + case 'hour': + time = this._d.valueOf(); + time -= mod$1( + time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), + MS_PER_HOUR + ); + break; + case 'minute': + time = this._d.valueOf(); + time -= mod$1(time, MS_PER_MINUTE); + break; + case 'second': + time = this._d.valueOf(); + time -= mod$1(time, MS_PER_SECOND); + break; + } + + this._d.setTime(time); + hooks.updateOffset(this, true); + return this; + } + + function endOf(units) { + var time, startOfDate; + units = normalizeUnits(units); + if (units === undefined || units === 'millisecond' || !this.isValid()) { + return this; + } + + startOfDate = this._isUTC ? utcStartOfDate : localStartOfDate; + + switch (units) { + case 'year': + time = startOfDate(this.year() + 1, 0, 1) - 1; + break; + case 'quarter': + time = + startOfDate( + this.year(), + this.month() - (this.month() % 3) + 3, + 1 + ) - 1; + break; + case 'month': + time = startOfDate(this.year(), this.month() + 1, 1) - 1; + break; + case 'week': + time = + startOfDate( + this.year(), + this.month(), + this.date() - this.weekday() + 7 + ) - 1; + break; + case 'isoWeek': + time = + startOfDate( + this.year(), + this.month(), + this.date() - (this.isoWeekday() - 1) + 7 + ) - 1; + break; + case 'day': + case 'date': + time = startOfDate(this.year(), this.month(), this.date() + 1) - 1; + break; + case 'hour': + time = this._d.valueOf(); + time += + MS_PER_HOUR - + mod$1( + time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), + MS_PER_HOUR + ) - + 1; + break; + case 'minute': + time = this._d.valueOf(); + time += MS_PER_MINUTE - mod$1(time, MS_PER_MINUTE) - 1; + break; + case 'second': + time = this._d.valueOf(); + time += MS_PER_SECOND - mod$1(time, MS_PER_SECOND) - 1; + break; + } + + this._d.setTime(time); + hooks.updateOffset(this, true); + return this; + } + + function valueOf() { + return this._d.valueOf() - (this._offset || 0) * 60000; + } + + function unix() { + return Math.floor(this.valueOf() / 1000); + } + + function toDate() { + return new Date(this.valueOf()); + } + + function toArray() { + var m = this; + return [ + m.year(), + m.month(), + m.date(), + m.hour(), + m.minute(), + m.second(), + m.millisecond(), + ]; + } + + function toObject() { + var m = this; + return { + years: m.year(), + months: m.month(), + date: m.date(), + hours: m.hours(), + minutes: m.minutes(), + seconds: m.seconds(), + milliseconds: m.milliseconds(), + }; + } + + function toJSON() { + // new Date(NaN).toJSON() === null + return this.isValid() ? this.toISOString() : null; + } + + function isValid$2() { + return isValid(this); + } + + function parsingFlags() { + return extend({}, getParsingFlags(this)); + } + + function invalidAt() { + return getParsingFlags(this).overflow; + } + + function creationData() { + return { + input: this._i, + format: this._f, + locale: this._locale, + isUTC: this._isUTC, + strict: this._strict, + }; + } + + addFormatToken('N', 0, 0, 'eraAbbr'); + addFormatToken('NN', 0, 0, 'eraAbbr'); + addFormatToken('NNN', 0, 0, 'eraAbbr'); + addFormatToken('NNNN', 0, 0, 'eraName'); + addFormatToken('NNNNN', 0, 0, 'eraNarrow'); + + addFormatToken('y', ['y', 1], 'yo', 'eraYear'); + addFormatToken('y', ['yy', 2], 0, 'eraYear'); + addFormatToken('y', ['yyy', 3], 0, 'eraYear'); + addFormatToken('y', ['yyyy', 4], 0, 'eraYear'); + + addRegexToken('N', matchEraAbbr); + addRegexToken('NN', matchEraAbbr); + addRegexToken('NNN', matchEraAbbr); + addRegexToken('NNNN', matchEraName); + addRegexToken('NNNNN', matchEraNarrow); + + addParseToken(['N', 'NN', 'NNN', 'NNNN', 'NNNNN'], function ( + input, + array, + config, + token + ) { + var era = config._locale.erasParse(input, token, config._strict); + if (era) { + getParsingFlags(config).era = era; + } else { + getParsingFlags(config).invalidEra = input; + } + }); + + addRegexToken('y', matchUnsigned); + addRegexToken('yy', matchUnsigned); + addRegexToken('yyy', matchUnsigned); + addRegexToken('yyyy', matchUnsigned); + addRegexToken('yo', matchEraYearOrdinal); + + addParseToken(['y', 'yy', 'yyy', 'yyyy'], YEAR); + addParseToken(['yo'], function (input, array, config, token) { + var match; + if (config._locale._eraYearOrdinalRegex) { + match = input.match(config._locale._eraYearOrdinalRegex); + } + + if (config._locale.eraYearOrdinalParse) { + array[YEAR] = config._locale.eraYearOrdinalParse(input, match); + } else { + array[YEAR] = parseInt(input, 10); + } + }); + + function localeEras(m, format) { + var i, + l, + date, + eras = this._eras || getLocale('en')._eras; + for (i = 0, l = eras.length; i < l; ++i) { + switch (typeof eras[i].since) { + case 'string': + // truncate time + date = hooks(eras[i].since).startOf('day'); + eras[i].since = date.valueOf(); + break; + } + + switch (typeof eras[i].until) { + case 'undefined': + eras[i].until = +Infinity; + break; + case 'string': + // truncate time + date = hooks(eras[i].until).startOf('day').valueOf(); + eras[i].until = date.valueOf(); + break; + } + } + return eras; + } + + function localeErasParse(eraName, format, strict) { + var i, + l, + eras = this.eras(), + name, + abbr, + narrow; + eraName = eraName.toUpperCase(); + + for (i = 0, l = eras.length; i < l; ++i) { + name = eras[i].name.toUpperCase(); + abbr = eras[i].abbr.toUpperCase(); + narrow = eras[i].narrow.toUpperCase(); + + if (strict) { + switch (format) { + case 'N': + case 'NN': + case 'NNN': + if (abbr === eraName) { + return eras[i]; + } + break; + + case 'NNNN': + if (name === eraName) { + return eras[i]; + } + break; + + case 'NNNNN': + if (narrow === eraName) { + return eras[i]; + } + break; + } + } else if ([name, abbr, narrow].indexOf(eraName) >= 0) { + return eras[i]; + } + } + } + + function localeErasConvertYear(era, year) { + var dir = era.since <= era.until ? +1 : -1; + if (year === undefined) { + return hooks(era.since).year(); + } else { + return hooks(era.since).year() + (year - era.offset) * dir; + } + } + + function getEraName() { + var i, + l, + val, + eras = this.localeData().eras(); + for (i = 0, l = eras.length; i < l; ++i) { + // truncate time + val = this.clone().startOf('day').valueOf(); + + if (eras[i].since <= val && val <= eras[i].until) { + return eras[i].name; + } + if (eras[i].until <= val && val <= eras[i].since) { + return eras[i].name; + } + } + + return ''; + } + + function getEraNarrow() { + var i, + l, + val, + eras = this.localeData().eras(); + for (i = 0, l = eras.length; i < l; ++i) { + // truncate time + val = this.clone().startOf('day').valueOf(); + + if (eras[i].since <= val && val <= eras[i].until) { + return eras[i].narrow; + } + if (eras[i].until <= val && val <= eras[i].since) { + return eras[i].narrow; + } + } + + return ''; + } + + function getEraAbbr() { + var i, + l, + val, + eras = this.localeData().eras(); + for (i = 0, l = eras.length; i < l; ++i) { + // truncate time + val = this.clone().startOf('day').valueOf(); + + if (eras[i].since <= val && val <= eras[i].until) { + return eras[i].abbr; + } + if (eras[i].until <= val && val <= eras[i].since) { + return eras[i].abbr; + } + } + + return ''; + } + + function getEraYear() { + var i, + l, + dir, + val, + eras = this.localeData().eras(); + for (i = 0, l = eras.length; i < l; ++i) { + dir = eras[i].since <= eras[i].until ? +1 : -1; + + // truncate time + val = this.clone().startOf('day').valueOf(); + + if ( + (eras[i].since <= val && val <= eras[i].until) || + (eras[i].until <= val && val <= eras[i].since) + ) { + return ( + (this.year() - hooks(eras[i].since).year()) * dir + + eras[i].offset + ); + } + } + + return this.year(); + } + + function erasNameRegex(isStrict) { + if (!hasOwnProp(this, '_erasNameRegex')) { + computeErasParse.call(this); + } + return isStrict ? this._erasNameRegex : this._erasRegex; + } + + function erasAbbrRegex(isStrict) { + if (!hasOwnProp(this, '_erasAbbrRegex')) { + computeErasParse.call(this); + } + return isStrict ? this._erasAbbrRegex : this._erasRegex; + } + + function erasNarrowRegex(isStrict) { + if (!hasOwnProp(this, '_erasNarrowRegex')) { + computeErasParse.call(this); + } + return isStrict ? this._erasNarrowRegex : this._erasRegex; + } + + function matchEraAbbr(isStrict, locale) { + return locale.erasAbbrRegex(isStrict); + } + + function matchEraName(isStrict, locale) { + return locale.erasNameRegex(isStrict); + } + + function matchEraNarrow(isStrict, locale) { + return locale.erasNarrowRegex(isStrict); + } + + function matchEraYearOrdinal(isStrict, locale) { + return locale._eraYearOrdinalRegex || matchUnsigned; + } + + function computeErasParse() { + var abbrPieces = [], + namePieces = [], + narrowPieces = [], + mixedPieces = [], + i, + l, + eras = this.eras(); + + for (i = 0, l = eras.length; i < l; ++i) { + namePieces.push(regexEscape(eras[i].name)); + abbrPieces.push(regexEscape(eras[i].abbr)); + narrowPieces.push(regexEscape(eras[i].narrow)); + + mixedPieces.push(regexEscape(eras[i].name)); + mixedPieces.push(regexEscape(eras[i].abbr)); + mixedPieces.push(regexEscape(eras[i].narrow)); + } + + this._erasRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); + this._erasNameRegex = new RegExp('^(' + namePieces.join('|') + ')', 'i'); + this._erasAbbrRegex = new RegExp('^(' + abbrPieces.join('|') + ')', 'i'); + this._erasNarrowRegex = new RegExp( + '^(' + narrowPieces.join('|') + ')', + 'i' + ); + } + + // FORMATTING + + addFormatToken(0, ['gg', 2], 0, function () { + return this.weekYear() % 100; + }); + + addFormatToken(0, ['GG', 2], 0, function () { + return this.isoWeekYear() % 100; + }); + + function addWeekYearFormatToken(token, getter) { + addFormatToken(0, [token, token.length], 0, getter); + } + + addWeekYearFormatToken('gggg', 'weekYear'); + addWeekYearFormatToken('ggggg', 'weekYear'); + addWeekYearFormatToken('GGGG', 'isoWeekYear'); + addWeekYearFormatToken('GGGGG', 'isoWeekYear'); + + // ALIASES + + addUnitAlias('weekYear', 'gg'); + addUnitAlias('isoWeekYear', 'GG'); + + // PRIORITY + + addUnitPriority('weekYear', 1); + addUnitPriority('isoWeekYear', 1); + + // PARSING + + addRegexToken('G', matchSigned); + addRegexToken('g', matchSigned); + addRegexToken('GG', match1to2, match2); + addRegexToken('gg', match1to2, match2); + addRegexToken('GGGG', match1to4, match4); + addRegexToken('gggg', match1to4, match4); + addRegexToken('GGGGG', match1to6, match6); + addRegexToken('ggggg', match1to6, match6); + + addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function ( + input, + week, + config, + token + ) { + week[token.substr(0, 2)] = toInt(input); + }); + + addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { + week[token] = hooks.parseTwoDigitYear(input); + }); + + // MOMENTS + + function getSetWeekYear(input) { + return getSetWeekYearHelper.call( + this, + input, + this.week(), + this.weekday(), + this.localeData()._week.dow, + this.localeData()._week.doy + ); + } + + function getSetISOWeekYear(input) { + return getSetWeekYearHelper.call( + this, + input, + this.isoWeek(), + this.isoWeekday(), + 1, + 4 + ); + } + + function getISOWeeksInYear() { + return weeksInYear(this.year(), 1, 4); + } + + function getISOWeeksInISOWeekYear() { + return weeksInYear(this.isoWeekYear(), 1, 4); + } + + function getWeeksInYear() { + var weekInfo = this.localeData()._week; + return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); + } + + function getWeeksInWeekYear() { + var weekInfo = this.localeData()._week; + return weeksInYear(this.weekYear(), weekInfo.dow, weekInfo.doy); + } + + function getSetWeekYearHelper(input, week, weekday, dow, doy) { + var weeksTarget; + if (input == null) { + return weekOfYear(this, dow, doy).year; + } else { + weeksTarget = weeksInYear(input, dow, doy); + if (week > weeksTarget) { + week = weeksTarget; + } + return setWeekAll.call(this, input, week, weekday, dow, doy); + } + } + + function setWeekAll(weekYear, week, weekday, dow, doy) { + var dayOfYearData = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy), + date = createUTCDate(dayOfYearData.year, 0, dayOfYearData.dayOfYear); + + this.year(date.getUTCFullYear()); + this.month(date.getUTCMonth()); + this.date(date.getUTCDate()); + return this; + } + + // FORMATTING + + addFormatToken('Q', 0, 'Qo', 'quarter'); + + // ALIASES + + addUnitAlias('quarter', 'Q'); + + // PRIORITY + + addUnitPriority('quarter', 7); + + // PARSING + + addRegexToken('Q', match1); + addParseToken('Q', function (input, array) { + array[MONTH] = (toInt(input) - 1) * 3; + }); + + // MOMENTS + + function getSetQuarter(input) { + return input == null + ? Math.ceil((this.month() + 1) / 3) + : this.month((input - 1) * 3 + (this.month() % 3)); + } + + // FORMATTING + + addFormatToken('D', ['DD', 2], 'Do', 'date'); + + // ALIASES + + addUnitAlias('date', 'D'); + + // PRIORITY + addUnitPriority('date', 9); + + // PARSING + + addRegexToken('D', match1to2); + addRegexToken('DD', match1to2, match2); + addRegexToken('Do', function (isStrict, locale) { + // TODO: Remove "ordinalParse" fallback in next major release. + return isStrict + ? locale._dayOfMonthOrdinalParse || locale._ordinalParse + : locale._dayOfMonthOrdinalParseLenient; + }); + + addParseToken(['D', 'DD'], DATE); + addParseToken('Do', function (input, array) { + array[DATE] = toInt(input.match(match1to2)[0]); + }); + + // MOMENTS + + var getSetDayOfMonth = makeGetSet('Date', true); + + // FORMATTING + + addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); + + // ALIASES + + addUnitAlias('dayOfYear', 'DDD'); + + // PRIORITY + addUnitPriority('dayOfYear', 4); + + // PARSING + + addRegexToken('DDD', match1to3); + addRegexToken('DDDD', match3); + addParseToken(['DDD', 'DDDD'], function (input, array, config) { + config._dayOfYear = toInt(input); + }); + + // HELPERS + + // MOMENTS + + function getSetDayOfYear(input) { + var dayOfYear = + Math.round( + (this.clone().startOf('day') - this.clone().startOf('year')) / 864e5 + ) + 1; + return input == null ? dayOfYear : this.add(input - dayOfYear, 'd'); + } + + // FORMATTING + + addFormatToken('m', ['mm', 2], 0, 'minute'); + + // ALIASES + + addUnitAlias('minute', 'm'); + + // PRIORITY + + addUnitPriority('minute', 14); + + // PARSING + + addRegexToken('m', match1to2); + addRegexToken('mm', match1to2, match2); + addParseToken(['m', 'mm'], MINUTE); + + // MOMENTS + + var getSetMinute = makeGetSet('Minutes', false); + + // FORMATTING + + addFormatToken('s', ['ss', 2], 0, 'second'); + + // ALIASES + + addUnitAlias('second', 's'); + + // PRIORITY + + addUnitPriority('second', 15); + + // PARSING + + addRegexToken('s', match1to2); + addRegexToken('ss', match1to2, match2); + addParseToken(['s', 'ss'], SECOND); + + // MOMENTS + + var getSetSecond = makeGetSet('Seconds', false); + + // FORMATTING + + addFormatToken('S', 0, 0, function () { + return ~~(this.millisecond() / 100); + }); + + addFormatToken(0, ['SS', 2], 0, function () { + return ~~(this.millisecond() / 10); + }); + + addFormatToken(0, ['SSS', 3], 0, 'millisecond'); + addFormatToken(0, ['SSSS', 4], 0, function () { + return this.millisecond() * 10; + }); + addFormatToken(0, ['SSSSS', 5], 0, function () { + return this.millisecond() * 100; + }); + addFormatToken(0, ['SSSSSS', 6], 0, function () { + return this.millisecond() * 1000; + }); + addFormatToken(0, ['SSSSSSS', 7], 0, function () { + return this.millisecond() * 10000; + }); + addFormatToken(0, ['SSSSSSSS', 8], 0, function () { + return this.millisecond() * 100000; + }); + addFormatToken(0, ['SSSSSSSSS', 9], 0, function () { + return this.millisecond() * 1000000; + }); + + // ALIASES + + addUnitAlias('millisecond', 'ms'); + + // PRIORITY + + addUnitPriority('millisecond', 16); + + // PARSING + + addRegexToken('S', match1to3, match1); + addRegexToken('SS', match1to3, match2); + addRegexToken('SSS', match1to3, match3); + + var token, getSetMillisecond; + for (token = 'SSSS'; token.length <= 9; token += 'S') { + addRegexToken(token, matchUnsigned); + } + + function parseMs(input, array) { + array[MILLISECOND] = toInt(('0.' + input) * 1000); + } + + for (token = 'S'; token.length <= 9; token += 'S') { + addParseToken(token, parseMs); + } + + getSetMillisecond = makeGetSet('Milliseconds', false); + + // FORMATTING + + addFormatToken('z', 0, 0, 'zoneAbbr'); + addFormatToken('zz', 0, 0, 'zoneName'); + + // MOMENTS + + function getZoneAbbr() { + return this._isUTC ? 'UTC' : ''; + } + + function getZoneName() { + return this._isUTC ? 'Coordinated Universal Time' : ''; + } + + var proto = Moment.prototype; + + proto.add = add; + proto.calendar = calendar$1; + proto.clone = clone; + proto.diff = diff; + proto.endOf = endOf; + proto.format = format; + proto.from = from; + proto.fromNow = fromNow; + proto.to = to; + proto.toNow = toNow; + proto.get = stringGet; + proto.invalidAt = invalidAt; + proto.isAfter = isAfter; + proto.isBefore = isBefore; + proto.isBetween = isBetween; + proto.isSame = isSame; + proto.isSameOrAfter = isSameOrAfter; + proto.isSameOrBefore = isSameOrBefore; + proto.isValid = isValid$2; + proto.lang = lang; + proto.locale = locale; + proto.localeData = localeData; + proto.max = prototypeMax; + proto.min = prototypeMin; + proto.parsingFlags = parsingFlags; + proto.set = stringSet; + proto.startOf = startOf; + proto.subtract = subtract; + proto.toArray = toArray; + proto.toObject = toObject; + proto.toDate = toDate; + proto.toISOString = toISOString; + proto.inspect = inspect; + if (typeof Symbol !== 'undefined' && Symbol.for != null) { + proto[Symbol.for('nodejs.util.inspect.custom')] = function () { + return 'Moment<' + this.format() + '>'; + }; + } + proto.toJSON = toJSON; + proto.toString = toString; + proto.unix = unix; + proto.valueOf = valueOf; + proto.creationData = creationData; + proto.eraName = getEraName; + proto.eraNarrow = getEraNarrow; + proto.eraAbbr = getEraAbbr; + proto.eraYear = getEraYear; + proto.year = getSetYear; + proto.isLeapYear = getIsLeapYear; + proto.weekYear = getSetWeekYear; + proto.isoWeekYear = getSetISOWeekYear; + proto.quarter = proto.quarters = getSetQuarter; + proto.month = getSetMonth; + proto.daysInMonth = getDaysInMonth; + proto.week = proto.weeks = getSetWeek; + proto.isoWeek = proto.isoWeeks = getSetISOWeek; + proto.weeksInYear = getWeeksInYear; + proto.weeksInWeekYear = getWeeksInWeekYear; + proto.isoWeeksInYear = getISOWeeksInYear; + proto.isoWeeksInISOWeekYear = getISOWeeksInISOWeekYear; + proto.date = getSetDayOfMonth; + proto.day = proto.days = getSetDayOfWeek; + proto.weekday = getSetLocaleDayOfWeek; + proto.isoWeekday = getSetISODayOfWeek; + proto.dayOfYear = getSetDayOfYear; + proto.hour = proto.hours = getSetHour; + proto.minute = proto.minutes = getSetMinute; + proto.second = proto.seconds = getSetSecond; + proto.millisecond = proto.milliseconds = getSetMillisecond; + proto.utcOffset = getSetOffset; + proto.utc = setOffsetToUTC; + proto.local = setOffsetToLocal; + proto.parseZone = setOffsetToParsedOffset; + proto.hasAlignedHourOffset = hasAlignedHourOffset; + proto.isDST = isDaylightSavingTime; + proto.isLocal = isLocal; + proto.isUtcOffset = isUtcOffset; + proto.isUtc = isUtc; + proto.isUTC = isUtc; + proto.zoneAbbr = getZoneAbbr; + proto.zoneName = getZoneName; + proto.dates = deprecate( + 'dates accessor is deprecated. Use date instead.', + getSetDayOfMonth + ); + proto.months = deprecate( + 'months accessor is deprecated. Use month instead', + getSetMonth + ); + proto.years = deprecate( + 'years accessor is deprecated. Use year instead', + getSetYear + ); + proto.zone = deprecate( + 'moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/', + getSetZone + ); + proto.isDSTShifted = deprecate( + 'isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information', + isDaylightSavingTimeShifted + ); + + function createUnix(input) { + return createLocal(input * 1000); + } + + function createInZone() { + return createLocal.apply(null, arguments).parseZone(); + } + + function preParsePostFormat(string) { + return string; + } + + var proto$1 = Locale.prototype; + + proto$1.calendar = calendar; + proto$1.longDateFormat = longDateFormat; + proto$1.invalidDate = invalidDate; + proto$1.ordinal = ordinal; + proto$1.preparse = preParsePostFormat; + proto$1.postformat = preParsePostFormat; + proto$1.relativeTime = relativeTime; + proto$1.pastFuture = pastFuture; + proto$1.set = set; + proto$1.eras = localeEras; + proto$1.erasParse = localeErasParse; + proto$1.erasConvertYear = localeErasConvertYear; + proto$1.erasAbbrRegex = erasAbbrRegex; + proto$1.erasNameRegex = erasNameRegex; + proto$1.erasNarrowRegex = erasNarrowRegex; + + proto$1.months = localeMonths; + proto$1.monthsShort = localeMonthsShort; + proto$1.monthsParse = localeMonthsParse; + proto$1.monthsRegex = monthsRegex; + proto$1.monthsShortRegex = monthsShortRegex; + proto$1.week = localeWeek; + proto$1.firstDayOfYear = localeFirstDayOfYear; + proto$1.firstDayOfWeek = localeFirstDayOfWeek; + + proto$1.weekdays = localeWeekdays; + proto$1.weekdaysMin = localeWeekdaysMin; + proto$1.weekdaysShort = localeWeekdaysShort; + proto$1.weekdaysParse = localeWeekdaysParse; + + proto$1.weekdaysRegex = weekdaysRegex; + proto$1.weekdaysShortRegex = weekdaysShortRegex; + proto$1.weekdaysMinRegex = weekdaysMinRegex; + + proto$1.isPM = localeIsPM; + proto$1.meridiem = localeMeridiem; + + function get$1(format, index, field, setter) { + var locale = getLocale(), + utc = createUTC().set(setter, index); + return locale[field](utc, format); + } + + function listMonthsImpl(format, index, field) { + if (isNumber(format)) { + index = format; + format = undefined; + } + + format = format || ''; + + if (index != null) { + return get$1(format, index, field, 'month'); + } + + var i, + out = []; + for (i = 0; i < 12; i++) { + out[i] = get$1(format, i, field, 'month'); + } + return out; + } + + // () + // (5) + // (fmt, 5) + // (fmt) + // (true) + // (true, 5) + // (true, fmt, 5) + // (true, fmt) + function listWeekdaysImpl(localeSorted, format, index, field) { + if (typeof localeSorted === 'boolean') { + if (isNumber(format)) { + index = format; + format = undefined; + } + + format = format || ''; + } else { + format = localeSorted; + index = format; + localeSorted = false; + + if (isNumber(format)) { + index = format; + format = undefined; + } + + format = format || ''; + } + + var locale = getLocale(), + shift = localeSorted ? locale._week.dow : 0, + i, + out = []; + + if (index != null) { + return get$1(format, (index + shift) % 7, field, 'day'); + } + + for (i = 0; i < 7; i++) { + out[i] = get$1(format, (i + shift) % 7, field, 'day'); + } + return out; + } + + function listMonths(format, index) { + return listMonthsImpl(format, index, 'months'); + } + + function listMonthsShort(format, index) { + return listMonthsImpl(format, index, 'monthsShort'); + } + + function listWeekdays(localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdays'); + } + + function listWeekdaysShort(localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdaysShort'); + } + + function listWeekdaysMin(localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdaysMin'); + } + + getSetGlobalLocale('en', { + eras: [ + { + since: '0001-01-01', + until: +Infinity, + offset: 1, + name: 'Anno Domini', + narrow: 'AD', + abbr: 'AD', + }, + { + since: '0000-12-31', + until: -Infinity, + offset: 1, + name: 'Before Christ', + narrow: 'BC', + abbr: 'BC', + }, + ], + dayOfMonthOrdinalParse: /\d{1,2}(th|st|nd|rd)/, + ordinal: function (number) { + var b = number % 10, + output = + toInt((number % 100) / 10) === 1 + ? 'th' + : b === 1 + ? 'st' + : b === 2 + ? 'nd' + : b === 3 + ? 'rd' + : 'th'; + return number + output; + }, + }); + + // Side effect imports + + hooks.lang = deprecate( + 'moment.lang is deprecated. Use moment.locale instead.', + getSetGlobalLocale + ); + hooks.langData = deprecate( + 'moment.langData is deprecated. Use moment.localeData instead.', + getLocale + ); + + var mathAbs = Math.abs; + + function abs() { + var data = this._data; + + this._milliseconds = mathAbs(this._milliseconds); + this._days = mathAbs(this._days); + this._months = mathAbs(this._months); + + data.milliseconds = mathAbs(data.milliseconds); + data.seconds = mathAbs(data.seconds); + data.minutes = mathAbs(data.minutes); + data.hours = mathAbs(data.hours); + data.months = mathAbs(data.months); + data.years = mathAbs(data.years); + + return this; + } + + function addSubtract$1(duration, input, value, direction) { + var other = createDuration(input, value); + + duration._milliseconds += direction * other._milliseconds; + duration._days += direction * other._days; + duration._months += direction * other._months; + + return duration._bubble(); + } + + // supports only 2.0-style add(1, 's') or add(duration) + function add$1(input, value) { + return addSubtract$1(this, input, value, 1); + } + + // supports only 2.0-style subtract(1, 's') or subtract(duration) + function subtract$1(input, value) { + return addSubtract$1(this, input, value, -1); + } + + function absCeil(number) { + if (number < 0) { + return Math.floor(number); + } else { + return Math.ceil(number); + } + } + + function bubble() { + var milliseconds = this._milliseconds, + days = this._days, + months = this._months, + data = this._data, + seconds, + minutes, + hours, + years, + monthsFromDays; + + // if we have a mix of positive and negative values, bubble down first + // check: https://github.com/moment/moment/issues/2166 + if ( + !( + (milliseconds >= 0 && days >= 0 && months >= 0) || + (milliseconds <= 0 && days <= 0 && months <= 0) + ) + ) { + milliseconds += absCeil(monthsToDays(months) + days) * 864e5; + days = 0; + months = 0; + } + + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; + + seconds = absFloor(milliseconds / 1000); + data.seconds = seconds % 60; + + minutes = absFloor(seconds / 60); + data.minutes = minutes % 60; + + hours = absFloor(minutes / 60); + data.hours = hours % 24; + + days += absFloor(hours / 24); + + // convert days to months + monthsFromDays = absFloor(daysToMonths(days)); + months += monthsFromDays; + days -= absCeil(monthsToDays(monthsFromDays)); + + // 12 months -> 1 year + years = absFloor(months / 12); + months %= 12; + + data.days = days; + data.months = months; + data.years = years; + + return this; + } + + function daysToMonths(days) { + // 400 years have 146097 days (taking into account leap year rules) + // 400 years have 12 months === 4800 + return (days * 4800) / 146097; + } + + function monthsToDays(months) { + // the reverse of daysToMonths + return (months * 146097) / 4800; + } + + function as(units) { + if (!this.isValid()) { + return NaN; + } + var days, + months, + milliseconds = this._milliseconds; + + units = normalizeUnits(units); + + if (units === 'month' || units === 'quarter' || units === 'year') { + days = this._days + milliseconds / 864e5; + months = this._months + daysToMonths(days); + switch (units) { + case 'month': + return months; + case 'quarter': + return months / 3; + case 'year': + return months / 12; + } + } else { + // handle milliseconds separately because of floating point math errors (issue #1867) + days = this._days + Math.round(monthsToDays(this._months)); + switch (units) { + case 'week': + return days / 7 + milliseconds / 6048e5; + case 'day': + return days + milliseconds / 864e5; + case 'hour': + return days * 24 + milliseconds / 36e5; + case 'minute': + return days * 1440 + milliseconds / 6e4; + case 'second': + return days * 86400 + milliseconds / 1000; + // Math.floor prevents floating point math errors here + case 'millisecond': + return Math.floor(days * 864e5) + milliseconds; + default: + throw new Error('Unknown unit ' + units); + } + } + } + + // TODO: Use this.as('ms')? + function valueOf$1() { + if (!this.isValid()) { + return NaN; + } + return ( + this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6 + ); + } + + function makeAs(alias) { + return function () { + return this.as(alias); + }; + } + + var asMilliseconds = makeAs('ms'), + asSeconds = makeAs('s'), + asMinutes = makeAs('m'), + asHours = makeAs('h'), + asDays = makeAs('d'), + asWeeks = makeAs('w'), + asMonths = makeAs('M'), + asQuarters = makeAs('Q'), + asYears = makeAs('y'); + + function clone$1() { + return createDuration(this); + } + + function get$2(units) { + units = normalizeUnits(units); + return this.isValid() ? this[units + 's']() : NaN; + } + + function makeGetter(name) { + return function () { + return this.isValid() ? this._data[name] : NaN; + }; + } + + var milliseconds = makeGetter('milliseconds'), + seconds = makeGetter('seconds'), + minutes = makeGetter('minutes'), + hours = makeGetter('hours'), + days = makeGetter('days'), + months = makeGetter('months'), + years = makeGetter('years'); + + function weeks() { + return absFloor(this.days() / 7); + } + + var round = Math.round, + thresholds = { + ss: 44, // a few seconds to seconds + s: 45, // seconds to minute + m: 45, // minutes to hour + h: 22, // hours to day + d: 26, // days to month/week + w: null, // weeks to month + M: 11, // months to year + }; + + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { + return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + } + + function relativeTime$1(posNegDuration, withoutSuffix, thresholds, locale) { + var duration = createDuration(posNegDuration).abs(), + seconds = round(duration.as('s')), + minutes = round(duration.as('m')), + hours = round(duration.as('h')), + days = round(duration.as('d')), + months = round(duration.as('M')), + weeks = round(duration.as('w')), + years = round(duration.as('y')), + a = + (seconds <= thresholds.ss && ['s', seconds]) || + (seconds < thresholds.s && ['ss', seconds]) || + (minutes <= 1 && ['m']) || + (minutes < thresholds.m && ['mm', minutes]) || + (hours <= 1 && ['h']) || + (hours < thresholds.h && ['hh', hours]) || + (days <= 1 && ['d']) || + (days < thresholds.d && ['dd', days]); + + if (thresholds.w != null) { + a = + a || + (weeks <= 1 && ['w']) || + (weeks < thresholds.w && ['ww', weeks]); + } + a = a || + (months <= 1 && ['M']) || + (months < thresholds.M && ['MM', months]) || + (years <= 1 && ['y']) || ['yy', years]; + + a[2] = withoutSuffix; + a[3] = +posNegDuration > 0; + a[4] = locale; + return substituteTimeAgo.apply(null, a); + } + + // This function allows you to set the rounding function for relative time strings + function getSetRelativeTimeRounding(roundingFunction) { + if (roundingFunction === undefined) { + return round; + } + if (typeof roundingFunction === 'function') { + round = roundingFunction; + return true; + } + return false; + } + + // This function allows you to set a threshold for relative time strings + function getSetRelativeTimeThreshold(threshold, limit) { + if (thresholds[threshold] === undefined) { + return false; + } + if (limit === undefined) { + return thresholds[threshold]; + } + thresholds[threshold] = limit; + if (threshold === 's') { + thresholds.ss = limit - 1; + } + return true; + } + + function humanize(argWithSuffix, argThresholds) { + if (!this.isValid()) { + return this.localeData().invalidDate(); + } + + var withSuffix = false, + th = thresholds, + locale, + output; + + if (typeof argWithSuffix === 'object') { + argThresholds = argWithSuffix; + argWithSuffix = false; + } + if (typeof argWithSuffix === 'boolean') { + withSuffix = argWithSuffix; + } + if (typeof argThresholds === 'object') { + th = Object.assign({}, thresholds, argThresholds); + if (argThresholds.s != null && argThresholds.ss == null) { + th.ss = argThresholds.s - 1; + } + } + + locale = this.localeData(); + output = relativeTime$1(this, !withSuffix, th, locale); + + if (withSuffix) { + output = locale.pastFuture(+this, output); + } + + return locale.postformat(output); + } + + var abs$1 = Math.abs; + + function sign(x) { + return (x > 0) - (x < 0) || +x; + } + + function toISOString$1() { + // for ISO strings we do not use the normal bubbling rules: + // * milliseconds bubble up until they become hours + // * days do not bubble at all + // * months bubble up until they become years + // This is because there is no context-free conversion between hours and days + // (think of clock changes) + // and also not between days and months (28-31 days per month) + if (!this.isValid()) { + return this.localeData().invalidDate(); + } + + var seconds = abs$1(this._milliseconds) / 1000, + days = abs$1(this._days), + months = abs$1(this._months), + minutes, + hours, + years, + s, + total = this.asSeconds(), + totalSign, + ymSign, + daysSign, + hmsSign; + + if (!total) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } + + // 3600 seconds -> 60 minutes -> 1 hour + minutes = absFloor(seconds / 60); + hours = absFloor(minutes / 60); + seconds %= 60; + minutes %= 60; + + // 12 months -> 1 year + years = absFloor(months / 12); + months %= 12; + + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + s = seconds ? seconds.toFixed(3).replace(/\.?0+$/, '') : ''; + + totalSign = total < 0 ? '-' : ''; + ymSign = sign(this._months) !== sign(total) ? '-' : ''; + daysSign = sign(this._days) !== sign(total) ? '-' : ''; + hmsSign = sign(this._milliseconds) !== sign(total) ? '-' : ''; + + return ( + totalSign + + 'P' + + (years ? ymSign + years + 'Y' : '') + + (months ? ymSign + months + 'M' : '') + + (days ? daysSign + days + 'D' : '') + + (hours || minutes || seconds ? 'T' : '') + + (hours ? hmsSign + hours + 'H' : '') + + (minutes ? hmsSign + minutes + 'M' : '') + + (seconds ? hmsSign + s + 'S' : '') + ); + } + + var proto$2 = Duration.prototype; + + proto$2.isValid = isValid$1; + proto$2.abs = abs; + proto$2.add = add$1; + proto$2.subtract = subtract$1; + proto$2.as = as; + proto$2.asMilliseconds = asMilliseconds; + proto$2.asSeconds = asSeconds; + proto$2.asMinutes = asMinutes; + proto$2.asHours = asHours; + proto$2.asDays = asDays; + proto$2.asWeeks = asWeeks; + proto$2.asMonths = asMonths; + proto$2.asQuarters = asQuarters; + proto$2.asYears = asYears; + proto$2.valueOf = valueOf$1; + proto$2._bubble = bubble; + proto$2.clone = clone$1; + proto$2.get = get$2; + proto$2.milliseconds = milliseconds; + proto$2.seconds = seconds; + proto$2.minutes = minutes; + proto$2.hours = hours; + proto$2.days = days; + proto$2.weeks = weeks; + proto$2.months = months; + proto$2.years = years; + proto$2.humanize = humanize; + proto$2.toISOString = toISOString$1; + proto$2.toString = toISOString$1; + proto$2.toJSON = toISOString$1; + proto$2.locale = locale; + proto$2.localeData = localeData; + + proto$2.toIsoString = deprecate( + 'toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', + toISOString$1 + ); + proto$2.lang = lang; + + // FORMATTING + + addFormatToken('X', 0, 0, 'unix'); + addFormatToken('x', 0, 0, 'valueOf'); + + // PARSING + + addRegexToken('x', matchSigned); + addRegexToken('X', matchTimestamp); + addParseToken('X', function (input, array, config) { + config._d = new Date(parseFloat(input) * 1000); + }); + addParseToken('x', function (input, array, config) { + config._d = new Date(toInt(input)); + }); + + //! moment.js + + hooks.version = '2.29.1'; + + setHookCallback(createLocal); + + hooks.fn = proto; + hooks.min = min; + hooks.max = max; + hooks.now = now; + hooks.utc = createUTC; + hooks.unix = createUnix; + hooks.months = listMonths; + hooks.isDate = isDate; + hooks.locale = getSetGlobalLocale; + hooks.invalid = createInvalid; + hooks.duration = createDuration; + hooks.isMoment = isMoment; + hooks.weekdays = listWeekdays; + hooks.parseZone = createInZone; + hooks.localeData = getLocale; + hooks.isDuration = isDuration; + hooks.monthsShort = listMonthsShort; + hooks.weekdaysMin = listWeekdaysMin; + hooks.defineLocale = defineLocale; + hooks.updateLocale = updateLocale; + hooks.locales = listLocales; + hooks.weekdaysShort = listWeekdaysShort; + hooks.normalizeUnits = normalizeUnits; + hooks.relativeTimeRounding = getSetRelativeTimeRounding; + hooks.relativeTimeThreshold = getSetRelativeTimeThreshold; + hooks.calendarFormat = getCalendarFormat; + hooks.prototype = proto; + + // currently HTML5 input type only supports 24-hour formats + hooks.HTML5_FMT = { + DATETIME_LOCAL: 'YYYY-MM-DDTHH:mm', // + DATETIME_LOCAL_SECONDS: 'YYYY-MM-DDTHH:mm:ss', // + DATETIME_LOCAL_MS: 'YYYY-MM-DDTHH:mm:ss.SSS', // + DATE: 'YYYY-MM-DD', // + TIME: 'HH:mm', // + TIME_SECONDS: 'HH:mm:ss', // + TIME_MS: 'HH:mm:ss.SSS', // + WEEK: 'GGGG-[W]WW', // + MONTH: 'YYYY-MM', // + }; + + return hooks; + +}))); diff --git a/pkg/http-server/assets/webhook-scan-logs.css b/pkg/http-server/assets/webhook-scan-logs.css new file mode 100644 index 000000000..5e701fb4c --- /dev/null +++ b/pkg/http-server/assets/webhook-scan-logs.css @@ -0,0 +1,33 @@ +ul { + list-style: none; + margin-left: 16px; + padding: 0; +} + +.jsontree_child-nodes { + margin-left: 16px; + padding-left: 16px; +} + +* { + box-sizing: border-box; +} + +.table-sm td, .table-sm th { + padding: 10px; +} + +.review-status.warn { + color: #e28d43; + font-weight: bold; +} + +.review-status.rejected { + color: #e24343; + font-weight: bold; +} + +.review-status.allowed { + color: #43e268; + font-weight: bold; +} diff --git a/pkg/http-server/assets/webhook-scan-logs.js b/pkg/http-server/assets/webhook-scan-logs.js new file mode 100644 index 000000000..86553323b --- /dev/null +++ b/pkg/http-server/assets/webhook-scan-logs.js @@ -0,0 +1,44 @@ +// Replace all json-objects elements to be a JSON tree +let jsonElements = document.getElementsByClassName("json-object") +for (var i = 0; i < jsonElements.length; i++) { + let element = jsonElements[i] + if (element.innerText.length < 1) { + continue + } + + let data = JSON.parse(element.innerText); + element.innerText = "" + + jsonTree.create(data, element); +} + +// Replace all time-object elements to be in the 'DD/MM/YYYY hh:mm:ss A' format of moment.js +let timeElements = document.getElementsByClassName("time-object") +for (var i = 0; i < timeElements.length; i++) { + let element = timeElements[i] + let elapsedTimeUntilNow = Date.now() - new Date(element.innerText) + if (elapsedTimeUntilNow / 1000 < 120) { + // In case elapsed less than 2 minutes, show "A few seconds ago" + element.innerText = moment(element.innerText).fromNow() + } + else { + element.innerText = moment(element.innerText).format('DD/MM/YYYY hh:mm:ss A') + } +} + +// Change the colors of the review status +let statusElements = document.getElementsByClassName("review-status") +for (var i = 0; i < statusElements.length; i++) { + let element = statusElements[i] + switch (element.innerText) { + case "Allowed": + element.classList.add("allowed") + break + case "Rejected": + element.classList.add("rejected") + break + default: + element.classList.add("warn") + break + } +} diff --git a/pkg/http-server/constants.go b/pkg/http-server/constants.go index b0a10227a..07097a4d3 100644 --- a/pkg/http-server/constants.go +++ b/pkg/http-server/constants.go @@ -20,6 +20,9 @@ const ( // GatewayDefaultPort - default port at which the http server listens GatewayDefaultPort = "9010" + // GatewayDefaultPort - default port at which the https server listens + TLSGatewayDefaultPort = "9443" + // APIVersion - default api version for REST endpoints APIVersion = "v1" diff --git a/pkg/http-server/handler.go b/pkg/http-server/handler.go index eb86e5a90..41fc52691 100644 --- a/pkg/http-server/handler.go +++ b/pkg/http-server/handler.go @@ -18,10 +18,13 @@ package httpserver // APIHandler struct for http api server type APIHandler struct { - test bool + test bool + configFile string } // NewAPIHandler returns a new APIHandler{} -func NewAPIHandler() *APIHandler { - return &APIHandler{} +func NewAPIHandler(configFile string) *APIHandler { + return &APIHandler{ + configFile: configFile, + } } diff --git a/pkg/http-server/handler_test.go b/pkg/http-server/handler_test.go index 15c926eb1..dd394336b 100644 --- a/pkg/http-server/handler_test.go +++ b/pkg/http-server/handler_test.go @@ -8,8 +8,10 @@ import ( func TestNewAPIHandler(t *testing.T) { t.Run("new API gateway", func(t *testing.T) { var ( - want = APIHandler{} - got = NewAPIHandler() + want = APIHandler{ + configFile: "", + } + got = NewAPIHandler("") ) if !reflect.DeepEqual(*got, want) { t.Errorf("got: '%v', want: '%v'", *got, want) diff --git a/pkg/http-server/health_test.go b/pkg/http-server/health_test.go index 08b92f8a8..2a447ea47 100644 --- a/pkg/http-server/health_test.go +++ b/pkg/http-server/health_test.go @@ -8,7 +8,7 @@ import ( func TestHealth(t *testing.T) { - handler := NewAPIHandler() + handler := NewAPIHandler("") t.Run("test health api", func(t *testing.T) { var ( diff --git a/pkg/http-server/k8s_testdata/config-deny-category.toml b/pkg/http-server/k8s_testdata/config-deny-category.toml new file mode 100644 index 000000000..b3d4204f7 --- /dev/null +++ b/pkg/http-server/k8s_testdata/config-deny-category.toml @@ -0,0 +1,5 @@ +[k8s-deny-rules] + denied-categories = [ + "Identity and Access Management", + "Network Security", + ] diff --git a/pkg/http-server/k8s_testdata/config-deny-high.toml b/pkg/http-server/k8s_testdata/config-deny-high.toml new file mode 100644 index 000000000..5046654bc --- /dev/null +++ b/pkg/http-server/k8s_testdata/config-deny-high.toml @@ -0,0 +1,5 @@ +[severity] +level = "medium" + +[k8s-deny-rules] + denied-severity = "high" diff --git a/pkg/http-server/k8s_testdata/config-deny-non-existing-category.toml b/pkg/http-server/k8s_testdata/config-deny-non-existing-category.toml new file mode 100644 index 000000000..d38bc482e --- /dev/null +++ b/pkg/http-server/k8s_testdata/config-deny-non-existing-category.toml @@ -0,0 +1,8 @@ +[severity] +level = "medium" + +[k8s-deny-rules] + denied-categories = [ + "Hola", + "Invalid", + ] diff --git a/pkg/http-server/k8s_testdata/config-medium-severity.toml b/pkg/http-server/k8s_testdata/config-medium-severity.toml new file mode 100644 index 000000000..17b1aed23 --- /dev/null +++ b/pkg/http-server/k8s_testdata/config-medium-severity.toml @@ -0,0 +1,2 @@ +[severity] +level = "medium" diff --git a/pkg/http-server/k8s_testdata/config-specific-rule.toml b/pkg/http-server/k8s_testdata/config-specific-rule.toml new file mode 100644 index 000000000..ad35bd7c2 --- /dev/null +++ b/pkg/http-server/k8s_testdata/config-specific-rule.toml @@ -0,0 +1,5 @@ +[rules] + scan-rules = [ + "AWS.S3Bucket.DS.High.1043", + "accurics.kubernetes.IAM.107" + ] diff --git a/pkg/http-server/k8s_testdata/empty.json b/pkg/http-server/k8s_testdata/empty.json new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/http-server/k8s_testdata/empty_object.json b/pkg/http-server/k8s_testdata/empty_object.json new file mode 100644 index 000000000..fb4f3bad9 --- /dev/null +++ b/pkg/http-server/k8s_testdata/empty_object.json @@ -0,0 +1,7 @@ +{ + "apiVersion": "admission.k8s.io/v1", + "kind": "AdmissionReview", + "request": { + "uid": "705ab4f5-6393-11e8-b7cc-42010a800002" + } +} diff --git a/pkg/http-server/k8s_testdata/invalid.json b/pkg/http-server/k8s_testdata/invalid.json new file mode 100644 index 000000000..f9ff3aaaf --- /dev/null +++ b/pkg/http-server/k8s_testdata/invalid.json @@ -0,0 +1 @@ +some invalid tf file diff --git a/pkg/http-server/k8s_testdata/risky_testconfig.json b/pkg/http-server/k8s_testdata/risky_testconfig.json new file mode 100644 index 000000000..3b1f42d2a --- /dev/null +++ b/pkg/http-server/k8s_testdata/risky_testconfig.json @@ -0,0 +1,27 @@ +{ + "apiVersion":"admission.k8s.io/v1", + "kind":"AdmissionReview", + "request":{ + "uid":"705ab4f5-6393-11e8-b7cc-42010a800002", + "object": + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "root-run-unset" + }, + "spec": { + "containers": [ + { + "name": "busybox", + "image": "busybox", + "securityContext": { + "allowPrivilegeEscalation": false, + "readOnlyRootFilesystem": true + } + } + ] + } + } + } +} diff --git a/pkg/http-server/k8s_testdata/testconfig.json b/pkg/http-server/k8s_testdata/testconfig.json new file mode 100644 index 000000000..6e3bc1a1f --- /dev/null +++ b/pkg/http-server/k8s_testdata/testconfig.json @@ -0,0 +1,27 @@ +{ + "apiVersion": "admission.k8s.io/v1", + "kind": "AdmissionReview", + "request": { + "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", + "object": { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "creationTimestamp": "2021-02-16T19:16:01Z", + "labels": { + "run": "nginx" + }, + "name": "nginx", + "namespace": "default", + "resourceVersion": "17561", + "selfLink": "/api/v1/namespaces/default/pods/nginx", + "uid": "7a269efe-d951-49b6-a3af-e1a265cb9efe" + }, + "spec": { + "containers": [ + ] + } + }, + "operation": "CREATE" + } +} diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json new file mode 100755 index 000000000..24409fb1c --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json @@ -0,0 +1,21 @@ +{ + "name": "privilegeEscalationCheck", + "file": "securityContextCheck.rego", + "template_args": { + "allowed": "false", + "arg1": "cpu", + "arg2": "limits", + "name": "privilegeEscalationCheck", + "not_allowed": "true", + "param": "allowPrivilegeEscalation", + "param1": "securityContext", + "prefix": "", + "suffix": "", + "value": "true" + }, + "severity": "HIGH", + "description": "Containers Should Not Run with AllowPrivilegeEscalation", + "reference_id": "AC-K8-CA-PO-H-0165", + "category": "Cloud Assets Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json new file mode 100755 index 000000000..d0bff541c --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json @@ -0,0 +1,14 @@ +{ + "name": "kubeDashboardEnabled", + "file": "kubeDashboardEnabled.rego", + "template_args": { + "name": "kubeDashboardEnabled", + "prefix": "", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Ensure Kubernetes Dashboard Is Not Deployed", + "reference_id": "AC-K8-DS-PO-M-0176", + "category": "Data Security", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json new file mode 100755 index 000000000..d8a40cc5e --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json @@ -0,0 +1,14 @@ +{ + "name": "tillerDeployed", + "file": "tillerDeployed.rego", + "template_args": { + "name": "tillerDeployed", + "prefix": "", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Ensure That Tiller (Helm V2) Is Not Deployed", + "reference_id": "AC-K8-DS-PO-M-0177", + "category": "Data Security", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json new file mode 100755 index 000000000..d07858d49 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json @@ -0,0 +1,14 @@ +{ + "name": "priviledgedContainersEnabled", + "file": "priviledgedContainersEnabled.rego", + "template_args": { + "name": "priviledgedContainersEnabled", + "prefix": "", + "suffix": "" + }, + "severity": "HIGH", + "description": "Minimize the admission of privileged containers", + "reference_id": "AC-K8-IA-PO-H-0106", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json new file mode 100755 index 000000000..71f74c306 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json @@ -0,0 +1,14 @@ +{ + "name": "disallowedSysCalls", + "file": "disallowedSysCalls.rego", + "template_args": { + "name": "disallowedSysCalls", + "prefix": "", + "suffix": "" + }, + "severity": "HIGH", + "description": "Allowing the pod to make system level calls provide access to host/node sensitive information", + "reference_id": "AC-K8-IA-PO-H-0137", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json new file mode 100755 index 000000000..16cfd6d99 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json @@ -0,0 +1,14 @@ +{ + "name": "allowedHostPath", + "file": "allowedHostPath.rego", + "template_args": { + "name": "allowedHostPath", + "prefix": "", + "suffix": "" + }, + "severity": "HIGH", + "description": "Allowing hostPaths to mount to Pod arise the probability of getting access to the node's filesystem", + "reference_id": "AC-K8-IA-PO-H-0138", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json new file mode 100755 index 000000000..f7c9d5424 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json @@ -0,0 +1,21 @@ +{ + "name": "runAsNonRootCheck", + "file": "securityContextCheck.rego", + "template_args": { + "allowed": "false", + "arg1": "cpu", + "arg2": "limits", + "name": "runAsNonRootCheck", + "not_allowed": "true", + "param": "runAsNonRoot", + "param1": "securityContext", + "prefix": "", + "suffix": "", + "value": "false" + }, + "severity": "HIGH", + "description": "Minimize Admission of Root Containers", + "reference_id": "AC-K8-IA-PO-H-0168", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json new file mode 100755 index 000000000..6f9be71f1 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json @@ -0,0 +1,14 @@ +{ + "name": "autoMountTokenEnabled", + "file": "autoMountTokenEnabled.rego", + "template_args": { + "name": "autoMountTokenEnabled", + "prefix": "", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Ensure that Service Account Tokens are only mounted where necessary", + "reference_id": "AC-K8-IA-PO-M-0105", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json new file mode 100755 index 000000000..d7befdd86 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json @@ -0,0 +1,14 @@ +{ + "name": "appArmorProfile", + "file": "appArmorProfile.rego", + "template_args": { + "name": "appArmorProfile", + "prefix": "", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "AppArmor profile not set to default or custom profile will make the container vulnerable to kernel level threats", + "reference_id": "AC-K8-IA-PO-M-0135", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json new file mode 100755 index 000000000..5a22d3f4e --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json @@ -0,0 +1,14 @@ +{ + "name": "allowedProcMount", + "file": "allowedProcMount.rego", + "template_args": { + "name": "allowedProcMount", + "prefix": "", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Unmasking the procMount will allow more information than is necessary to the program running in the containers spawned by k8s", + "reference_id": "AC-K8-IA-PO-M-0139", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json new file mode 100755 index 000000000..10fad68ae --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json @@ -0,0 +1,21 @@ +{ + "name": "readOnlyFileSystem", + "file": "securityContextCheck.rego", + "template_args": { + "allowed": "false", + "arg1": "limits", + "arg2": "cpu", + "name": "readOnlyFileSystem", + "not_allowed": "true", + "param": "readOnlyRootFilesystem", + "param1": "securityContext", + "prefix": "", + "suffix": "", + "value": "false" + }, + "severity": "MEDIUM", + "description": "Container images with readOnlyRootFileSystem set as false mounts the container root file system with write permissions", + "reference_id": "AC-K8-IA-PO-M-0140", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json new file mode 100755 index 000000000..5293c73bf --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json @@ -0,0 +1,14 @@ +{ + "name": "secCompProfile", + "file": "secCompProfile.rego", + "template_args": { + "name": "secCompProfile", + "prefix": "", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Default seccomp profile not enabled will make the container to make non-essential system calls", + "reference_id": "AC-K8-IA-PO-M-0141", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json new file mode 100755 index 000000000..07843f8bf --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json @@ -0,0 +1,22 @@ +{ + "name": "allowedVolumes", + "file": "allowedVolumes.rego", + "template_args": { + "name": "allowedVolumes", + "prefix": "", + "secure_volumes": [ + "configMap", + "emptyDir", + "projected", + "secret", + "downwardAPI", + "persistentVolumeClaim" + ], + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Some volume types mount the host file system paths to the pod or container, thus increasing the chance of escaping the container to access the host", + "reference_id": "AC-K8-IA-PO-M-0143", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json new file mode 100755 index 000000000..a98195db9 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json @@ -0,0 +1,16 @@ +{ + "name": "falseHostPID", + "file": "specBoolCheck.rego", + "template_args": { + "name": "falseHostPID", + "param": "hostPID", + "prefix": "", + "suffix": "", + "value": "true" + }, + "severity": "MEDIUM", + "description": "Containers Should Not Share Host Process ID Namespace", + "reference_id": "AC-K8-IA-PO-M-0162", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json new file mode 100755 index 000000000..11f59e9a7 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json @@ -0,0 +1,15 @@ +{ + "name": "netRawCapabilityUsed", + "file": "capabilityUsed.rego", + "template_args": { + "attribute": "requiredDropCapabilities", + "name": "netRawCapabilityUsed", + "prefix": "", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Minimize the admission of containers with the NET_RAW capability", + "reference_id": "AC-K8-IA-PS-M-0112", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json new file mode 100755 index 000000000..23c8d904d --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json @@ -0,0 +1,14 @@ +{ + "name": "secretsAsEnvVariables", + "file": "secretsAsEnvVariables.rego", + "template_args": { + "name": "secretsAsEnvVariables", + "prefix": "", + "suffix": "" + }, + "severity": "HIGH", + "description": "Prefer using secrets as files over secrets as environment variables", + "reference_id": "AC-K8-NS-PO-H-0117", + "category": "Network Security", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json new file mode 100755 index 000000000..b211361a6 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json @@ -0,0 +1,14 @@ +{ + "name": "capSysAdminUsed", + "file": "capSysAdminUsed.rego", + "template_args": { + "name": "capSysAdminUsed", + "prefix": "", + "suffix": "" + }, + "severity": "HIGH", + "description": "Do Not Use CAP_SYS_ADMIN Linux Capability", + "reference_id": "AC-K8-NS-PO-H-0170", + "category": "Network Security", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json new file mode 100755 index 000000000..43ba2432f --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json @@ -0,0 +1,14 @@ +{ + "name": "securityContextUsed", + "file": "securityContextUsed.rego", + "template_args": { + "name": "securityContextUsed", + "prefix": "", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Apply Security Context to Your Pods and Containers", + "reference_id": "AC-K8-NS-PO-M-0122", + "category": "Network Security", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json new file mode 100755 index 000000000..804a12ed4 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json @@ -0,0 +1,14 @@ +{ + "name": "imageWithoutDigest", + "file": "imageWithoutDigest.rego", + "template_args": { + "name": "imageWithoutDigest", + "prefix": "", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Image without digest affects the integrity principle of image security", + "reference_id": "AC-K8-NS-PO-M-0133", + "category": "Network Security", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json new file mode 100755 index 000000000..e96b364da --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json @@ -0,0 +1,16 @@ +{ + "name": "falseHostIPC", + "file": "specBoolCheck.rego", + "template_args": { + "name": "falseHostIPC", + "param": "hostIPC", + "prefix": "", + "suffix": "", + "value": "true" + }, + "severity": "MEDIUM", + "description": "Containers Should Not Share Host IPC Namespace", + "reference_id": "AC-K8-NS-PO-M-0163", + "category": "Network Security", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json new file mode 100755 index 000000000..5c893ce0b --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json @@ -0,0 +1,16 @@ +{ + "name": "falseHostNetwork", + "file": "specBoolCheck.rego", + "template_args": { + "name": "falseHostNetwork", + "param": "hostNetwork", + "prefix": "", + "suffix": "", + "value": "true" + }, + "severity": "MEDIUM", + "description": "Containers Should Not Share the Host Network Namespace", + "reference_id": "AC-K8-NS-PO-M-0164", + "category": "Network Security", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json new file mode 100755 index 000000000..df493d82c --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json @@ -0,0 +1,17 @@ +{ + "name": "dontConnectDockerSock", + "file": "dockerSockCheck.rego", + "template_args": { + "attrib": "spec.volumes[_].hostPath", + "name": "dontConnectDockerSock", + "param": "path", + "prefix": "", + "suffix": "", + "value": "/var/run/docker" + }, + "severity": "MEDIUM", + "description": "Restrict Mounting Docker Socket in a Container", + "reference_id": "AC-K8-NS-PO-M-0171", + "category": "Network Security", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json new file mode 100755 index 000000000..224310674 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json @@ -0,0 +1,14 @@ +{ + "name": "containersAsHighUID", + "file": "containersAsHighUID.rego", + "template_args": { + "name": "containersAsHighUID", + "prefix": "", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Containers Should Run as a High UID to Avoid Host Conflict", + "reference_id": "AC-K8-NS-PO-M-0182", + "category": "Network Security", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json new file mode 100755 index 000000000..6340d311e --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json @@ -0,0 +1,19 @@ +{ + "name": "alwaysPullImages", + "file": "commandCheck.rego", + "template_args": { + "argument": "--enable-admission-plugins", + "name": "alwaysPullImages", + "negation": "", + "optional": "", + "param": "AlwaysPullImages", + "prefix": "", + "presence": "not", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "AlwaysPullImages plugin is not set", + "reference_id": "AC-K8-OE-PK-M-0034", + "category": "Operational Efficiency", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json new file mode 100755 index 000000000..aebef8612 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json @@ -0,0 +1,21 @@ +{ + "name": "CpuRequestsCheck", + "file": "securityContextCheck.rego", + "template_args": { + "allowed": "true", + "arg1": "requests", + "arg2": "cpu", + "name": "CpuRequestsCheck", + "not_allowed": "false", + "param": "resources", + "param1": "resources", + "prefix": "", + "suffix": "", + "value": "false" + }, + "severity": "Medium", + "description": "CPU Request Not Set in config file.", + "reference_id": "AC-K8-OE-PK-M-0155", + "category": "Operational Efficiency", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json new file mode 100755 index 000000000..c74835c6e --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json @@ -0,0 +1,21 @@ +{ + "name": "CpulimitsCheck", + "file": "securityContextCheck.rego", + "template_args": { + "allowed": "true", + "arg1": "limits", + "arg2": "cpu", + "name": "CpulimitsCheck", + "not_allowed": "false", + "param": "limits", + "param1": "resources", + "prefix": "", + "suffix": "", + "value": "false" + }, + "severity": "Medium", + "description": "CPU Limits Not Set in config file.", + "reference_id": "AC-K8-OE-PK-M-0156", + "category": "Operational Efficiency", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json new file mode 100755 index 000000000..691b58895 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json @@ -0,0 +1,21 @@ +{ + "name": "MemoryRequestsCheck", + "file": "securityContextCheck.rego", + "template_args": { + "allowed": "true", + "arg1": "requests", + "arg2": "memory", + "name": "MemoryRequestsCheck", + "not_allowed": "false", + "param": "resources", + "param1": "resources", + "prefix": "", + "suffix": "", + "value": "false" + }, + "severity": "Medium", + "description": "Memory Request Not Set in config file.", + "reference_id": "AC-K8-OE-PK-M-0157", + "category": "Operational Efficiency", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json new file mode 100755 index 000000000..7ab678c76 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json @@ -0,0 +1,21 @@ +{ + "name": "MemorylimitsCheck", + "file": "securityContextCheck.rego", + "template_args": { + "allowed": "true", + "arg1": "limits", + "arg2": "memory", + "name": "MemorylimitsCheck", + "not_allowed": "false", + "param": "limits", + "param1": "resources", + "prefix": "", + "suffix": "", + "value": "false" + }, + "severity": "Medium", + "description": "Memory Limits Not Set in config file.", + "reference_id": "AC-K8-OE-PK-M-0158", + "category": "Operational Efficiency", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json new file mode 100755 index 000000000..9ce09380f --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json @@ -0,0 +1,16 @@ +{ + "name": "nolivenessProbe", + "file": "probeCheck.rego", + "template_args": { + "argument": "livenessProbe", + "argumentTF": "liveness_probe", + "name": "nolivenessProbe", + "prefix": "", + "suffix": "" + }, + "severity": "LOW", + "description": "No liveness probe will ensure there is no recovery in case of unexpected errors", + "reference_id": "AC-K8-OE-PO-L-0129", + "category": "Operational Efficiency", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json new file mode 100755 index 000000000..a0e4058fd --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json @@ -0,0 +1,16 @@ +{ + "name": "noReadinessProbe", + "file": "probeCheck.rego", + "template_args": { + "argument": "readinessProbe", + "argumentTF": "readiness_probe", + "name": "noReadinessProbe", + "prefix": "", + "suffix": "" + }, + "severity": "LOW", + "description": "No readiness probe will affect automatic recovery in case of unexpected errors", + "reference_id": "AC-K8-OE-PO-L-0130", + "category": "Operational Efficiency", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json new file mode 100755 index 000000000..83eec4e4d --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json @@ -0,0 +1,14 @@ +{ + "name": "imageWithLatestTag", + "file": "imageWithLatestTag.rego", + "template_args": { + "name": "imageWithLatestTag", + "prefix": "", + "suffix": "" + }, + "severity": "LOW", + "description": "No tag or container image with :Latest tag makes difficult to rollback and track", + "reference_id": "AC-K8-OE-PO-L-0134", + "category": "Operational Efficiency", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json new file mode 100755 index 000000000..6e0c8fd97 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json @@ -0,0 +1,14 @@ +{ + "name": "otherNamespace", + "file": "otherNamespace.rego", + "template_args": { + "name": "otherNamespace", + "prefix": "", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Default Namespace Should Not be Used", + "reference_id": "AC-K8-OE-PO-M-0166", + "category": "Operational Efficiency", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego new file mode 100755 index 000000000..e7d8463e1 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego @@ -0,0 +1,107 @@ +### this policy depends on the parameters specified by the user/client. Here we are considering that no hostPath are allowed### +package accurics + +#rule for pod +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + vols := pod.config.spec.volumes[_] + parameters := {} + has_field(vols, "hostPath") + allowedPaths := get_allowed_paths(parameters) + input_hostpath_violation(allowedPaths, vols) +} + +#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + vols := kind.config.spec.template.spec.volumes[_] + #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } + parameters := {} + has_field(vols, "hostPath") + allowedPaths := get_allowed_paths(parameters) + input_hostpath_violation(allowedPaths, vols) +} + +#rule for cron_job +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + vols := cron_job.config.spec.jobTemplate.spec.template.spec.volumes[_] + #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } + parameters := {} + has_field(vols, "hostPath") + allowedPaths := get_allowed_paths(parameters) + input_hostpath_violation(allowedPaths, vols) +} + +#function for all KINDs +has_field(object, field) = true { + object[field] +} + +#now allowed paths are null, this function will run## +get_allowed_paths(params) = out { + not params.allowedHostPath == "undefined" + out = [] +} + +input_hostpath_violation(allowedPaths, volume) { + allowedPaths == [] +} + +### below functions are for violation when user has specified the hostPath, for testing uncomment the parameter array of objects at top#### + +get_allowed_paths(params) = out { + out = params.allowedHostPath +} + +input_hostpath_violation(allowedPaths, volume) { + not input_hostpath_allowed(allowedPaths, volume) +} + +input_hostpath_allowed(allowedPaths, volume) { + allowedHostPath := allowedPaths[_] + path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) + not allowedHostPath.readOnly == true +} + +input_hostpath_allowed(allowedPaths, volume) { + allowedHostPath := allowedPaths[_] + path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) + allowedHostPath.readOnly + not writeable_input_volume_mounts(volume.name) +} + +writeable_input_volume_mounts(volume_name) { + containers := input.kubernetes_pod[_].config.spec.containers[_] + mount := containers.volumeMounts[_] + mount.name == volume_name + not mount.readOnly +} + +path_matches(prefix, path) { + a := split(trim(prefix, "/"), "/") + b := split(trim(path, "/"), "/") + prefix_matches(a, b) +} + +prefix_matches(a, b) { + count(a) <= count(b) + not any_not_equal_upto(a, b, count(a)) +} + +any_not_equal_upto(a, b, n) { + a[i] != b[i] + i < n +} diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego new file mode 100755 index 000000000..d8d6e62cc --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego @@ -0,0 +1,126 @@ +package accurics + +#rule for pod_security_policy +{{.prefix}}{{.name}}{{.suffix}}[psp.id] { + psp := input.kubernetes_pod_security_policy[_] + psp.config.spec.allowProcMountTypes != "Default" +} + +#rule for pod_security_policy terraform +{{.prefix}}{{.name}}{{.suffix}}[psp.id] { + psp := input.kubernetes_pod_security_policy[_] + psp.config.spec.allow_proc_mount_types != "Default" +} + +#rule for pod +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + parameters := {} + container := pod.config.spec.containers[_] + container.securityContext.procMount + allowedProcMount := get_allowed_proc_mount(parameters) + not input_proc_mount_type_allowed(allowedProcMount, container) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + parameters := {} + container := pod.config.spec.initContainers[_] + container.securityContext.procMount + allowedProcMount := get_allowed_proc_mount(parameters) + not input_proc_mount_type_allowed(allowedProcMount, container) +} + +#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.containers[_] + #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } + parameters := {} + container.securityContext.procMount + allowedProcMount := get_allowed_proc_mount(parameters) + not input_proc_mount_type_allowed(allowedProcMount, container) +} + +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.initContainers[_] + #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } + parameters := {} + container.securityContext.procMount + allowedProcMount := get_allowed_proc_mount(parameters) + not input_proc_mount_type_allowed(allowedProcMount, container) +} + +#rule for cron_job +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] + #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } + parameters := {} + container.securityContext.procMount + allowedProcMount := get_allowed_proc_mount(parameters) + not input_proc_mount_type_allowed(allowedProcMount, container) +} + +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] + #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } + parameters := {} + container.securityContext.procMount + allowedProcMount := get_allowed_proc_mount(parameters) + not input_proc_mount_type_allowed(allowedProcMount, container) +} + +###this will get satisfied as no parameters are provided, thus checking with the baseline configuration which is checking that the procmount is default#### +get_allowed_proc_mount(params) = out { + not params.procMount + out = "default" +} + +get_allowed_proc_mount(params) = out { + not valid_proc_mount(params.procMount) + out = "default" +} + +get_allowed_proc_mount(params) = out { + out = lower(params.procMount) +} + +valid_proc_mount(str) { + lower(str) == "default" +} + +valid_proc_mount(str) { + lower(str) == "unmasked" +} + +input_proc_mount_type_allowed(allowedProcMount, c) { + allowedProcMount == "default" + lower(c.securityContext.procMount) == "default" +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego new file mode 100755 index 000000000..cef0f216d --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego @@ -0,0 +1,58 @@ +package accurics + +####fixed the minimum set of allowed volumes, this may change as per the user#### + +#rule for pod_security_policy +{{.prefix}}{{.name}}{{.suffix}}[psp.id] { + psp := input.kubernetes_pod_security_policy[_] + secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] + volume_field := psp.config.spec.volumes[_] + not input_volume_type_allowed(volume_field, secure_volumes) +} + +#rule for pod +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] + volume_fields := {x | pod.config.spec.volumes[_][x]; x != "name"} + field := volume_fields[_] + not input_volume_type_allowed(field, secure_volumes) +} + +#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] + volume_fields := {x | kind.config.spec.template.spec.volumes[_][x]; x != "name"} + field := volume_fields[_] + not input_volume_type_allowed(field, secure_volumes) +} + +#rule for cron_job +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] + volume_fields := {x | cron_job.config.spec.jobTemplate.spec.template.spec.volumes[_][x]; x != "name"} + field := volume_fields[_] + not input_volume_type_allowed(field, secure_volumes) +} + +input_volume_type_allowed(field, secure_volumes) { + secure_volumes[_] == "*" +} + +input_volume_type_allowed(field, secure_volumes) { + field == secure_volumes[_] +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json new file mode 100755 index 000000000..3a5ba1b5b --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": false, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_cron_job", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.73", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json new file mode 100755 index 000000000..860d3a3a0 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": false, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_daemonset", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.74", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json new file mode 100755 index 000000000..a873dc584 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": false, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_deployment", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.75", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json new file mode 100755 index 000000000..497e12df1 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": false, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_job", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.76", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json new file mode 100755 index 000000000..72f9f6971 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": false, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_pod", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.77", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json new file mode 100755 index 000000000..2ec1d9e1f --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": false, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_replicaset", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.78", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json new file mode 100755 index 000000000..0becff4f8 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": false, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_replication_controller", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.79", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json new file mode 100755 index 000000000..6c54f996b --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": false, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_stateful_set", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.80", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json new file mode 100755 index 000000000..8e8aebda8 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": true, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_cron_job", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.81", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json new file mode 100755 index 000000000..9d06ffb02 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": true, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_daemonset", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.82", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json new file mode 100755 index 000000000..0259e0256 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": true, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_deployment", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.83", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json new file mode 100755 index 000000000..646842e5b --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": true, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_job", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.84", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json new file mode 100755 index 000000000..59f98d1aa --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": true, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_pod", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.85", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json new file mode 100755 index 000000000..32ae92f81 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": true, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_replicaset", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.86", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json new file mode 100755 index 000000000..b82fb1132 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": true, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_replication_controller", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.87", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json new file mode 100755 index 000000000..33f7d06b3 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json @@ -0,0 +1,16 @@ +{ + "name": "containerHasAllowedCapabilities", + "file": "containerHasAllowedCapabilities.rego", + "template_args": { + "is_init": true, + "name": "containerHasAllowedCapabilities", + "prefix": "", + "resource_type": "kubernetes_stateful_set", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Pod has extra capabilities allowed", + "reference_id": "accurics.kubernetes.IAM.88", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego new file mode 100644 index 000000000..bfe42d7e7 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego @@ -0,0 +1,119 @@ +package accurics + +# Checks if any extra capabilities are added +{{.prefix}}{{.name}}{{.suffix}}[api.id] { + {{- template "spec" . }} + count(spec.allowedCapabilities) > 0 +} + +# Note, no TF-equivalent + +{{- if eq .is_init true}} + +{{.prefix}}{{.name}}{{.suffix}}[api.id] { + {{- template "initContainersSecurityContext" .}} + count(initContainersSecurityContext.capabilities.add) > 0 +} + +{{.prefix}}{{.name}}{{.suffix}}[api.id] { + {{- template "initContainersSecurityContextTF" .}} + count(initContainersSecurityContextTF.capabilities.add) > 0 +} + +{{- else }} + +{{.prefix}}{{.name}}{{.suffix}}[api.id] { + {{- template "containersSecurityContext" .}} + count(containersSecurityContext.capabilities.add) > 0 +} + +{{.prefix}}{{.name}}{{.suffix}}[api.id] { + {{- template "containersSecurityContextTF" .}} + count(containersSecurityContextTF.capabilities.add) > 0 +} + +{{- end }} + + +################################## +### Template definitions below ### +################################## +{{- define "api" }} + api = input.{{.resource_type}}[_] +{{- end}} + +# resolves path to the spec key +{{- define "spec" }} + {{- template "api" . }} + {{- if eq .resource_type "kubernetes_pod" }} + spec = api.config.spec + {{- else if eq .resource_type "kubernetes_pod_security_policy" }} + spec = api.config.spec + {{- else if eq .resource_type "kubernetes_cron_job" }} + spec = api.config.spec.jobTemplate.spec.template.spec + {{- else }} + spec = api.config.spec.template.spec + {{- end }} +{{- end }} + +# resolves path to the spec key for terraform-defined k8s resources +{{- define "specTF" }} + {{- template "api" . }} + {{- if eq .resource_type "kubernetes_pod" }} + specTF = api.config.spec + {{- else if eq .resource_type "kubernetes_pod_security_policy" }} + specTF = api.config.spec + {{- else if eq .resource_type "kubernetes_cron_job" }} + specTF = api.config.spec.job_template.spec.template.spec + {{- else }} + specTF = api.config.spec.template.spec + {{- end }} +{{- end }} + +# resolves path to the containers list +{{- define "containers" }} + {{- template "spec" . }} + containers = spec.containers[_] +{{- end }} + +# resolves path to the containers' security context +{{- define "containersSecurityContext" }} + {{- template "containers" . }} + containersSecurityContext = containers.securityContext +{{- end }} + +# resolves path to the containers list for terraform-defined k8s resources +{{- define "containersTF" }} + {{- template "specTF" . }} + containersTF = specTF.containers[_] +{{- end }} + +# resolves path to the containers' security context for terraform-defined k8s resources +{{- define "containersSecurityContextTF" }} + {{- template "containersTF" . }} + containersSecurityContextTF = containersTF.security_context +{{- end }} + +# resolves path to the initContainers list +{{- define "initContainers" }} + {{- template "spec" . }} + initContainers = spec.initContainers[_] +{{- end }} + +# resolves path to the initContainers' security context +{{- define "initContainersSecurityContext" }} + {{- template "initContainers" . }} + initContainersSecurityContext = initContainers.securityContext +{{- end }} + +# resolves path to the initContainers list for terraform-defined k8s resources +{{- define "initContainersTF" }} + {{- template "specTF" . }} + initContainersTF = specTF.init_containers[_] +{{- end }} + +# resolves path to the initContainers' security context for terraform-defined k8s resources +{{- define "initContainersSecurityContextTF" }} + {{- template "initContainersTF" . }} + initContainersSecurityContextTF = initContainersTF.security_context +{{- end }} diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego new file mode 100755 index 000000000..7be8687f3 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego @@ -0,0 +1,108 @@ +package accurics + +#rule for pod security policy, will be valid for terraform pod_security_policy +{{.prefix}}{{.name}}{{.suffix}}[psp.id] { + psp := input.kubernetes_pod_security_policy[_] + psp.config.metadata.annotations["apparmor.security.beta.kubernetes.io/defaultProfileName"] != "runtime/default" +} + +#rule for pod, covers containers +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.containers[_] + not input_apparmor_allowed(container.name, pod.config.metadata) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.initContainers[_] + not input_apparmor_allowed(container.name, pod.config.metadata) +} + +#terraform init_containers +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.init_containers[_] + not input_apparmor_allowed(container.name, pod.config.metadata) +} + +##rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.containers[_] + not input_apparmor_allowed(container.name, kind.config.spec.template.metadata) +} + +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.initContainers[_] + not input_apparmor_allowed(container.name, kind.config.spec.template.metadata) +} + +#terraform init_containers +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.init_containers[_] + not input_apparmor_allowed(container.name, kind.config.spec.template.metadata) +} + +#rule for cron_job, covers containers +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] + not input_apparmor_allowed(container.name, cron_job.config.spec.jobTemplate.spec.template.metadata) +} + +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] + not input_apparmor_allowed(container.name, cron_job.config.spec.jobTemplate.spec.template.metadata) +} + +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] + not input_apparmor_allowed(container.name, cron_job.config.spec.jobTemplate.spec.template.metadata) +} + +#function for all Kinds +input_apparmor_allowed(containerName, metadata) { + metadata.annotations[key] == "runtime/default" + key == sprintf("container.apparmor.security.beta.kubernetes.io/%v", [containerName]) +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego new file mode 100755 index 000000000..248878b60 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego @@ -0,0 +1,33 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + pod.config.spec.automountServiceAccountToken == true +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_service_account[_] + pod.config.automountServiceAccountToken == true +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + pod.config.spec.template.spec.automountServiceAccountToken == true +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + pod.config.spec.jobTemplate.spec.template.spec.automountServiceAccountToken == true +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego new file mode 100755 index 000000000..d8d1ae40a --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego @@ -0,0 +1,69 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.containers[_] + container.securityContext.capabilities.add == "-SYS_ADMIN" +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + initcontainer := pod.config.spec.initContainers[_] + initcontainer.securityContext.capabilities.add == "-SYS_ADMIN" +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] + container.securityContext.capabilities.add == "-SYS_ADMIN" +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] + initcontainer.securityContext.capabilities.add == "-SYS_ADMIN" +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + containerCheck(pod.config.spec.template.spec) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + initContainerCheck(pod.config.spec.template.spec) +} + +initContainerCheck(spec) { + container := spec.initContainers[_] + container.securityContext.capabilities.add == "-SYS_ADMIN" +} + +containerCheck(spec) { + container := spec.containers[_] + container.securityContext.capabilities.add == "-SYS_ADMIN" +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego new file mode 100755 index 000000000..9920b3851 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego @@ -0,0 +1,74 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod_security_policy[_] + pod.config.spec.{{.attribute}} != [] +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.containers[_] + container.{{.attribute}} != [] +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + initcontainer := pod.config.spec.initContainers[_] + initcontainer.{{.attribute}} != [] +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] + container.{{.attribute}} != [] +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] + initcontainer.{{.attribute}} != [] +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + containerCheck(pod.config.spec.template.spec) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + initContainerCheck(pod.config.spec.template.spec) +} + +initContainerCheck(spec) { + container := spec.initContainers[_] + container.{{.attribute}} != [] +} + +containerCheck(spec) { + container := spec.containers[_] + container.{{.attribute}} != [] +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego new file mode 100755 index 000000000..07c315aed --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego @@ -0,0 +1,19 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[pod_kubeapi.id] { + pod_kubeapi := input.kubernetes_pod[_] + cmds := pod_kubeapi.config.spec.containers[_].command + {{.negation}} check(cmds) +} + +check(cmds) { + cmd := cmds[_] + startswith(cmd, "{{.argument}}") + {{.presence}} contains(cmd, "{{.param}}") + {{.optional}} +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + cmds := pod.config.spec.containers[_].imagePullPolicy != "Always" +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json new file mode 100755 index 000000000..56545ab51 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": false, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_cron_job", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.105", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json new file mode 100755 index 000000000..4d978019c --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": false, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_daemonset", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.106", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json new file mode 100755 index 000000000..ce27d4bb3 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": false, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_job", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.108", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json new file mode 100755 index 000000000..b740200ba --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": false, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_pod", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.109", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json new file mode 100755 index 000000000..19489a08a --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": false, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_replicaset", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.110", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json new file mode 100755 index 000000000..d4a8f5515 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": false, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_replication_controller", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.111", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json new file mode 100755 index 000000000..a18dc1c63 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": false, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_stateful_set", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.112", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json new file mode 100755 index 000000000..d1cdd0952 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": true, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_cron_job", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.113", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json new file mode 100755 index 000000000..64b9de334 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": true, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_daemonset", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.114", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json new file mode 100755 index 000000000..2f573f89b --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": true, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_deployment", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.115", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json new file mode 100755 index 000000000..0f80098b5 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": true, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_job", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.116", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json new file mode 100755 index 000000000..9e420b8e7 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": true, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_pod", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.117", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json new file mode 100755 index 000000000..17d411640 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": true, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_replicaset", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.118", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json new file mode 100755 index 000000000..076a4927b --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": true, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_replication_controller", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.119", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json new file mode 100755 index 000000000..e5c922b56 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json @@ -0,0 +1,16 @@ +{ + "name": "containerResourcesNotDefined", + "file": "containerResourcesNotDefined.rego", + "template_args": { + "is_init": true, + "name": "containerResourcesNotDefined", + "prefix": "", + "resource_type": "kubernetes_stateful_set", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Container does not have resource limitations defined", + "reference_id": "accurics.kubernetes.IAM.120", + "category": "Identity and Access Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego new file mode 100644 index 000000000..b0730347b --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego @@ -0,0 +1,111 @@ +package accurics + +{{- if eq .is_init true}} + +{{.prefix}}{{.name}}{{.suffix}}[api.id] { + {{- template "initContainers" . }} + not initContainers.resources +} + +{{.prefix}}{{.name}}{{.suffix}}[api.id] { + {{- template "initContainersTF" . }} + not initContainersTF.resources +} + +{{- else }} + +{{.prefix}}{{.name}}{{.suffix}}[api.id] { + {{- template "containers" . }} + not containers.resources +} + +{{.prefix}}{{.name}}{{.suffix}}[api.id] { + {{- template "containersTF" . }} + not containersTF.resources +} + +{{- end }} + + +################################## +### Template definitions below ### +################################## +{{- define "api" }} + api = input.{{.resource_type}}[_] +{{- end}} + +# resolves path to the spec key +{{- define "spec" }} + {{- template "api" . }} + {{- if eq .resource_type "kubernetes_pod" }} + spec = api.config.spec + {{- else if eq .resource_type "kubernetes_pod_security_policy" }} + spec = api.config.spec + {{- else if eq .resource_type "kubernetes_cron_job" }} + spec = api.config.spec.jobTemplate.spec.template.spec + {{- else }} + spec = api.config.spec.template.spec + {{- end }} +{{- end }} + +# resolves path to the spec key for terraform-defined k8s resources +{{- define "specTF" }} + {{- template "api" . }} + {{- if eq .resource_type "kubernetes_pod" }} + specTF = api.config.spec + {{- else if eq .resource_type "kubernetes_pod_security_policy" }} + specTF = api.config.spec + {{- else if eq .resource_type "kubernetes_cron_job" }} + specTF = api.config.spec.job_template.spec.template.spec + {{- else }} + specTF = api.config.spec.template.spec + {{- end }} +{{- end }} + +# resolves path to the containers list +{{- define "containers" }} + {{- template "spec" . }} + containers = spec.containers[_] +{{- end }} + +# resolves path to the containers' security context +{{- define "containersSecurityContext" }} + {{- template "containers" . }} + containersSecurityContext = containers.securityContext +{{- end }} + +# resolves path to the containers list for terraform-defined k8s resources +{{- define "containersTF" }} + {{- template "specTF" . }} + containersTF = specTF.containers[_] +{{- end }} + +# resolves path to the containers' security context for terraform-defined k8s resources +{{- define "containersSecurityContextTF" }} + {{- template "containersTF" . }} + containersSecurityContextTF = containersTF.security_context +{{- end }} + +# resolves path to the initContainers list +{{- define "initContainers" }} + {{- template "spec" . }} + initContainers = spec.initContainers[_] +{{- end }} + +# resolves path to the initContainers' security context +{{- define "initContainersSecurityContext" }} + {{- template "initContainers" . }} + initContainersSecurityContext = initContainers.securityContext +{{- end }} + +# resolves path to the initContainers list for terraform-defined k8s resources +{{- define "initContainersTF" }} + {{- template "specTF" . }} + initContainersTF = specTF.init_containers[_] +{{- end }} + +# resolves path to the initContainers' security context for terraform-defined k8s resources +{{- define "initContainersSecurityContextTF" }} + {{- template "initContainersTF" . }} + initContainersSecurityContextTF = initContainersTF.security_context +{{- end }} diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json new file mode 100755 index 000000000..7962ef773 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": false, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_cron_job", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.57", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json new file mode 100755 index 000000000..f8f70eaf8 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": false, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_daemonset", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.58", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json new file mode 100755 index 000000000..739c621d5 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": false, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_deployment", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.59", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json new file mode 100755 index 000000000..f8a42a84a --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": false, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_job", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.60", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json new file mode 100755 index 000000000..fc7091e05 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": false, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_pod", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.61", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json new file mode 100755 index 000000000..3fc461ffb --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": false, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_replicaset", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.62", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json new file mode 100755 index 000000000..59662b349 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": false, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_replication_controller", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.63", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json new file mode 100755 index 000000000..13cb5dc87 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": false, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_stateful_set", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.64", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json new file mode 100755 index 000000000..00ec55c5a --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": true, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_cron_job", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.65", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json new file mode 100755 index 000000000..0f4a3d56f --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": true, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_daemonset", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.66", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json new file mode 100755 index 000000000..42cd86828 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": true, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_deployment", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.67", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json new file mode 100755 index 000000000..d023923ea --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": true, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_job", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.68", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json new file mode 100755 index 000000000..c522de8fb --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": true, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_pod", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.69", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json new file mode 100755 index 000000000..45c9cb9d5 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": true, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_replicaset", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.70", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json new file mode 100755 index 000000000..4bbbdd267 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": true, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_replication_controller", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.71", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json new file mode 100755 index 000000000..88fc00e56 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json @@ -0,0 +1,16 @@ +{ + "name": "containerUsesSecretsInEnvironmentVar", + "file": "containerUsesSecretsInEnvironmentVar.rego", + "template_args": { + "is_init": true, + "name": "containerUsesSecretsInEnvironmentVar", + "prefix": "", + "resource_type": "kubernetes_stateful_set", + "suffix": "" + }, + "severity": "HIGH", + "description": "Container uses secrets in environment variables", + "reference_id": "accurics.kubernetes.EKM.72", + "category": "Encryption and Key Management", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego new file mode 100644 index 000000000..86c10e36e --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego @@ -0,0 +1,115 @@ +package accurics + +{{- if eq .is_init true}} + +{{.prefix}}{{.name}}{{.suffix}}[api.id] { + {{- template "initContainers" .}} + envVars := initContainers.env[_] + envVars.valueFrom.secretKeyRef +} + +{{.prefix}}{{.name}}{{.suffix}}[api.id] { + {{- template "initContainersTF" .}} + envVars := initContainersTF.env[_] + envVars.valueFrom.secretKeyRef +} + +{{- else }} + +{{.prefix}}{{.name}}{{.suffix}}[api.id] { + {{- template "containers" .}} + envVars := containers.env[_] + envVars.valueFrom.secretKeyRef +} + +{{.prefix}}{{.name}}{{.suffix}}[api.id] { + {{- template "containersTF" .}} + envVars := containersTF.env[_] + envVars.valueFrom.secretKeyRef +} + +{{- end }} + + +################################## +### Template definitions below ### +################################## +{{- define "api" }} + api = input.{{.resource_type}}[_] +{{- end}} + +# resolves path to the spec key +{{- define "spec" }} + {{- template "api" . }} + {{- if eq .resource_type "kubernetes_pod" }} + spec = api.config.spec + {{- else if eq .resource_type "kubernetes_pod_security_policy" }} + spec = api.config.spec + {{- else if eq .resource_type "kubernetes_cron_job" }} + spec = api.config.spec.jobTemplate.spec.template.spec + {{- else }} + spec = api.config.spec.template.spec + {{- end }} +{{- end }} + +# resolves path to the spec key for terraform-defined k8s resources +{{- define "specTF" }} + {{- template "api" . }} + {{- if eq .resource_type "kubernetes_pod" }} + specTF = api.config.spec + {{- else if eq .resource_type "kubernetes_pod_security_policy" }} + specTF = api.config.spec + {{- else if eq .resource_type "kubernetes_cron_job" }} + specTF = api.config.spec.job_template.spec.template.spec + {{- else }} + specTF = api.config.spec.template.spec + {{- end }} +{{- end }} + +# resolves path to the containers list +{{- define "containers" }} + {{- template "spec" . }} + containers = spec.containers[_] +{{- end }} + +# resolves path to the containers' security context +{{- define "containersSecurityContext" }} + {{- template "containers" . }} + containersSecurityContext = containers.securityContext +{{- end }} + +# resolves path to the containers list for terraform-defined k8s resources +{{- define "containersTF" }} + {{- template "specTF" . }} + containersTF = specTF.containers[_] +{{- end }} + +# resolves path to the containers' security context for terraform-defined k8s resources +{{- define "containersSecurityContextTF" }} + {{- template "containersTF" . }} + containersSecurityContextTF = containersTF.security_context +{{- end }} + +# resolves path to the initContainers list +{{- define "initContainers" }} + {{- template "spec" . }} + initContainers = spec.initContainers[_] +{{- end }} + +# resolves path to the initContainers' security context +{{- define "initContainersSecurityContext" }} + {{- template "initContainers" . }} + initContainersSecurityContext = initContainers.securityContext +{{- end }} + +# resolves path to the initContainers list for terraform-defined k8s resources +{{- define "initContainersTF" }} + {{- template "specTF" . }} + initContainersTF = specTF.init_containers[_] +{{- end }} + +# resolves path to the initContainers' security context for terraform-defined k8s resources +{{- define "initContainersSecurityContextTF" }} + {{- template "initContainersTF" . }} + initContainersSecurityContextTF = initContainersTF.security_context +{{- end }} diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego new file mode 100755 index 000000000..6d6e8e677 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego @@ -0,0 +1,102 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + pod.config.spec.securityContext.runAsUser < 1000 +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.containers[_] + container.securityContext.runAsUser < 1000 +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + initcontainer := pod.config.spec.initContainers[_] + initcontainer.securityContext.runAsUser < 1000 +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + pod.config.spec.jobTemplate.spec.template.spec.securityContext.runAsUser < 1000 +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] + container.securityContext.runAsUser < 1000 +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] + initcontainer.securityContext.runAsUser < 1000 +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod_security_policy[_] + ranges := pod.config.spec.runAsUser.ranges[_] + ranges.min < 1000 +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + pod.config.spec.template.spec.securityContext.runAsUser < 1000 +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + checkContainer(pod.config.spec.template.spec) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + checkInitContainer(pod.config.spec.template.spec) +} + +checkInitContainer(spec) { + containers := spec.initContainers[_] + containers.securityContext.runAsUser < 1000 +} + +checkContainer(spec) { + containers := spec.containers[_] + containers.securityContext.runAsUser < 1000 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego new file mode 100755 index 000000000..f918cc215 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego @@ -0,0 +1,51 @@ +### this pollicy depends on the parameters specified by the user/client. Here we are considering that no kernel level syscalls are allowed### +package accurics + +#rule for pod +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + forbiddenSysctls = ["kernel.*"] + sysctl := pod.config.spec.securityContext.sysctls[_].name + forbidden_sysctl(sysctl, forbiddenSysctls) +} + +##rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + forbiddenSysctls = ["kernel.*"] + sysctl := kind.config.spec.template.spec.securityContext.sysctls[_].name + forbidden_sysctl(sysctl, forbiddenSysctls) +} + +#rule for cron_job +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + forbiddenSysctls = ["kernel.*"] + sysctl := cron_job.config.spec.jobTemplate.spec.template.spec.securityContext.sysctls[_].name + forbidden_sysctl(sysctl, forbiddenSysctls) +} + +# if all syscalls are forbidden +forbidden_sysctl(sysctl, arg) { + arg[_] == "*" +} + +forbidden_sysctl(sysctl, arg) { + arg[_] == sysctl +} + +forbidden_sysctl(sysctl, arg) { + startswith(sysctl, trim(arg[_], "*")) +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json new file mode 100644 index 000000000..10000a6e6 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json @@ -0,0 +1,14 @@ +{ + "name": "disAllowedVolumes", + "file": "disAllowedVolumes.rego", + "template_args": { + "name": "disAllowedVolumes", + "prefix": "", + "suffix": "" + }, + "severity": "MEDIUM", + "description": "Vulnerable to CVE-2020-8555 (affected version of kube-controller-manager: v1.18.0, v1.17.0 - v1.17.4, v1.16.0 - v1.16.8,< v1.15.11", + "reference_id": "AC-K8-DS-PO-M-0143", + "category": "Data Security", + "version": 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego new file mode 100644 index 000000000..706bbafa8 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego @@ -0,0 +1,52 @@ +package accurics + +#rule for pod_security_policy +{{.prefix}}{{.name}}{{.suffix}}[psp.id] { + psp := input.kubernetes_pod_security_policy[_] + affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] + volume_type := psp.config.spec.volumes[_] + volNotAllowed(volume_type, affected_volumes) +} + +#rule for pod +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] + volume_types := {x | pod.config.spec.volumes[_][x]; x != "name"} + vol:= volume_types[_] + volNotAllowed(vol, affected_volumes) +} + +#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] + volume_types := {x | kind.config.spec.template.spec.volumes[_][x]; x != "name"} + vol:= volume_types[_] + volNotAllowed(vol, affected_volumes) +} + +#rule for cron_job +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] + volume_types := {x | cron_job.config.spec.jobTemplate.spec.template.spec.volumes[_][x]; x != "name"} + vol:= volume_types[_] + volNotAllowed(vol, affected_volumes) +} + +volNotAllowed(field, affected_volumes) { + field == affected_volumes[_] +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego new file mode 100755 index 000000000..890439ed6 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego @@ -0,0 +1,35 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + vol := pod.config.spec.jobTemplate.spec.template.spec.volumes[_] + socketPathCheck(vol.hostPath.path) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + vol := pod.config.spec.volumes[_] + socketPathCheck(vol.hostPath.path) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + vol := pod.config.spec.template.spec.volumes[_] + socketPathCheck(vol.hostPath.path) +} + +socketPathCheck(attrib) { + contains(attrib, "/var/run/docker") +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego new file mode 100755 index 000000000..de75e611a --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego @@ -0,0 +1,196 @@ +package accurics + +#rule for pod, covers containers, initContainers, terraform, init_containers +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.containers[_] + checkForPodLatest(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.containers[_] + checkForPodNoTag(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.initContainers[_] + checkForPodLatest(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.initContainers[_] + checkForPodNoTag(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.init_containers[_] + checkForPodLatest(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.init_containers[_] + checkForPodNoTag(container) +} + +#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers, initContainers, terraform init_containers +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.containers[_] + checkForPodLatest(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.containers[_] + checkForPodNoTag(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.initContainers[_] + checkForPodLatest(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.init_containers[_] + checkForPodLatest(container) + } + +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.initContainers[_] + checkForPodNoTag(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.init_containers[_] + checkForPodNoTag(container) +} + +#rule for cron_job, covers containers, initContainers, terraform init_containers +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] + checkForPodLatest(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] + checkForPodNoTag(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] + checkForPodLatest(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] + checkForPodLatest(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] + checkForPodNoTag(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] + checkForPodNoTag(container) +} + +#check function for All KINDs +checkForPodLatest(arg) { + img_split := split(arg.image, ":") + tag := img_split[count(img_split) - 1] + tag == "latest" +} + +checkForPodNoTag(argument) { + img_split := split(argument.image, ":") + count(img_split) == 1 +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego new file mode 100755 index 000000000..6cf92e6f6 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego @@ -0,0 +1,105 @@ +package accurics + +#rule for pod, same will satisfy terraform pod, covers containers, initContainers, and terraform init_containers +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.containers[_] + satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] + not all(satisfied) +} + +#rule for init containers +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.initContainers[_] + satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] + not all(satisfied) +} + +#rule for terraform init_containers +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.init_containers[_] + satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] + not all(satisfied) +} + +#rule for deployment, daemonset, job, replica_set, replication_controller, stateful_set covers containers, initContainers, terraform init_containers +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.containers[_] + satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] + not all(satisfied) +} + +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.initContainers[_] + satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] + not all(satisfied) +} + +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.init_containers[_] + satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] + not all(satisfied) +} + +#rule for cron_job, covers containers, initContainers, terraform init_containers +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] + satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] + not all(satisfied) +} + +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] + satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] + not all(satisfied) +} + +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] + satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] + not all(satisfied) +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego new file mode 100755 index 000000000..d9746d16b --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego @@ -0,0 +1,6 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + pod.config.metadata.labels.app == "kubernetes-dashboard" +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego new file mode 100755 index 000000000..7ce3427d2 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego @@ -0,0 +1,20 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_cron_job", "undefined"), + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_pod", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + pod.config.metadata.namespace == "default" +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego new file mode 100755 index 000000000..148651ed3 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego @@ -0,0 +1,11 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + pod.config.spec.privileged == true +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod_security_policy[_] + pod.config.spec.privileged == true +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego new file mode 100755 index 000000000..19eaf1aef --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego @@ -0,0 +1,68 @@ +#liveenessprobe and readinessprobe are not applicable for init containers. +package accurics + +#rule for pod +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.containers[_] + not container["{{.argument}}"] +} + +#rule for pod terraform +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.containers[_] + not container["{{.argumentTF}}"] +} + +#rule for deployment, daemonset, job, replica_Set, replication_controller, stateful_set +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.containers[_] + not container["{{.argument}}"] +} + +#rule for terraform deployment, daemonset, job, replica_Set, replication_controller, stateful_set +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.containers[_] + not container["{{.argumentTF}}"] +} + +#rule for cronjob +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] + not container["{{.argument}}"] +} + +#rule for terraform cronjob +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] + not container["{{.argumentTF}}"] +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego new file mode 100755 index 000000000..f4bd21aeb --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego @@ -0,0 +1,153 @@ +package accurics + +#rule for pod, pod_security_policy covers containers +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_pod", "undefined"), + object.get(input, "kubernetes_pod_security_policy", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + not input_container_allowed(kind.config.metadata) +} + +#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + not input_container_allowed(kind.config.spec.template.metadata) +} + +#rule for cron_job +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + not input_container_allowed(cron_job.config.spec.jobTemplate.spec.template.metadata) +} + +input_container_allowed(metadata) { + metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"] == "runtime/default" +} + +input_container_allowed(metadata) { + metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"] == "docker/default" +} + + ####Kubernetes v1.19 or later######## + +#rule for pod covers containers and checks field seccompProfile at container security context which is found at spec.containers. +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.containers[_] + not check_seccomp(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.initContainers[_] + not check_seccomp(container) +} + +#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.containers[_] + not check_seccomp(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + container := kind.config.spec.template.spec.initContainers[_] + not check_seccomp(container) +} + +#rule for cron_job +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] + not check_seccomp(container) +} + +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] + not check_seccomp(container) +} + +##rule to check seccompProfile at PodSecurityContext which is found at PodSpec## + +#rule for pod +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + not check_seccomp(pod.config.spec) +} + +#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers +{{.prefix}}{{.name}}{{.suffix}}[kind.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + kind := item[_] + not check_seccomp(kind.config.spec.template.spec) +} + +#rule for cron_job +{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { + cron_job := input.kubernetes_cron_job[_] + not check_seccomp(cron_job.config.spec.jobTemplate.spec.template.spec) +} + +#function for all Kinds and scenarios +check_seccomp(container) { + container.securityContext.seccompProfile.type == "RuntimeDefault" +} + +check_seccomp(container) { + container.securityContext.seccompProfile.type == "DockerDefault" +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego new file mode 100755 index 000000000..780bb4577 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego @@ -0,0 +1,75 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + containers := pod.config.spec.containers[_] + env := containers.env[_] + env.valueFrom != [] +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + initcontainer := pod.config.spec.initContainers[_] + env := initcontainer.env[_] + env.valueFrom != [] +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + containers := pod.config.spec.jobTemplate.spec.template.spec.containers[_] + env := containers.env[_] + env.valueFrom != [] +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] + env := initcontainer.env[_] + env.valueFrom != [] +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + checkContainer(pod.config.spec.template.spec) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + checkInitContainer(pod.config.spec.template.spec) +} + +checkInitContainer(spec) { + containers := spec.initContainers[_] + env := containers.env[_] + env.valueFrom != [] +} + +checkContainer(spec) { + containers := spec.containers[_] + env := containers.env[_] + env.valueFrom != [] +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego new file mode 100755 index 000000000..f76c53966 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego @@ -0,0 +1,76 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + checkCorrectAttribute(pod.config.spec.jobTemplate.spec.template.spec) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + checkCorrectAttribute(pod.config.spec.template.spec) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + checkCorrectAttribute(pod.config.spec) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod_security_policy[_] + podSecurityCheck(pod.config.spec) +} + +checkCorrectAttribute(spec) { + container := spec.containers[_] + containerSecurityCheck(container) +} + +checkCorrectAttribute(spec) { + container := spec.initContainers[_] + containerSecurityCheck(container) +} + +checkCorrectAttribute(spec) { + secContext := spec.securityContext + podSecurityCheck(secContext) +} + +containerSecurityCheck(container) { + {{.not_allowed}} + container.{{.param1}}.{{.param}} == {{.value}} +} + +containerSecurityCheck(container) { + object.get(container, "{{.param1}}", "undefined") == "undefined" +} + +containerSecurityCheck(container) { + not container.{{.param1}}.{{.param}} +} + +containerSecurityCheck(container) { + {{.allowed}} + not container.{{.param1}}.{{.arg1}}.{{.arg2}} +} + +podSecurityCheck(secContext) { + {{.not_allowed}} + secContext.{{.param}} == {{.value}} +} + +podSecurityCheck(secContext) { + {{.not_allowed}} + object.get(secContext, "{{.param}}", "undefined") == "undefined" +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego new file mode 100755 index 000000000..5d24387ea --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego @@ -0,0 +1,103 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + container := pod.config.spec.containers[_] + not container.securityContext +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + initcontainer := pod.config.spec.initContainers[_] + not initcontainer.securityContext +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + not pod.config.spec.securityContext +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] + not container.securityContext +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] + not initcontainer.securityContext +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + not pod.config.spec.jobTemplate.spec.template.spec.securityContext +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined"), + object.get(input, "kubernetes_cron_job", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + checkPod(pod) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined"), + object.get(input, "kubernetes_cron_job", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + checkInitContainer(pod.config.spec.template.spec) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined"), + object.get(input, "kubernetes_cron_job", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + checkContainer(pod.config.spec.template.spec) +} + +checkContainer(spec) { + containers := spec.containers[_] + not containers.securityContext +} + +checkInitContainer(spec) { + containers := spec.initContainers[_] + not containers.securityContext +} + +checkPod(pod) { + not pod.config.spec.template.spec.securityContext +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego new file mode 100755 index 000000000..579448722 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego @@ -0,0 +1,36 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + attribute := pod.config.spec.jobTemplate.spec.template.spec + boolCheck(attribute) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + attribute := pod.config.spec + boolCheck(attribute) +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + attribute := pod.config.spec.template.spec + + boolCheck(attribute) +} + +boolCheck(attribute) { + attribute.{{.param}} == {{.value}} +} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego new file mode 100755 index 000000000..f4b90f2e4 --- /dev/null +++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego @@ -0,0 +1,35 @@ +package accurics + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_pod[_] + pod.config.metadata.labels.app == "helm" + pod.config.metadata.labels.name == "tiller" +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + pod := input.kubernetes_cron_job[_] + pod.config.spec.jobTemplate.spec.template.metadata.labels.app == "helm" + pod.config.spec.jobTemplate.spec.template.metadata.labels.name == "tiller" +} + +{{.prefix}}{{.name}}{{.suffix}}[pod.id] { + item_list := [ + object.get(input, "kubernetes_daemonset", "undefined"), + object.get(input, "kubernetes_deployment", "undefined"), + object.get(input, "kubernetes_job", "undefined"), + object.get(input, "kubernetes_replica_set", "undefined"), + object.get(input, "kubernetes_replication_controller", "undefined"), + object.get(input, "kubernetes_stateful_set", "undefined") + ] + + item = item_list[_] + item != "undefined" + + pod := item[_] + checkPod(pod) +} + +checkPod(pod) { + pod.config.spec.template.metadata.labels.app == "helm" + pod.config.spec.template.metadata.labels.name == "tiller" +} \ No newline at end of file diff --git a/pkg/http-server/routes.go b/pkg/http-server/routes.go index 400e99021..c7e86aee2 100644 --- a/pkg/http-server/routes.go +++ b/pkg/http-server/routes.go @@ -29,13 +29,19 @@ type Route struct { // Routes returns a slice of routes of API endpoints to be registered with // http server -func (g *APIServer) Routes() []*Route { - h := NewAPIHandler() +func (g *APIServer) Routes(configFile string) []*Route { + h := NewAPIHandler(configFile) routes := []*Route{ {verb: "GET", path: "/health", fn: h.Health}, {verb: "POST", path: versionedPath("/{iac}/{iacVersion}/{cloud}/local/file/scan"), fn: h.scanFile}, {verb: "POST", path: versionedPath("/{iac}/{iacVersion}/{cloud}/remote/dir/scan"), fn: h.scanRemoteRepo}, + + // K8s Webhook Routes + {verb: "GET", path: "/k8s/webhooks/{apiKey}/logs", fn: h.getLogs}, + {verb: "GET", path: "/k8s/webhooks/{apiKey}/logs/{uid}", fn: h.getLogByUID}, + {verb: "POST", path: versionedPath("/k8s/webhooks/{apiKey}/scan"), fn: h.scanIncomingWebhook}, } + return routes } diff --git a/pkg/http-server/routes_test.go b/pkg/http-server/routes_test.go index 06d3dadab..3e2058828 100644 --- a/pkg/http-server/routes_test.go +++ b/pkg/http-server/routes_test.go @@ -8,7 +8,7 @@ func TestRoutes(t *testing.T) { t.Run("health route check", func(t *testing.T) { var ( server = NewAPIServer() - got = server.Routes() + got = server.Routes("") passed = false ) diff --git a/pkg/http-server/server.go b/pkg/http-server/server.go index 68529d84a..2e19a377c 100644 --- a/pkg/http-server/server.go +++ b/pkg/http-server/server.go @@ -17,7 +17,8 @@ package httpserver // APIServer struct for http api server -type APIServer struct{} +type APIServer struct { +} // NewAPIServer returns a new APIServer{} func NewAPIServer() *APIServer { diff --git a/pkg/http-server/start.go b/pkg/http-server/start.go index a7a5212bd..de6d1463b 100644 --- a/pkg/http-server/start.go +++ b/pkg/http-server/start.go @@ -28,12 +28,12 @@ import ( ) // Start initializes api routes and starts http server -func Start() { +func Start(configFile string, certFile string, privateKeyFile string) { // create a new API server server := NewAPIServer() // get all routes - routes := server.Routes() + routes := server.Routes(configFile) serverPort := os.Getenv(TerrascanServerPort) if serverPort == "" { @@ -41,11 +41,11 @@ func Start() { } // register routes and start the http server - server.start(routes, serverPort) + server.start(routes, certFile, privateKeyFile) } // start http server -func (g *APIServer) start(routes []*Route, port string) { +func (g *APIServer) start(routes []*Route, certFile string, privateKeyFile string) { var ( err error @@ -61,6 +61,17 @@ func (g *APIServer) start(routes []*Route, port string) { router.Methods(v.verb).Path(v.path).HandlerFunc(v.fn) } + // Add a route for all static templates / assets. Currently used for the Webhook logs views + // go/terrascan/asset is the path where the assets files are located inside the docker container + router.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(http.Dir("/go/terrascan/assets")))) + + var port = GatewayDefaultPort + + // In case a certificate file is specified, we run the server with a different port + if certFile != "" { + port = TLSGatewayDefaultPort + } + // start http server server := &http.Server{ Addr: ":" + port, @@ -68,7 +79,13 @@ func (g *APIServer) start(routes []*Route, port string) { } go func() { - err = server.ListenAndServe() + var err error + if certFile != "" { + // In case a certificate file is specified, the server support TLS + err = server.ListenAndServeTLS(certFile, privateKeyFile) + } else { + err = server.ListenAndServe() + } if err != nil && err != http.ErrServerClosed { logger.Fatal(err) } diff --git a/pkg/http-server/templates/index.html b/pkg/http-server/templates/index.html new file mode 100644 index 000000000..43b0bd936 --- /dev/null +++ b/pkg/http-server/templates/index.html @@ -0,0 +1,34 @@ + + + + K8s Admission Review Logs + + + + + + + + + + + + + + + + {{range .}} + + + + + + + {{end}} + +
        TimeStatusRequestReasoning
        {{.CreatedAt}}{{.Status}}{{.Request}}{{.Reasoning}}
        + + + + + diff --git a/pkg/http-server/templates/show.html b/pkg/http-server/templates/show.html new file mode 100644 index 000000000..7b3633086 --- /dev/null +++ b/pkg/http-server/templates/show.html @@ -0,0 +1,40 @@ + + + + K8s Admission Review Logs + + + + + +
        + + + + + + + + + + + + + + + + + + + + + + + +
        UID{{.UID}}
        Status{{.Status}}
        Request{{.Request}}
        Violations Summary{{.Violations}}
        Deniable Violations{{.DeniableViolations}}
        +
        + + + + + diff --git a/pkg/http-server/webhook-deny-rule-matcher.go b/pkg/http-server/webhook-deny-rule-matcher.go new file mode 100644 index 000000000..4becf0be9 --- /dev/null +++ b/pkg/http-server/webhook-deny-rule-matcher.go @@ -0,0 +1,35 @@ +package httpserver + +import ( + "github.com/accurics/terrascan/pkg/config" + "github.com/accurics/terrascan/pkg/results" + "github.com/accurics/terrascan/pkg/utils" +) + +type webhookDenyRuleMatcher struct { +} + +// This class should check if one of the violations found is relevant for the specified K8s deny rules +func (g *webhookDenyRuleMatcher) match(violation results.Violation, denyRules config.K8sDenyRules) bool { + if &denyRules == nil { + return false + } + + // Currently we support: + // 1. A minimum severity level + // 2. A category list + // In case one of the conditions is met, we return true. (We perform an OR between the rules) + if len(denyRules.DeniedSeverity) > 0 && utils.CheckSeverity(violation.Severity, denyRules.DeniedSeverity) { + return true + } + + if denyRules.Categories != nil { + for _, category := range denyRules.Categories { + if category == violation.Category { + return true + } + } + } + + return false +} diff --git a/pkg/http-server/webhook-deny-rule-matcher_test.go b/pkg/http-server/webhook-deny-rule-matcher_test.go new file mode 100644 index 000000000..117ce8fbb --- /dev/null +++ b/pkg/http-server/webhook-deny-rule-matcher_test.go @@ -0,0 +1,98 @@ +package httpserver + +import ( + "github.com/accurics/terrascan/pkg/config" + "github.com/accurics/terrascan/pkg/results" + "testing" +) + +func TestDenyRuleMatcher(t *testing.T) { + testMediumSeverity := "MEDIUM" + testCategory := "Identity and Access Management" + testRuleName := "My Amazing Rule" + + table := []struct { + name string + ruleSeverity string + ruleCategory string + ruleName string + k8sDenyRules config.K8sDenyRules + expectedResult bool + }{ + { + name: "no deny rules", + ruleSeverity: testMediumSeverity, + ruleCategory: testCategory, + ruleName: testRuleName, + expectedResult: false, + }, + { + name: "matched severity", + ruleSeverity: testMediumSeverity, + ruleCategory: testCategory, + ruleName: testRuleName, + k8sDenyRules: config.K8sDenyRules{DeniedSeverity: testMediumSeverity}, + expectedResult: true, + }, + + { + name: "lower severity", + ruleSeverity: testMediumSeverity, + ruleCategory: testCategory, + ruleName: testRuleName, + k8sDenyRules: config.K8sDenyRules{DeniedSeverity: "LOW"}, + expectedResult: true, + }, + { + name: "higher severity", + ruleSeverity: testMediumSeverity, + ruleCategory: testCategory, + ruleName: testRuleName, + k8sDenyRules: config.K8sDenyRules{DeniedSeverity: "High"}, + expectedResult: false, + }, + { + name: "not matching category", + ruleSeverity: testMediumSeverity, + ruleCategory: testCategory, + ruleName: testRuleName, + k8sDenyRules: config.K8sDenyRules{Categories: []string{"WRONG!"}}, + expectedResult: false, + }, + + { + name: "matching category", + ruleSeverity: testMediumSeverity, + ruleCategory: testCategory, + ruleName: testRuleName, + k8sDenyRules: config.K8sDenyRules{Categories: []string{"WRONG!", testCategory}}, + expectedResult: true, + }, + { + name: "incorrect severity by matching category", + ruleSeverity: testMediumSeverity, + ruleCategory: testCategory, + ruleName: testRuleName, + k8sDenyRules: config.K8sDenyRules{Categories: []string{"WRONG!", testCategory}, DeniedSeverity: "HIGH"}, + expectedResult: true, + }, + } + + var denyRuleMatcher = webhookDenyRuleMatcher{} + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + + violation := results.Violation{ + RuleName: tt.ruleName, + Severity: tt.ruleSeverity, + Category: tt.ruleCategory, + } + + result := denyRuleMatcher.match(violation, tt.k8sDenyRules) + if result != tt.expectedResult { + t.Errorf("Expected: %v, Got: %v", tt.expectedResult, result) + } + }) + } +} diff --git a/pkg/http-server/webhook-scan-logger.go b/pkg/http-server/webhook-scan-logger.go new file mode 100644 index 000000000..9cea0d532 --- /dev/null +++ b/pkg/http-server/webhook-scan-logger.go @@ -0,0 +1,196 @@ +package httpserver + +import ( + "database/sql" + "go.uber.org/zap" + "os" + "time" +) + +type WebhookScanLogger struct { + test bool +} + +type webhookScanLog struct { + UID string + Request string + Allowed bool + ViolationsSummary string + DeniableViolations string + CreatedAt time.Time +} + +// The file name where the DB is stored. Currently we use an SQLite DB +var dbFileName = "k8s-admission-review-logs.db" + +func (g *WebhookScanLogger) log(webhookScanLog webhookScanLog) error { + // Insert a new Log record to the DB + + db, err := g.getDbHandler() + if err != nil { + return err + } + defer db.Close() + + insertLogSQL := `INSERT INTO logs(uid, request, allowed, violations_summary, deniable_violations, created_at) + VALUES (?, ?, ?, ?, ?, ?)` + + statement, err := db.Prepare(insertLogSQL) + if err != nil { + zap.S().Errorf("failed preparing SQL statement. error: '%v'", err) + return err + } + _, err = statement.Exec(webhookScanLog.UID, + webhookScanLog.Request, + webhookScanLog.Allowed, + webhookScanLog.ViolationsSummary, + webhookScanLog.DeniableViolations, + webhookScanLog.CreatedAt) + if err != nil { + zap.S().Errorf("failed to insert a new log. error: '%v'", err) + return err + } + + return nil +} + +func (g *WebhookScanLogger) fetchLogs() ([]webhookScanLog, error) { + // Fetch the entire logs in the DB, ordered by created_at DESC (the most updated will be at the top) + + db, err := g.getDbHandler() + if err != nil { + return nil, err + } + defer db.Close() + + row, err := db.Query("SELECT * FROM logs ORDER BY created_at DESC") + if err != nil { + zap.S().Errorf("failed query logs table. error: '%v'", err) + return nil, err + } + + var result []webhookScanLog + defer row.Close() + for row.Next() { + var id int + var uid string + var request string + var allowed bool + var violationsSummary string + var deniableViolations string + var createdAt time.Time + row.Scan(&id, &uid, &request, &allowed, &violationsSummary, &deniableViolations, &createdAt) + + result = append(result, webhookScanLog{ + UID: uid, + Request: request, + Allowed: allowed, + ViolationsSummary: violationsSummary, + DeniableViolations: deniableViolations, + CreatedAt: createdAt, + }) + } + + return result, nil +} + +func (g *WebhookScanLogger) fetchLogById(logUID string) (*webhookScanLog, error) { + // Fetch a specific log by its request UID + + db, err := g.getDbHandler() + if err != nil { + return nil, err + } + defer db.Close() + + row, err := db.Query("SELECT * FROM logs WHERE uid=?", logUID) + if err != nil { + zap.S().Errorf("failed query logs table. error: '%v'", err) + return nil, err + } + defer row.Close() + + for row.Next() { + var id int + var uid string + var request string + var allowed bool + var violationsSummary string + var deniableViolations string + var createdAt time.Time + row.Scan(&id, &uid, &request, &allowed, &violationsSummary, &deniableViolations, &createdAt) + + return &webhookScanLog{ + UID: uid, + Request: request, + Allowed: allowed, + ViolationsSummary: violationsSummary, + DeniableViolations: deniableViolations, + CreatedAt: createdAt, + }, nil + } + + return &webhookScanLog{}, nil +} + +func (g *WebhookScanLogger) initDBIfNeeded() error { + // Check where the SQL file exists. If it does do nothing. Otherwise, create the DB file and the Logs table. + if _, err := os.Stat(g.dbFilePath()); os.IsNotExist(err) { + file, err := os.Create(g.dbFilePath()) + if err != nil { + zap.S().Errorf("failed create db file. error: '%v'", err) + return err + } + file.Close() + + db, err := sql.Open("sqlite3", g.dbFilePath()) + if err != nil { + zap.S().Errorf("failed to open sql file. error: '%v'", err) + return err + } + defer db.Close() + + createLogsTableSQL := `CREATE TABLE logs ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "uid" TEXT UNIQUE, + "request" TEXT, + "allowed" INTEGER, + "violations_summary" TEXT, + "deniable_violations" TEXT, + "created_at" DATETIME + );` + statement, err := db.Prepare(createLogsTableSQL) + if err != nil { + zap.S().Errorf("failed to create logs table. error: '%v'", err) + return err + } + statement.Exec() + } + + return nil +} + +func (g *WebhookScanLogger) getDbHandler() (*sql.DB, error) { + g.initDBIfNeeded() + + db, err := sql.Open("sqlite3", g.dbFilePath()) + if err != nil { + zap.S().Errorf("failed to open sql file. error: '%v'", err) + } + + return db, err +} + +func (g *WebhookScanLogger) dbFilePath() string { + if g.test { + return "./" + dbFileName + } else { + // This is where the DB file should be located in the container (It is going to be saved in the host machine volume) + return "/data/k8s-admission-review-logs.db" + } +} + +// Used for Tests only - clear the DB file after the tests are done +func (g *WebhookScanLogger) clearDbFilePath() { + os.Remove(g.dbFilePath()) +} diff --git a/pkg/http-server/webhook-scan-logger_test.go b/pkg/http-server/webhook-scan-logger_test.go new file mode 100644 index 000000000..570fc8d84 --- /dev/null +++ b/pkg/http-server/webhook-scan-logger_test.go @@ -0,0 +1,78 @@ +package httpserver + +import ( + "testing" + "time" +) + +func TestLogs(t *testing.T) { + t.Run("Log a new webhook scan", func(t *testing.T) { + var logger = WebhookScanLogger{ + test: true, + } + defer logger.clearDbFilePath() + + fetchedLogs, err := logger.fetchLogs() + if len(fetchedLogs) > 0 { + t.Errorf("At the beginning no logs should exist. Got: '%v'", len(fetchedLogs)) + } + + if err != nil { + t.Errorf("Got error") + } + + var log = webhookScanLog{ + UID: "myUID", + Request: "MyRequest", + CreatedAt: time.Now(), + Allowed: true, + DeniableViolations: "MyViolations", + ViolationsSummary: "ViolationsSummary", + } + + logger.log(log) + + fetchedLogs, err = logger.fetchLogs() + if err != nil { + t.Errorf("Got error") + } + + if len(fetchedLogs) != 1 { + t.Errorf("A new log should be returned. Got: '%v' logs", len(fetchedLogs)) + } + + myFetchLog, err := logger.fetchLogById(log.UID) + if err != nil { + t.Errorf("Got error") + } + + if len(myFetchLog.UID) < 1 { + t.Errorf("Log with ID: '%v' is not returned by fetchLogById", log.UID) + } + + if myFetchLog.UID != log.UID { + t.Errorf("Wrong UID. Expected '%v', Got: '%v'", log.UID, myFetchLog.UID) + + } + + if myFetchLog.Allowed != log.Allowed { + t.Errorf("Wrong Allowed. Expected '%v', Got: '%v'", log.Allowed, myFetchLog.Allowed) + } + + if myFetchLog.ViolationsSummary != log.ViolationsSummary { + t.Errorf("Wrong ViolationsSummary. Expected '%v', Got: '%v'", log.ViolationsSummary, myFetchLog.ViolationsSummary) + } + + if myFetchLog.Request != log.Request { + t.Errorf("Wrong Request. Expected '%v', Got: '%v'", log.Request, myFetchLog.Request) + } + + if myFetchLog.DeniableViolations != log.DeniableViolations { + t.Errorf("Wrong DeniableViolations. Expected '%v', Got: '%v'", log.DeniableViolations, myFetchLog.DeniableViolations) + } + + if myFetchLog.CreatedAt.Unix() != log.CreatedAt.Unix() { + t.Errorf("Wrong CreatedAt. Expected '%v', Got: '%v'", log.CreatedAt, myFetchLog.CreatedAt) + } + }) +} diff --git a/pkg/http-server/webhook-scan-logs.go b/pkg/http-server/webhook-scan-logs.go new file mode 100644 index 000000000..3ea0a6e32 --- /dev/null +++ b/pkg/http-server/webhook-scan-logs.go @@ -0,0 +1,242 @@ +/* + Copyright (C) 2020 Accurics, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package httpserver + +import ( + "encoding/json" + "fmt" + "github.com/accurics/terrascan/pkg/results" + "github.com/gorilla/mux" + _ "github.com/mattn/go-sqlite3" + "go.uber.org/zap" + "html/template" + "net/http" + "time" +) + +type webhookDisplayedViolation struct { + RuleName string `json:"rule_name"` + Category string `json:"category"` + Description string `json:"description"` + Severity string `json:"severity"` +} + +type webhookDisplayedReview struct { + Request webhookDisplayedRequest `json:"request"` +} + +type webhookDisplayedRequest struct { + Operation string `json:"operation"` + Object map[string]interface{} `json:"object"` +} + +type webhookDisplayedIndexScanLog struct { + CreatedAt time.Time + LogUrl string + Status string + Request string + Reasoning string +} + +type webhookDisplayedShowLog struct { + CreatedAt time.Time + UID string + Status string + Request string + Violations string + DeniableViolations string +} + +func (g *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) { + // Return an HTML page including all the logs history + + params := mux.Vars(r) + + apiKey := params["apiKey"] + if !g.validateAuthorization(apiKey, w) { + return + } + + logger := WebhookScanLogger{ + test: g.test, + } + + // The templates are saved in the docker in this location + t, _ := template.ParseFiles("/go/terrascan/index.html") + + logs, err := logger.fetchLogs() + if err != nil { + errMsg := fmt.Sprintf("error reading logs from DB: '%v'", err) + zap.S().Error(errMsg) + apiErrorResponse(w, errMsg, http.StatusInternalServerError) + return + } + + var logsData []webhookDisplayedIndexScanLog + for _, log := range logs { + logsData = append(logsData, webhookDisplayedIndexScanLog{ + CreatedAt: log.CreatedAt, + Status: g.getLogStatus(log), + LogUrl: g.getLogPath(r.Host, apiKey, log.UID), + Reasoning: g.getLogReasoning(log), + Request: g.getLogRequest(log), + }) + } + + t.Execute(w, logsData) +} + +func (g *APIHandler) getLogByUID(w http.ResponseWriter, r *http.Request) { + // Return an HTML page including the selected log + + params := mux.Vars(r) + + if !g.validateAuthorization(params["apiKey"], w) { + return + } + + var uid = params["uid"] + if len(uid) < 1 { + apiErrorResponse(w, "Log UID is missing", http.StatusBadRequest) + return + } + + logger := WebhookScanLogger{ + test: g.test, + } + + log, err := logger.fetchLogById(uid) + if err != nil { + errMsg := fmt.Sprintf("error reading logs from DB: '%v'", err) + zap.S().Error(errMsg) + apiErrorResponse(w, errMsg, http.StatusInternalServerError) + return + } + + if len(log.UID) < 1 { + apiErrorResponse(w, "Log is not found", http.StatusNotFound) + return + } + + displayedScanLog := webhookDisplayedShowLog{ + UID: log.UID, + CreatedAt: log.CreatedAt, + Status: g.getLogStatus(*log), + Request: log.Request, + Violations: log.ViolationsSummary, + DeniableViolations: log.DeniableViolations, + } + + t, _ := template.ParseFiles("/go/terrascan/show.html") + + t.Execute(w, displayedScanLog) +} + +func (g *APIHandler) getLogPath(host string, apiKey string, logUID string) string { + // Use this as the link to show the a specific log + return fmt.Sprintf("https://%v/k8s/webhooks/%v/logs/%v", host, apiKey, logUID) +} + +func (g *APIHandler) getLogStatus(log webhookScanLog) string { + // Calculate a log status: + // 1. !Allowed -> Rejected + // 2. Allowed -> if there are violations -> Allowed with Warnings. Otherwise -> Allowed + if !log.Allowed { + return "Rejected" + } + + var violationStore results.ViolationStore + err := json.Unmarshal([]byte(log.ViolationsSummary), &violationStore) + if err != nil { + zap.S().Errorf("Failed to ..") + } + + if len(violationStore.Violations) > 0 { + return "Allowed with warnings" + } + + return "Allowed" +} + +func (g *APIHandler) getLogReasoning(log webhookScanLog) string { + // Reasoning: + // - In case the request is denied (rejected), show the violations that cause the denial. + // - Otherwise, if there are violations, show the full violations list was found + // - Otherwise, reasoning is empty + + var violations []*results.Violation + if !log.Allowed { + err := json.Unmarshal([]byte(log.DeniableViolations), &violations) + if err != nil { + zap.S().Errorf("Failed to deserialize deniable violations summary. Error: %v", err.Error()) + return "" + } + } else { + var violationStore results.ViolationStore + err := json.Unmarshal([]byte(log.ViolationsSummary), &violationStore) + if err != nil { + zap.S().Errorf("Failed to deserialize violations summary. Error: %v", err.Error()) + return "" + } + + violations = violationStore.Violations + } + + var result []webhookDisplayedViolation + + if len(violations) < 1 { + return "" + } else { + for _, v := range violations { + result = append(result, webhookDisplayedViolation{ + Category: v.Category, + Description: v.Description, + RuleName: v.RuleName, + Severity: v.Severity, + }) + } + + encoded, err := json.Marshal(result) + if err != nil { + zap.S().Errorf("failed to serialize violations: '%v'", err) + return "" + } + + return string(encoded) + } + + return "" +} + +func (g *APIHandler) getLogRequest(log webhookScanLog) string { + var review webhookDisplayedReview + + err := json.Unmarshal([]byte(log.Request), &review) + + if err != nil { + zap.S().Errorf("Failed to deserialize request. Error: %v", err.Error()) + return "{}" + } + + result, err := json.Marshal(review.Request) + if err != nil { + zap.S().Errorf("Failed to serialize request. Error: %v", err.Error()) + return "{}" + } + + return string(result) +} diff --git a/pkg/http-server/webhook-scan.go b/pkg/http-server/webhook-scan.go new file mode 100644 index 000000000..f98eef9e3 --- /dev/null +++ b/pkg/http-server/webhook-scan.go @@ -0,0 +1,297 @@ +/* + Copyright (C) 2020 Accurics, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package httpserver + +import ( + "encoding/json" + "fmt" + "github.com/accurics/terrascan/pkg/config" + "github.com/accurics/terrascan/pkg/results" + "github.com/accurics/terrascan/pkg/runtime" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtimeK8s "k8s.io/apimachinery/pkg/runtime" + + "k8s.io/apimachinery/pkg/runtime/serializer" + + "github.com/gorilla/mux" + "go.uber.org/zap" + "io/ioutil" + v1 "k8s.io/api/admission/v1" + "net/http" + "os" + "time" +) + +func (g *APIHandler) scanIncomingWebhook(w http.ResponseWriter, r *http.Request) { + currentTime := time.Now() + + params := mux.Vars(r) + apiKey := params["apiKey"] + + // Validate if authorized (API key is specified and matched the server one (saved in an environment variable) + if !g.validateAuthorization(apiKey, w) { + return + } + + // Read the request into byte array + bytesRequestAdmissionReview, err := ioutil.ReadAll(r.Body) + if err != nil { + msg := fmt.Sprintf("Failed to read admission review: '%v'", err) + apiErrorResponse(w, msg, http.StatusBadRequest) + return + } + + zap.S().Debugf("scanning configuration webhook request: %+v", string(bytesRequestAdmissionReview)) + + // Unmarshal the byte array into a v1.AdmissionReview object + requestedAdmissionReview, err := g.deserializeAdmissionReviewRequest(bytesRequestAdmissionReview) + if err != nil { + apiErrorResponse(w, err.Error(), http.StatusBadRequest) + return + } + + // In case the object is nil => an operation of DELETE happened, just return 'allow' since there is nothing to check + if len(requestedAdmissionReview.Request.Object.Raw) < 1 { + g.sendResponseAdmissionReview(w, *requestedAdmissionReview, true, nil, "") + return + } + + // Save the object into a temp file for the policy engines + tempFile, err := g.writeObjectToTempFile(requestedAdmissionReview.Request.Object.Raw) + defer os.Remove(tempFile.Name()) + if err != nil { + apiErrorResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + // Run the policy engines + output, err := g.executeEngines(*tempFile) + if err != nil { + apiErrorResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + // Calculate if there are anydeny violations + denyViolations, err := g.getDenyViolations(*output) + allowed := len(denyViolations) < 1 + logPath := g.getLogPath(r.Host, apiKey, string(requestedAdmissionReview.Request.UID)) + + // Log the request in the DB + err = g.logWebhook(*output, string(requestedAdmissionReview.Request.UID), bytesRequestAdmissionReview, denyViolations, currentTime, allowed) + if err != nil { + apiErrorResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + // Send the correct response according to the result + g.sendResponseAdmissionReview(w, *requestedAdmissionReview, allowed, output, logPath) +} + +func (g *APIHandler) validateAuthorization(apiKey string, w http.ResponseWriter) bool { + if len(apiKey) < 1 { + msg := "apiKey is missing" + zap.S().Error(msg) + apiErrorResponse(w, msg, http.StatusBadRequest) + return false + } + + savedApiKey := os.Getenv("K8S_WEBHOOK_API_KEY") + if len(savedApiKey) < 1 { + msg := "K8S_WEBHOOK_API_KEY environment variable MUST be declared" + zap.S().Error(msg) + apiErrorResponse(w, msg, http.StatusInternalServerError) + return false + } + + if apiKey != savedApiKey { + msg := "Invalid apiKey" + zap.S().Error(msg) + apiErrorResponse(w, msg, http.StatusUnauthorized) + return false + } + + return true +} + +func (g *APIHandler) getDeniedViolations(violations results.ViolationStore, denyRules config.K8sDenyRules) []*results.Violation { + // Check whether one of the violations matches the deny violations configuration + + var denyViolations []*results.Violation + + denyRuleMatcher := webhookDenyRuleMatcher{} + + for _, violation := range violations.Violations { + if denyRuleMatcher.match(*violation, denyRules) { + denyViolations = append(denyViolations, violation) + } + } + + return denyViolations +} + +func (g *APIHandler) writeObjectToTempFile(objectBytes []byte) (*os.File, error) { + tempFile, err := ioutil.TempFile("", "terrascan-*.json") + if err != nil { + zap.S().Errorf("failed to create temp file: '%v'", err) + return nil, err + } + + zap.S().Debugf("created temp config file at '%s'", tempFile.Name()) + + _, err = tempFile.Write(objectBytes) + if err != nil { + zap.S().Errorf("failed to write object to temp file: '%v'", err) + return nil, err + } + + return tempFile, nil +} + +func (g *APIHandler) executeEngines(tempFile os.File) (*runtime.Output, error) { + var executor *runtime.Executor + var err error + if g.test { + executor, err = runtime.NewExecutor("k8s", "v1", []string{"k8s"}, + tempFile.Name(), "", g.configFile, []string{"./k8s_testdata/testpolicies"}, []string{}, []string{}, []string{}, "") + } else { + executor, err = runtime.NewExecutor("k8s", "v1", []string{"k8s"}, + tempFile.Name(), "", g.configFile, []string{}, []string{}, []string{}, []string{}, "") + } + + if err != nil { + zap.S().Errorf("failed to create runtime executer: '%v'", err) + return nil, err + } + + result, err := executor.Execute() + if err != nil { + zap.S().Error("failed to scan resource object. error: '%v'", err) + return nil, err + } + + return &result, nil +} + +func (g *APIHandler) getDenyViolations(output runtime.Output) ([]*results.Violation, error) { + // Calcualte the deny violations according to the configuration specified in the config file + configReader, err := config.NewTerrascanConfigReader(g.configFile) + if err != nil { + zap.S().Errorf("error loading config file: '%v'", err) + return nil, err + } + + denyViolations := g.getDeniedViolations(*output.Violations.ViolationStore, configReader.GetK8sDenyRules()) + + return denyViolations, nil +} + +func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, + requestedAdmissionReview v1.AdmissionReview, + allowed bool, + output *runtime.Output, + logPath string) { + responseAdmissionReview := &v1.AdmissionReview{} + responseAdmissionReview.SetGroupVersionKind(requestedAdmissionReview.GroupVersionKind()) + + responseAdmissionReview.Response = &v1.AdmissionResponse{ + UID: requestedAdmissionReview.Request.UID, + Allowed: allowed, + } + + if output != nil { + // Means we ran the engines and we have results + if allowed { + if len(output.Violations.ViolationStore.Violations) > 0 { + // In case there are no denial violations, just return the log URL as a warning + responseAdmissionReview.Response.Warnings = []string{logPath} + } + } else { + // In case the request was denied, return 403 and the log URL as an error message + responseAdmissionReview.Response.Result = &metav1.Status{Message: logPath, Code: 403} + } + } + + respBytes, err := json.Marshal(responseAdmissionReview) + if err != nil { + msg := fmt.Sprintf("failed to serialize admission review response: %v", err) + zap.S().Error(msg) + apiErrorResponse(w, msg, http.StatusInternalServerError) + } + + zap.S().Debugf("Response result: %+v", string(respBytes)) + + apiResponse(w, string(respBytes), http.StatusOK) +} + +func (g *APIHandler) logWebhook(output runtime.Output, + uid string, + bytesAdmissionReview []byte, + denyViolations []*results.Violation, + currentTime time.Time, + allowed bool) error { + var deniedViolationsEncoded string + + if len(denyViolations) < 1 { + deniedViolationsEncoded = "" + } else { + d, _ := json.Marshal(denyViolations) + deniedViolationsEncoded = string(d) + } + + encodedViolationsSummary, _ := json.Marshal(output.Violations.ViolationStore) + + logger := WebhookScanLogger{ + test: g.test, + } + + err := logger.log(webhookScanLog{ + UID: uid, + Request: string(bytesAdmissionReview), + Allowed: allowed, + DeniableViolations: deniedViolationsEncoded, + ViolationsSummary: string(encodedViolationsSummary), + CreatedAt: currentTime, + }) + if err != nil { + zap.S().Error("error logging scan result: '%v'", err) + return err + } + + return nil +} + +func (g *APIHandler) deserializeAdmissionReviewRequest(bytesAdmissionReview []byte) (*v1.AdmissionReview, error) { + var scheme = runtimeK8s.NewScheme() + v1.AddToScheme(scheme) + + var codecs = serializer.NewCodecFactory(scheme) + deserializer := codecs.UniversalDeserializer() + + obj, _, err := deserializer.Decode(bytesAdmissionReview, nil, nil) + if err != nil { + zap.S().Errorf("Request could not be decoded: %v", err) + return nil, err + } + + requestedAdmissionReview, ok := obj.(*v1.AdmissionReview) + if !ok { + zap.S().Errorf("Failed to deserialize request body to v1.AdmissionReview. Obj: %v", obj) + return nil, err + } + + return requestedAdmissionReview, nil +} diff --git a/pkg/http-server/webhook-scan_test.go b/pkg/http-server/webhook-scan_test.go new file mode 100644 index 000000000..c7e85016a --- /dev/null +++ b/pkg/http-server/webhook-scan_test.go @@ -0,0 +1,244 @@ +package httpserver + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/gorilla/mux" + "io/ioutil" + v1 "k8s.io/api/admission/v1" + "net/http" + "net/http/httptest" + "os" + "testing" +) + +func TestUWebhooks(t *testing.T) { + testFilePath := "./k8s_testdata/testconfig.json" + testApiKey := "Test-API-KEY" + testEnvApiKey := "Test-API-KEY" + testConfigFile := "" + + table := []struct { + name string + contentRequestPath string + apiKey string + envApiKey string + wantStatus int + configFile string + warnings bool + allowed bool + statusCode int32 + statusMessage bool + }{ + { + name: "missing api key", + contentRequestPath: testFilePath, + apiKey: "", + envApiKey: testEnvApiKey, + wantStatus: http.StatusBadRequest, + configFile: testConfigFile, + }, + { + name: "missing K8S_WEBHOOK_API_KEY", + contentRequestPath: testFilePath, + apiKey: testApiKey, + envApiKey: "", + wantStatus: http.StatusInternalServerError, + configFile: testConfigFile, + }, + { + name: "invalid api key", + contentRequestPath: testFilePath, + apiKey: testApiKey, + envApiKey: "Invalid API KEY", + wantStatus: http.StatusUnauthorized, + configFile: testConfigFile, + }, + { + name: "invalid api key", + contentRequestPath: testFilePath, + apiKey: testApiKey, + envApiKey: "Invalid API KEY", + wantStatus: http.StatusUnauthorized, + configFile: testConfigFile, + }, + { + name: "invalid request json content", + contentRequestPath: "./k8s_testdata/invalid.json", + apiKey: testApiKey, + envApiKey: testEnvApiKey, + wantStatus: http.StatusBadRequest, + configFile: testConfigFile, + }, + { + name: "empty request json content", + contentRequestPath: "./k8s_testdata/empty.json", + apiKey: testApiKey, + envApiKey: testEnvApiKey, + wantStatus: http.StatusBadRequest, + configFile: testConfigFile, + }, + { + name: "request with empty object", + contentRequestPath: "./k8s_testdata/empty_object.json", + apiKey: testApiKey, + envApiKey: testEnvApiKey, + wantStatus: http.StatusOK, + configFile: testConfigFile, + warnings: false, + allowed: true, + }, + { + name: "safe request object", + contentRequestPath: testFilePath, + apiKey: testApiKey, + envApiKey: testEnvApiKey, + wantStatus: http.StatusOK, + configFile: testConfigFile, + warnings: false, + allowed: true, + }, + { + name: "risky request object without config", + contentRequestPath: "./k8s_testdata/risky_testconfig.json", + apiKey: testApiKey, + envApiKey: testEnvApiKey, + configFile: testConfigFile, + warnings: true, + allowed: true, + wantStatus: http.StatusOK, + }, + { + name: "risky request object with config that make it safe", + contentRequestPath: "./k8s_testdata/risky_testconfig.json", + apiKey: testApiKey, + envApiKey: testEnvApiKey, + configFile: "./k8s_testdata/config-specific-rule.toml", + warnings: false, + allowed: true, + wantStatus: http.StatusOK, + }, + { + name: "risky request object with config that just removes some of the violations", + contentRequestPath: "./k8s_testdata/risky_testconfig.json", + apiKey: testApiKey, + envApiKey: testEnvApiKey, + configFile: "./k8s_testdata/config-medium-severity.toml", + warnings: true, + allowed: true, + wantStatus: http.StatusOK, + }, + { + name: "risky request object with denied severity", + contentRequestPath: "./k8s_testdata/risky_testconfig.json", + apiKey: testApiKey, + envApiKey: testEnvApiKey, + configFile: "./k8s_testdata/config-deny-high.toml", + warnings: false, + allowed: false, + statusCode: 403, + statusMessage: true, + wantStatus: http.StatusOK, + }, + { + name: "risky request object with denied categories", + contentRequestPath: "./k8s_testdata/risky_testconfig.json", + apiKey: testApiKey, + envApiKey: testEnvApiKey, + configFile: "./k8s_testdata/config-deny-category.toml", + warnings: false, + allowed: false, + statusCode: 403, + statusMessage: true, + wantStatus: http.StatusOK, + }, + { + name: "risky request object with denied categories that does not exist", + contentRequestPath: "./k8s_testdata/risky_testconfig.json", + apiKey: testApiKey, + envApiKey: testEnvApiKey, + configFile: "./k8s_testdata/config-deny-non-existing-category.toml", + warnings: true, + allowed: true, + wantStatus: http.StatusOK, + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + os.Setenv("K8S_WEBHOOK_API_KEY", tt.envApiKey) + + // test file to upload + path := tt.contentRequestPath + jsonFile, err := os.Open(path) + if err != nil { + t.Error(err) + return + } + defer jsonFile.Close() + logger := WebhookScanLogger{ + test: true, + } + defer logger.clearDbFilePath() + + byteValue, _ := ioutil.ReadAll(jsonFile) + + var admissionRequest v1.AdmissionReview + json.Unmarshal(byteValue, &admissionRequest) + + var url string + if len(tt.apiKey) > 0 { + url = fmt.Sprintf("/v1/k8s/webhooks/%v/scan", tt.apiKey) + } else { + url = fmt.Sprintf("/v1/k8s/webhooks/scan") + } + + req := httptest.NewRequest("POST", url, bytes.NewReader(byteValue)) + req.Header.Set("Content-Type", "application/json") + req = mux.SetURLVars(req, map[string]string{ + "apiKey": tt.apiKey, + }) + res := httptest.NewRecorder() + // new api handler + h := &APIHandler{test: true, configFile: tt.configFile} + h.scanIncomingWebhook(res, req) + + if res.Code != tt.wantStatus { + t.Errorf("incorrect status code, got: '%v', want: '%v', error: '%v'", res.Code, tt.wantStatus, res.Body) + } + + var response v1.AdmissionReview + _ = json.Unmarshal(res.Body.Bytes(), &response) + + if res.Code == http.StatusOK { + if tt.warnings && response.Response.Warnings == nil { + t.Errorf("Expected warnings but received None") + } + + if tt.allowed != response.Response.Allowed { + t.Errorf("Mismach in allowed. Got: %v, expected: %v", response.Response.Allowed, tt.allowed) + } + + if tt.statusCode != 0 && tt.statusCode != response.Response.Result.Code { + t.Errorf("Mismach Statud code Got: %v, expected: %v", response.Response.Result.Code, tt.statusCode) + } + + if tt.warnings || tt.statusMessage { + var logPath string + if tt.warnings { + logPath = response.Response.Warnings[0] + } else if tt.statusMessage { + logPath = response.Response.Result.Message + } + + expectedLogPath := fmt.Sprintf("https://%v/k8s/webhooks/%v/logs/705ab4f5-6393-11e8-b7cc-42010a800002", req.Host, tt.envApiKey) + + if logPath != expectedLogPath { + t.Errorf("Mismach Log path. Got: %v, expected: %v", logPath, expectedLogPath) + } + } + } + }) + } +} diff --git a/pkg/initialize/run.go b/pkg/initialize/run.go index 6f610f66a..7302cadc1 100644 --- a/pkg/initialize/run.go +++ b/pkg/initialize/run.go @@ -54,7 +54,7 @@ func Run(isNonInitCmd bool) error { return err } - zap.S().Debug("intialized successfully") + zap.S().Debug("initialized successfully") return nil } diff --git a/pkg/runtime/executor_test.go b/pkg/runtime/executor_test.go index 1effb0ea2..51f1729fb 100644 --- a/pkg/runtime/executor_test.go +++ b/pkg/runtime/executor_test.go @@ -1,12 +1,9 @@ /* Copyright (C) 2020 Accurics, Inc. - Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/pkg/runtime/testdata/scan-skip-rules-low-severity.toml b/pkg/runtime/testdata/scan-skip-rules-low-severity.toml index ba3e21fda..2137332cb 100644 --- a/pkg/runtime/testdata/scan-skip-rules-low-severity.toml +++ b/pkg/runtime/testdata/scan-skip-rules-low-severity.toml @@ -17,4 +17,4 @@ level = "low" list = [ "RESILIENCE", "IDENTITY AND ACCESS MANAGEMENT", -] \ No newline at end of file +] From fa10ba995d8eb162b6c5aad05f3d6743e5dc0579 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Sun, 14 Mar 2021 07:40:08 +0530 Subject: [PATCH 02/38] add cert and key file paths flags in server command --- pkg/cli/root.go | 3 +++ pkg/cli/server.go | 4 ++-- pkg/http-server/webhook-scan-logs.go | 9 ++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pkg/cli/root.go b/pkg/cli/root.go index f7872e995..3d4247b26 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -23,10 +23,13 @@ import ( var ( // LogLevel Logging level (debug, info, warn, error, panic, fatal) LogLevel string + // LogType Logging output type (console, json) LogType string + // OutputType Violation output type (human, json, yaml, xml) OutputType string + // ConfigFile Config file path ConfigFile string ) diff --git a/pkg/cli/server.go b/pkg/cli/server.go index c5889c12b..ef395ab53 100644 --- a/pkg/cli/server.go +++ b/pkg/cli/server.go @@ -47,7 +47,7 @@ func server(cmd *cobra.Command, args []string) { } func init() { - rootCmd.PersistentFlags().StringVarP(&PrivateKeyFile, "key-path", "", "", "private key file path") - rootCmd.PersistentFlags().StringVarP(&CertFile, "cert-path", "", "", "certificate file path") + serverCmd.Flags().StringVarP(&PrivateKeyFile, "key-path", "", "", "private key file path") + serverCmd.Flags().StringVarP(&CertFile, "cert-path", "", "", "certificate file path") RegisterCommand(rootCmd, serverCmd) } diff --git a/pkg/http-server/webhook-scan-logs.go b/pkg/http-server/webhook-scan-logs.go index 3ea0a6e32..e3227d7fc 100644 --- a/pkg/http-server/webhook-scan-logs.go +++ b/pkg/http-server/webhook-scan-logs.go @@ -19,13 +19,14 @@ package httpserver import ( "encoding/json" "fmt" + "html/template" + "net/http" + "time" + "github.com/accurics/terrascan/pkg/results" "github.com/gorilla/mux" _ "github.com/mattn/go-sqlite3" "go.uber.org/zap" - "html/template" - "net/http" - "time" ) type webhookDisplayedViolation struct { @@ -218,8 +219,6 @@ func (g *APIHandler) getLogReasoning(log webhookScanLog) string { return string(encoded) } - - return "" } func (g *APIHandler) getLogRequest(log webhookScanLog) string { From a5b4e5cd119f98bf8ff11b07c3e1e23928785f5e Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Sun, 14 Mar 2021 09:19:09 +0530 Subject: [PATCH 03/38] add support to accept API server port from user --- pkg/cli/server.go | 6 +++++- pkg/http-server/constants.go | 6 ------ pkg/http-server/start.go | 19 ++++--------------- 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/pkg/cli/server.go b/pkg/cli/server.go index ef395ab53..be6de0091 100644 --- a/pkg/cli/server.go +++ b/pkg/cli/server.go @@ -22,6 +22,9 @@ import ( ) var ( + // Port at which API server will listen + Port string + // CertFile Certificate file path, required in order to enable secure HTTP server CertFile string @@ -43,11 +46,12 @@ Run Terrascan as an API server that inspects incoming IaC (Infrastructure-as-Cod } func server(cmd *cobra.Command, args []string) { - httpserver.Start(ConfigFile, CertFile, PrivateKeyFile) + httpserver.Start(Port, ConfigFile, CertFile, PrivateKeyFile) } func init() { serverCmd.Flags().StringVarP(&PrivateKeyFile, "key-path", "", "", "private key file path") serverCmd.Flags().StringVarP(&CertFile, "cert-path", "", "", "certificate file path") + serverCmd.Flags().StringVarP(&Port, "port", "p", httpserver.GatewayDefaultPort, "server port") RegisterCommand(rootCmd, serverCmd) } diff --git a/pkg/http-server/constants.go b/pkg/http-server/constants.go index 07097a4d3..2226cf49a 100644 --- a/pkg/http-server/constants.go +++ b/pkg/http-server/constants.go @@ -20,12 +20,6 @@ const ( // GatewayDefaultPort - default port at which the http server listens GatewayDefaultPort = "9010" - // GatewayDefaultPort - default port at which the https server listens - TLSGatewayDefaultPort = "9443" - // APIVersion - default api version for REST endpoints APIVersion = "v1" - - // TerrascanServerPort allows user to configure server at a port other than default - TerrascanServerPort = "TERRASCAN_SERVER_PORT" ) diff --git a/pkg/http-server/start.go b/pkg/http-server/start.go index de6d1463b..62940106b 100644 --- a/pkg/http-server/start.go +++ b/pkg/http-server/start.go @@ -28,24 +28,20 @@ import ( ) // Start initializes api routes and starts http server -func Start(configFile string, certFile string, privateKeyFile string) { +func Start(port, configFile, certFile, privateKeyFile string) { + // create a new API server server := NewAPIServer() // get all routes routes := server.Routes(configFile) - serverPort := os.Getenv(TerrascanServerPort) - if serverPort == "" { - serverPort = GatewayDefaultPort - } - // register routes and start the http server - server.start(routes, certFile, privateKeyFile) + server.start(routes, port, certFile, privateKeyFile) } // start http server -func (g *APIServer) start(routes []*Route, certFile string, privateKeyFile string) { +func (g *APIServer) start(routes []*Route, port, certFile, privateKeyFile string) { var ( err error @@ -65,13 +61,6 @@ func (g *APIServer) start(routes []*Route, certFile string, privateKeyFile strin // go/terrascan/asset is the path where the assets files are located inside the docker container router.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(http.Dir("/go/terrascan/assets")))) - var port = GatewayDefaultPort - - // In case a certificate file is specified, we run the server with a different port - if certFile != "" { - port = TLSGatewayDefaultPort - } - // start http server server := &http.Server{ Addr: ":" + port, From 0ecddb249b8c17c49159f31f6230986f91535098 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Sun, 14 Mar 2021 09:25:20 +0530 Subject: [PATCH 04/38] update API path for validating webhook --- pkg/http-server/routes.go | 4 ++-- pkg/http-server/webhook-scan.go | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pkg/http-server/routes.go b/pkg/http-server/routes.go index c7e86aee2..e0618a16d 100644 --- a/pkg/http-server/routes.go +++ b/pkg/http-server/routes.go @@ -36,10 +36,10 @@ func (g *APIServer) Routes(configFile string) []*Route { {verb: "POST", path: versionedPath("/{iac}/{iacVersion}/{cloud}/local/file/scan"), fn: h.scanFile}, {verb: "POST", path: versionedPath("/{iac}/{iacVersion}/{cloud}/remote/dir/scan"), fn: h.scanRemoteRepo}, - // K8s Webhook Routes + // k8s webhook Routes {verb: "GET", path: "/k8s/webhooks/{apiKey}/logs", fn: h.getLogs}, {verb: "GET", path: "/k8s/webhooks/{apiKey}/logs/{uid}", fn: h.getLogByUID}, - {verb: "POST", path: versionedPath("/k8s/webhooks/{apiKey}/scan"), fn: h.scanIncomingWebhook}, + {verb: "POST", path: versionedPath("/k8s/webhooks/{apiKey}/scan/validate"), fn: h.validateK8SWebhook}, } return routes diff --git a/pkg/http-server/webhook-scan.go b/pkg/http-server/webhook-scan.go index f98eef9e3..e77a9f3ce 100644 --- a/pkg/http-server/webhook-scan.go +++ b/pkg/http-server/webhook-scan.go @@ -19,6 +19,7 @@ package httpserver import ( "encoding/json" "fmt" + "github.com/accurics/terrascan/pkg/config" "github.com/accurics/terrascan/pkg/results" "github.com/accurics/terrascan/pkg/runtime" @@ -27,16 +28,18 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer" - "github.com/gorilla/mux" - "go.uber.org/zap" "io/ioutil" - v1 "k8s.io/api/admission/v1" "net/http" "os" "time" + + "github.com/gorilla/mux" + "go.uber.org/zap" + v1 "k8s.io/api/admission/v1" ) -func (g *APIHandler) scanIncomingWebhook(w http.ResponseWriter, r *http.Request) { +// validateK8SWebhook handles the incoming validating admission webhook from kubernetes API server +func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) { currentTime := time.Now() params := mux.Vars(r) From eae0b64824d612222d91a642896d5dcab013c479 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Sun, 14 Mar 2021 09:31:13 +0530 Subject: [PATCH 05/38] fix goimports for k8s webhook files --- .../webhook-deny-rule-matcher_test.go | 3 ++- pkg/http-server/webhook-scan-logger.go | 3 ++- pkg/http-server/webhook-scan.go | 16 +++++++--------- pkg/http-server/webhook-scan_test.go | 5 +++-- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/pkg/http-server/webhook-deny-rule-matcher_test.go b/pkg/http-server/webhook-deny-rule-matcher_test.go index 117ce8fbb..dff5cb598 100644 --- a/pkg/http-server/webhook-deny-rule-matcher_test.go +++ b/pkg/http-server/webhook-deny-rule-matcher_test.go @@ -1,9 +1,10 @@ package httpserver import ( + "testing" + "github.com/accurics/terrascan/pkg/config" "github.com/accurics/terrascan/pkg/results" - "testing" ) func TestDenyRuleMatcher(t *testing.T) { diff --git a/pkg/http-server/webhook-scan-logger.go b/pkg/http-server/webhook-scan-logger.go index 9cea0d532..32d5d6bd3 100644 --- a/pkg/http-server/webhook-scan-logger.go +++ b/pkg/http-server/webhook-scan-logger.go @@ -2,9 +2,10 @@ package httpserver import ( "database/sql" - "go.uber.org/zap" "os" "time" + + "go.uber.org/zap" ) type WebhookScanLogger struct { diff --git a/pkg/http-server/webhook-scan.go b/pkg/http-server/webhook-scan.go index e77a9f3ce..d68a2b930 100644 --- a/pkg/http-server/webhook-scan.go +++ b/pkg/http-server/webhook-scan.go @@ -19,23 +19,21 @@ package httpserver import ( "encoding/json" "fmt" - - "github.com/accurics/terrascan/pkg/config" - "github.com/accurics/terrascan/pkg/results" - "github.com/accurics/terrascan/pkg/runtime" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtimeK8s "k8s.io/apimachinery/pkg/runtime" - - "k8s.io/apimachinery/pkg/runtime/serializer" - "io/ioutil" "net/http" "os" "time" + "github.com/accurics/terrascan/pkg/config" + "github.com/accurics/terrascan/pkg/results" + "github.com/accurics/terrascan/pkg/runtime" "github.com/gorilla/mux" "go.uber.org/zap" + v1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtimeK8s "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" ) // validateK8SWebhook handles the incoming validating admission webhook from kubernetes API server diff --git a/pkg/http-server/webhook-scan_test.go b/pkg/http-server/webhook-scan_test.go index c7e85016a..dddfd2adc 100644 --- a/pkg/http-server/webhook-scan_test.go +++ b/pkg/http-server/webhook-scan_test.go @@ -4,13 +4,14 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/gorilla/mux" "io/ioutil" - v1 "k8s.io/api/admission/v1" "net/http" "net/http/httptest" "os" "testing" + + "github.com/gorilla/mux" + v1 "k8s.io/api/admission/v1" ) func TestUWebhooks(t *testing.T) { From 8df76933dc9b93cf15112b02721aa916a88411eb Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Sun, 14 Mar 2021 11:49:10 +0530 Subject: [PATCH 06/38] add support for reading port from env variable --- pkg/http-server/constants.go | 3 +++ pkg/http-server/start.go | 5 +++++ pkg/http-server/webhook-scan_test.go | 2 +- test/e2e/help/golden/help_server.txt | 7 +++++-- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pkg/http-server/constants.go b/pkg/http-server/constants.go index 2226cf49a..b0a10227a 100644 --- a/pkg/http-server/constants.go +++ b/pkg/http-server/constants.go @@ -22,4 +22,7 @@ const ( // APIVersion - default api version for REST endpoints APIVersion = "v1" + + // TerrascanServerPort allows user to configure server at a port other than default + TerrascanServerPort = "TERRASCAN_SERVER_PORT" ) diff --git a/pkg/http-server/start.go b/pkg/http-server/start.go index 62940106b..397d5f9d7 100644 --- a/pkg/http-server/start.go +++ b/pkg/http-server/start.go @@ -36,6 +36,11 @@ func Start(port, configFile, certFile, privateKeyFile string) { // get all routes routes := server.Routes(configFile) + // get port + if os.Getenv(TerrascanServerPort) != "" { + port = os.Getenv(TerrascanServerPort) + } + // register routes and start the http server server.start(routes, port, certFile, privateKeyFile) } diff --git a/pkg/http-server/webhook-scan_test.go b/pkg/http-server/webhook-scan_test.go index dddfd2adc..6e1d7eddb 100644 --- a/pkg/http-server/webhook-scan_test.go +++ b/pkg/http-server/webhook-scan_test.go @@ -203,7 +203,7 @@ func TestUWebhooks(t *testing.T) { res := httptest.NewRecorder() // new api handler h := &APIHandler{test: true, configFile: tt.configFile} - h.scanIncomingWebhook(res, req) + h.validateK8SWebhook(res, req) if res.Code != tt.wantStatus { t.Errorf("incorrect status code, got: '%v', want: '%v', error: '%v'", res.Code, tt.wantStatus, res.Body) diff --git a/test/e2e/help/golden/help_server.txt b/test/e2e/help/golden/help_server.txt index e1bfdaa55..303a60ee4 100644 --- a/test/e2e/help/golden/help_server.txt +++ b/test/e2e/help/golden/help_server.txt @@ -6,10 +6,13 @@ Usage: terrascan server [flags] Flags: - -h, --help help for server + --cert-path string certificate file path + -h, --help help for server + --key-path string private key file path + -p, --port string server port (default "9010") Global Flags: -c, --config-path string config file path -l, --log-level string log level (debug, info, warn, error, panic, fatal) (default "info") -x, --log-type string log output type (console, json) (default "console") - -o, --output string output type (human, json, yaml, xml, junit-xml) (default "human") + -o, --output string output type (human, json, yaml, xml, junit-xml) (default "human") \ No newline at end of file From 3a2479501b133e811d8bd00332dc57c641d2e515 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Sun, 14 Mar 2021 15:30:37 +0530 Subject: [PATCH 07/38] refactor to move k8s specific methods into a dedicated package --- pkg/http-server/webhook-deny-rule-matcher.go | 35 ---- pkg/http-server/webhook-scan.go | 150 ++----------- pkg/k8s/admission-webhook/interface.go | 34 +++ .../admission-webhook/validating-webhook.go | 198 ++++++++++++++++++ 4 files changed, 253 insertions(+), 164 deletions(-) delete mode 100644 pkg/http-server/webhook-deny-rule-matcher.go create mode 100644 pkg/k8s/admission-webhook/interface.go create mode 100644 pkg/k8s/admission-webhook/validating-webhook.go diff --git a/pkg/http-server/webhook-deny-rule-matcher.go b/pkg/http-server/webhook-deny-rule-matcher.go deleted file mode 100644 index 4becf0be9..000000000 --- a/pkg/http-server/webhook-deny-rule-matcher.go +++ /dev/null @@ -1,35 +0,0 @@ -package httpserver - -import ( - "github.com/accurics/terrascan/pkg/config" - "github.com/accurics/terrascan/pkg/results" - "github.com/accurics/terrascan/pkg/utils" -) - -type webhookDenyRuleMatcher struct { -} - -// This class should check if one of the violations found is relevant for the specified K8s deny rules -func (g *webhookDenyRuleMatcher) match(violation results.Violation, denyRules config.K8sDenyRules) bool { - if &denyRules == nil { - return false - } - - // Currently we support: - // 1. A minimum severity level - // 2. A category list - // In case one of the conditions is met, we return true. (We perform an OR between the rules) - if len(denyRules.DeniedSeverity) > 0 && utils.CheckSeverity(violation.Severity, denyRules.DeniedSeverity) { - return true - } - - if denyRules.Categories != nil { - for _, category := range denyRules.Categories { - if category == violation.Category { - return true - } - } - } - - return false -} diff --git a/pkg/http-server/webhook-scan.go b/pkg/http-server/webhook-scan.go index d68a2b930..c24d95493 100644 --- a/pkg/http-server/webhook-scan.go +++ b/pkg/http-server/webhook-scan.go @@ -24,7 +24,7 @@ import ( "os" "time" - "github.com/accurics/terrascan/pkg/config" + admissionWebhook "github.com/accurics/terrascan/pkg/k8s/admission-webhook" "github.com/accurics/terrascan/pkg/results" "github.com/accurics/terrascan/pkg/runtime" "github.com/gorilla/mux" @@ -32,16 +32,16 @@ import ( v1 "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtimeK8s "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer" ) // validateK8SWebhook handles the incoming validating admission webhook from kubernetes API server func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) { - currentTime := time.Now() - params := mux.Vars(r) - apiKey := params["apiKey"] + var ( + currentTime = time.Now() + params = mux.Vars(r) + apiKey = params["apiKey"] + ) // Validate if authorized (API key is specified and matched the server one (saved in an environment variable) if !g.validateAuthorization(apiKey, w) { @@ -49,57 +49,43 @@ func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) } // Read the request into byte array - bytesRequestAdmissionReview, err := ioutil.ReadAll(r.Body) + payload, err := ioutil.ReadAll(r.Body) if err != nil { - msg := fmt.Sprintf("Failed to read admission review: '%v'", err) + msg := fmt.Sprintf("failed to read validating admission webhook body, error: '%v'", err) apiErrorResponse(w, msg, http.StatusBadRequest) return } - zap.S().Debugf("scanning configuration webhook request: %+v", string(bytesRequestAdmissionReview)) + zap.S().Debugf("scanning configuration webhook request: %+v", string(payload)) - // Unmarshal the byte array into a v1.AdmissionReview object - requestedAdmissionReview, err := g.deserializeAdmissionReviewRequest(bytesRequestAdmissionReview) - if err != nil { - apiErrorResponse(w, err.Error(), http.StatusBadRequest) - return - } - - // In case the object is nil => an operation of DELETE happened, just return 'allow' since there is nothing to check - if len(requestedAdmissionReview.Request.Object.Raw) < 1 { - g.sendResponseAdmissionReview(w, *requestedAdmissionReview, true, nil, "") - return - } + // create a new validating webhook processor + validatingWebhook := admissionWebhook.NewValidatingWebhook(g.configFile) - // Save the object into a temp file for the policy engines - tempFile, err := g.writeObjectToTempFile(requestedAdmissionReview.Request.Object.Raw) - defer os.Remove(tempFile.Name()) + // decode incoming admission review request + requestedAdmissionReview, err := validatingWebhook.DecodeAdmissionReviewRequest(payload) if err != nil { - apiErrorResponse(w, err.Error(), http.StatusInternalServerError) + apiErrorResponse(w, err.Error(), http.StatusBadRequest) return } - // Run the policy engines - output, err := g.executeEngines(*tempFile) + // process the admission review request + output, allowed, denyViolations, err := validatingWebhook.ProcessWebhook(requestedAdmissionReview) if err != nil { apiErrorResponse(w, err.Error(), http.StatusInternalServerError) return } - // Calculate if there are anydeny violations - denyViolations, err := g.getDenyViolations(*output) - allowed := len(denyViolations) < 1 logPath := g.getLogPath(r.Host, apiKey, string(requestedAdmissionReview.Request.UID)) // Log the request in the DB - err = g.logWebhook(*output, string(requestedAdmissionReview.Request.UID), bytesRequestAdmissionReview, denyViolations, currentTime, allowed) + err = g.logWebhook(*output, string(requestedAdmissionReview.Request.UID), payload, denyViolations, currentTime, allowed) if err != nil { apiErrorResponse(w, err.Error(), http.StatusInternalServerError) return } // Send the correct response according to the result - g.sendResponseAdmissionReview(w, *requestedAdmissionReview, allowed, output, logPath) + g.sendResponseAdmissionReview(w, requestedAdmissionReview, allowed, output, logPath) } func (g *APIHandler) validateAuthorization(apiKey string, w http.ResponseWriter) bool { @@ -110,15 +96,15 @@ func (g *APIHandler) validateAuthorization(apiKey string, w http.ResponseWriter) return false } - savedApiKey := os.Getenv("K8S_WEBHOOK_API_KEY") - if len(savedApiKey) < 1 { + saveAPIKey := os.Getenv("K8S_WEBHOOK_API_KEY") + if len(saveAPIKey) < 1 { msg := "K8S_WEBHOOK_API_KEY environment variable MUST be declared" zap.S().Error(msg) apiErrorResponse(w, msg, http.StatusInternalServerError) return false } - if apiKey != savedApiKey { + if apiKey != saveAPIKey { msg := "Invalid apiKey" zap.S().Error(msg) apiErrorResponse(w, msg, http.StatusUnauthorized) @@ -128,78 +114,6 @@ func (g *APIHandler) validateAuthorization(apiKey string, w http.ResponseWriter) return true } -func (g *APIHandler) getDeniedViolations(violations results.ViolationStore, denyRules config.K8sDenyRules) []*results.Violation { - // Check whether one of the violations matches the deny violations configuration - - var denyViolations []*results.Violation - - denyRuleMatcher := webhookDenyRuleMatcher{} - - for _, violation := range violations.Violations { - if denyRuleMatcher.match(*violation, denyRules) { - denyViolations = append(denyViolations, violation) - } - } - - return denyViolations -} - -func (g *APIHandler) writeObjectToTempFile(objectBytes []byte) (*os.File, error) { - tempFile, err := ioutil.TempFile("", "terrascan-*.json") - if err != nil { - zap.S().Errorf("failed to create temp file: '%v'", err) - return nil, err - } - - zap.S().Debugf("created temp config file at '%s'", tempFile.Name()) - - _, err = tempFile.Write(objectBytes) - if err != nil { - zap.S().Errorf("failed to write object to temp file: '%v'", err) - return nil, err - } - - return tempFile, nil -} - -func (g *APIHandler) executeEngines(tempFile os.File) (*runtime.Output, error) { - var executor *runtime.Executor - var err error - if g.test { - executor, err = runtime.NewExecutor("k8s", "v1", []string{"k8s"}, - tempFile.Name(), "", g.configFile, []string{"./k8s_testdata/testpolicies"}, []string{}, []string{}, []string{}, "") - } else { - executor, err = runtime.NewExecutor("k8s", "v1", []string{"k8s"}, - tempFile.Name(), "", g.configFile, []string{}, []string{}, []string{}, []string{}, "") - } - - if err != nil { - zap.S().Errorf("failed to create runtime executer: '%v'", err) - return nil, err - } - - result, err := executor.Execute() - if err != nil { - zap.S().Error("failed to scan resource object. error: '%v'", err) - return nil, err - } - - return &result, nil -} - -func (g *APIHandler) getDenyViolations(output runtime.Output) ([]*results.Violation, error) { - // Calcualte the deny violations according to the configuration specified in the config file - configReader, err := config.NewTerrascanConfigReader(g.configFile) - if err != nil { - zap.S().Errorf("error loading config file: '%v'", err) - return nil, err - } - - denyViolations := g.getDeniedViolations(*output.Violations.ViolationStore, configReader.GetK8sDenyRules()) - - return denyViolations, nil -} - func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, requestedAdmissionReview v1.AdmissionReview, allowed bool, @@ -274,25 +188,3 @@ func (g *APIHandler) logWebhook(output runtime.Output, return nil } - -func (g *APIHandler) deserializeAdmissionReviewRequest(bytesAdmissionReview []byte) (*v1.AdmissionReview, error) { - var scheme = runtimeK8s.NewScheme() - v1.AddToScheme(scheme) - - var codecs = serializer.NewCodecFactory(scheme) - deserializer := codecs.UniversalDeserializer() - - obj, _, err := deserializer.Decode(bytesAdmissionReview, nil, nil) - if err != nil { - zap.S().Errorf("Request could not be decoded: %v", err) - return nil, err - } - - requestedAdmissionReview, ok := obj.(*v1.AdmissionReview) - if !ok { - zap.S().Errorf("Failed to deserialize request body to v1.AdmissionReview. Obj: %v", obj) - return nil, err - } - - return requestedAdmissionReview, nil -} diff --git a/pkg/k8s/admission-webhook/interface.go b/pkg/k8s/admission-webhook/interface.go new file mode 100644 index 000000000..99e435acb --- /dev/null +++ b/pkg/k8s/admission-webhook/interface.go @@ -0,0 +1,34 @@ +/* + Copyright (C) 2020 Accurics, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package admissionwebhook + +import ( + admissionv1 "k8s.io/api/admission/v1" +) + +// AdmissionWebhook interface needs to be implemented by all k8s admission +// webhooks i.e validating and mutating webhooks +type AdmissionWebhook interface { + + // DecodeAdmissionReviewRequest reads the incoming admission request body + // and decodes it into an AdmissionReviewRequest struct + DecodeAdmissionReviewRequest(payload []byte) (admissionv1.AdmissionReview, error) + + // ProcessWebhook processes the incoming AdmissionReview and creates + // a AdmissionResponse + ProcessWebhook(admissionv1.AdmissionReview) (admissionv1.AdmissionReview, error) +} diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go new file mode 100644 index 000000000..24de8e6e7 --- /dev/null +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -0,0 +1,198 @@ +/* + Copyright (C) 2020 Accurics, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package admissionwebhook + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/accurics/terrascan/pkg/config" + "github.com/accurics/terrascan/pkg/results" + "github.com/accurics/terrascan/pkg/runtime" + "github.com/accurics/terrascan/pkg/utils" + "go.uber.org/zap" + admissionv1 "k8s.io/api/admission/v1" + runtimeK8s "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" +) + +// ValidatingWebhook handles the incoming validating admission webhook from +// the kubernetes API server and decides whether the admission request from +// the kubernetes client should be allowed or not +type ValidatingWebhook struct { + configFile string +} + +// NewValidatingWebhook returns a new, empty ValidatingWebhook struct +func NewValidatingWebhook(configFile string) *ValidatingWebhook { + return &ValidatingWebhook{configFile: configFile} +} + +// DecodeAdmissionReviewRequest reads the incoming admission request body, +// decodes it and returns an AdmissionReviewRequest struct +func (w *ValidatingWebhook) DecodeAdmissionReviewRequest(payload []byte) (admissionv1.AdmissionReview, error) { + + var ( + scheme = runtimeK8s.NewScheme() + codecs = serializer.NewCodecFactory(scheme) + deserializer = codecs.UniversalDeserializer() + requestedAdmissionReview admissionv1.AdmissionReview + ) + admissionv1.AddToScheme(scheme) + + // decode incoming admission request + _, _, err := deserializer.Decode(payload, nil, &requestedAdmissionReview) + if err != nil { + errMsg := "failed to decode validating admission webhook payload" + zap.S().Error(errMsg, zap.Error(err)) + return requestedAdmissionReview, fmt.Errorf("%s, error: %w", errMsg, err) + } + + return requestedAdmissionReview, nil +} + +// ProcessWebhook processes the incoming AdmissionReview and creates +// a response +func (w *ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) (output *runtime.Output, allowed bool, denyViolations []*results.Violation, err error) { + + // In case the object is nil => an operation of DELETE happened, just return 'allow' since there is nothing to check + if len(review.Request.Object.Raw) < 1 { + zap.S().Info("recieved empty validating admission review request", zap.Any("admission review object", review)) + return output, true, denyViolations, nil + } + + // Save the object into a temp file for the policy engines + tempFile, err := w.writeObjectToTempFile(review.Request.Object.Raw) + defer os.Remove(tempFile.Name()) + if err != nil { + msg := "failed to create temp file for validating admission review request" + zap.S().Error(msg, zap.Error(err)) + return output, true, denyViolations, fmt.Errorf("%s; error: %w", msg, err) + } + + // Run the policy engines + output, err = w.executeEngines(*tempFile) + if err != nil { + msg := "failed to evaluate terrascan policies" + zap.S().Errorf(msg, zap.Error(err)) + return output, allowed, denyViolations, fmt.Errorf("%s; error: %w", msg, err) + } + + // Calculate if there are anydeny violations + denyViolations, err = w.getDenyViolations(*output) + allowed = len(denyViolations) < 1 + + return output, allowed, denyViolations, nil +} + +func (w *ValidatingWebhook) executeEngines(tempFile os.File) (*runtime.Output, error) { + var executor *runtime.Executor + var err error + + executor, err = runtime.NewExecutor("k8s", "v1", []string{"k8s"}, + tempFile.Name(), "", w.configFile, []string{}, []string{}, []string{}, []string{}, "") + + if err != nil { + zap.S().Errorf("failed to create runtime executer: '%v'", err) + return nil, err + } + + result, err := executor.Execute() + if err != nil { + zap.S().Error("failed to scan resource object. error: '%v'", err) + return nil, err + } + + return &result, nil +} + +func (w *ValidatingWebhook) getDenyViolations(output runtime.Output) ([]*results.Violation, error) { + + // Calcualte the deny violations according to the configuration specified in the config file + configReader, err := config.NewTerrascanConfigReader(w.configFile) + if err != nil { + zap.S().Errorf("error loading config file: '%v'", err) + return nil, err + } + + denyViolations := w.getDeniedViolations(*output.Violations.ViolationStore, configReader.GetK8sDenyRules()) + + return denyViolations, nil +} + +func (w *ValidatingWebhook) getDeniedViolations(violations results.ViolationStore, denyRules config.K8sDenyRules) []*results.Violation { + // Check whether one of the violations matches the deny violations configuration + + var denyViolations []*results.Violation + + denyRuleMatcher := webhookDenyRuleMatcher{} + + for _, violation := range violations.Violations { + if denyRuleMatcher.match(*violation, denyRules) { + denyViolations = append(denyViolations, violation) + } + } + + return denyViolations +} + +type webhookDenyRuleMatcher struct { +} + +// This class should check if one of the violations found is relevant for the specified K8s deny rules +func (g *webhookDenyRuleMatcher) match(violation results.Violation, denyRules config.K8sDenyRules) bool { + if &denyRules == nil { + return false + } + + // Currently we support: + // 1. A minimum severity level + // 2. A category list + // In case one of the conditions is met, we return true. (We perform an OR between the rules) + if len(denyRules.DeniedSeverity) > 0 && utils.CheckSeverity(violation.Severity, denyRules.DeniedSeverity) { + return true + } + + if denyRules.Categories != nil { + for _, category := range denyRules.Categories { + if category == violation.Category { + return true + } + } + } + + return false +} + +func (w *ValidatingWebhook) writeObjectToTempFile(objectBytes []byte) (*os.File, error) { + tempFile, err := ioutil.TempFile("", "terrascan-*.json") + if err != nil { + zap.S().Errorf("failed to create temp file: '%v'", err) + return nil, err + } + + zap.S().Debugf("created temp config file at '%s'", tempFile.Name()) + + _, err = tempFile.Write(objectBytes) + if err != nil { + zap.S().Errorf("failed to write object to temp file: '%v'", err) + return nil, err + } + + return tempFile, nil +} From ad8f796c117a9b932e4fe7ce44740ff77347eac6 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Sun, 14 Mar 2021 20:26:17 +0530 Subject: [PATCH 08/38] remove API key from webhook logs url --- pkg/http-server/routes.go | 4 +- pkg/http-server/webhook-scan-logs.go | 19 ++----- pkg/http-server/webhook-scan.go | 50 ++++++------------- pkg/k8s/admission-webhook/interface.go | 7 ++- .../admission-webhook/validating-webhook.go | 31 ++++++++++++ 5 files changed, 58 insertions(+), 53 deletions(-) diff --git a/pkg/http-server/routes.go b/pkg/http-server/routes.go index e0618a16d..74f9a179b 100644 --- a/pkg/http-server/routes.go +++ b/pkg/http-server/routes.go @@ -37,8 +37,8 @@ func (g *APIServer) Routes(configFile string) []*Route { {verb: "POST", path: versionedPath("/{iac}/{iacVersion}/{cloud}/remote/dir/scan"), fn: h.scanRemoteRepo}, // k8s webhook Routes - {verb: "GET", path: "/k8s/webhooks/{apiKey}/logs", fn: h.getLogs}, - {verb: "GET", path: "/k8s/webhooks/{apiKey}/logs/{uid}", fn: h.getLogByUID}, + {verb: "GET", path: "/k8s/webhooks/logs", fn: h.getLogs}, + {verb: "GET", path: "/k8s/webhooks/logs/{uid}", fn: h.getLogByUID}, {verb: "POST", path: versionedPath("/k8s/webhooks/{apiKey}/scan/validate"), fn: h.validateK8SWebhook}, } diff --git a/pkg/http-server/webhook-scan-logs.go b/pkg/http-server/webhook-scan-logs.go index e3227d7fc..07bfb3484 100644 --- a/pkg/http-server/webhook-scan-logs.go +++ b/pkg/http-server/webhook-scan-logs.go @@ -63,15 +63,8 @@ type webhookDisplayedShowLog struct { } func (g *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) { - // Return an HTML page including all the logs history - - params := mux.Vars(r) - - apiKey := params["apiKey"] - if !g.validateAuthorization(apiKey, w) { - return - } + // Return an HTML page including all the logs history logger := WebhookScanLogger{ test: g.test, } @@ -92,7 +85,7 @@ func (g *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) { logsData = append(logsData, webhookDisplayedIndexScanLog{ CreatedAt: log.CreatedAt, Status: g.getLogStatus(log), - LogUrl: g.getLogPath(r.Host, apiKey, log.UID), + LogUrl: g.getLogPath(r.Host, log.UID), Reasoning: g.getLogReasoning(log), Request: g.getLogRequest(log), }) @@ -106,10 +99,6 @@ func (g *APIHandler) getLogByUID(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - if !g.validateAuthorization(params["apiKey"], w) { - return - } - var uid = params["uid"] if len(uid) < 1 { apiErrorResponse(w, "Log UID is missing", http.StatusBadRequest) @@ -147,9 +136,9 @@ func (g *APIHandler) getLogByUID(w http.ResponseWriter, r *http.Request) { t.Execute(w, displayedScanLog) } -func (g *APIHandler) getLogPath(host string, apiKey string, logUID string) string { +func (g *APIHandler) getLogPath(host, logUID string) string { // Use this as the link to show the a specific log - return fmt.Sprintf("https://%v/k8s/webhooks/%v/logs/%v", host, apiKey, logUID) + return fmt.Sprintf("https://%v/k8s/webhooks/logs/%v", host, logUID) } func (g *APIHandler) getLogStatus(log webhookScanLog) string { diff --git a/pkg/http-server/webhook-scan.go b/pkg/http-server/webhook-scan.go index c24d95493..a85fcafa9 100644 --- a/pkg/http-server/webhook-scan.go +++ b/pkg/http-server/webhook-scan.go @@ -21,10 +21,10 @@ import ( "fmt" "io/ioutil" "net/http" - "os" "time" admissionWebhook "github.com/accurics/terrascan/pkg/k8s/admission-webhook" + admissionwebhook "github.com/accurics/terrascan/pkg/k8s/admission-webhook" "github.com/accurics/terrascan/pkg/results" "github.com/accurics/terrascan/pkg/runtime" "github.com/gorilla/mux" @@ -38,13 +38,22 @@ import ( func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) { var ( - currentTime = time.Now() - params = mux.Vars(r) - apiKey = params["apiKey"] + currentTime = time.Now() + params = mux.Vars(r) + apiKey = params["apiKey"] + validatingWebhook = admissionWebhook.NewValidatingWebhook(g.configFile) ) // Validate if authorized (API key is specified and matched the server one (saved in an environment variable) - if !g.validateAuthorization(apiKey, w) { + if err := validatingWebhook.Authorize(apiKey); err != nil { + switch err { + case admissionWebhook.ErrAPIKeyMissing: + apiErrorResponse(w, err.Error(), http.StatusBadRequest) + case admissionwebhook.ErrAPIKeyEnvNotSet: + apiErrorResponse(w, err.Error(), http.StatusInternalServerError) + case admissionWebhook.ErrUnAuthorized: + apiErrorResponse(w, err.Error(), http.StatusUnauthorized) + } return } @@ -58,9 +67,6 @@ func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) zap.S().Debugf("scanning configuration webhook request: %+v", string(payload)) - // create a new validating webhook processor - validatingWebhook := admissionWebhook.NewValidatingWebhook(g.configFile) - // decode incoming admission review request requestedAdmissionReview, err := validatingWebhook.DecodeAdmissionReviewRequest(payload) if err != nil { @@ -75,7 +81,7 @@ func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) return } - logPath := g.getLogPath(r.Host, apiKey, string(requestedAdmissionReview.Request.UID)) + logPath := g.getLogPath(r.Host, string(requestedAdmissionReview.Request.UID)) // Log the request in the DB err = g.logWebhook(*output, string(requestedAdmissionReview.Request.UID), payload, denyViolations, currentTime, allowed) @@ -88,32 +94,6 @@ func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) g.sendResponseAdmissionReview(w, requestedAdmissionReview, allowed, output, logPath) } -func (g *APIHandler) validateAuthorization(apiKey string, w http.ResponseWriter) bool { - if len(apiKey) < 1 { - msg := "apiKey is missing" - zap.S().Error(msg) - apiErrorResponse(w, msg, http.StatusBadRequest) - return false - } - - saveAPIKey := os.Getenv("K8S_WEBHOOK_API_KEY") - if len(saveAPIKey) < 1 { - msg := "K8S_WEBHOOK_API_KEY environment variable MUST be declared" - zap.S().Error(msg) - apiErrorResponse(w, msg, http.StatusInternalServerError) - return false - } - - if apiKey != saveAPIKey { - msg := "Invalid apiKey" - zap.S().Error(msg) - apiErrorResponse(w, msg, http.StatusUnauthorized) - return false - } - - return true -} - func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, requestedAdmissionReview v1.AdmissionReview, allowed bool, diff --git a/pkg/k8s/admission-webhook/interface.go b/pkg/k8s/admission-webhook/interface.go index 99e435acb..07b826cd8 100644 --- a/pkg/k8s/admission-webhook/interface.go +++ b/pkg/k8s/admission-webhook/interface.go @@ -17,6 +17,8 @@ package admissionwebhook import ( + "github.com/accurics/terrascan/pkg/results" + "github.com/accurics/terrascan/pkg/runtime" admissionv1 "k8s.io/api/admission/v1" ) @@ -24,11 +26,14 @@ import ( // webhooks i.e validating and mutating webhooks type AdmissionWebhook interface { + // Authorize checks if the incoming webhooks have valid apiKey + Authorize(apiKey string) error + // DecodeAdmissionReviewRequest reads the incoming admission request body // and decodes it into an AdmissionReviewRequest struct DecodeAdmissionReviewRequest(payload []byte) (admissionv1.AdmissionReview, error) // ProcessWebhook processes the incoming AdmissionReview and creates // a AdmissionResponse - ProcessWebhook(admissionv1.AdmissionReview) (admissionv1.AdmissionReview, error) + ProcessWebhook(review admissionv1.AdmissionReview) (*runtime.Output, bool, []*results.Violation, error) } diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index 24de8e6e7..24f59b0dd 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -43,6 +43,37 @@ func NewValidatingWebhook(configFile string) *ValidatingWebhook { return &ValidatingWebhook{configFile: configFile} } +var ( + ErrAPIKeyMissing = fmt.Errorf("apiKey is missing in validating admission webhook url") + ErrAPIKeyEnvNotSet = fmt.Errorf("variable K8S_WEBHOOK_API_KEY not set in terrascan server environment") + ErrUnAuthorized = fmt.Errorf("invalid API key in validating admission webhook url") +) + +// Authorize checks if the incoming webhooks have valid apiKey +func (w *ValidatingWebhook) Authorize(apiKey string) error { + + // check if key exists in API request + if len(apiKey) < 1 { + zap.S().Error(ErrAPIKeyMissing) + return ErrAPIKeyMissing + } + + // API key not set in terrascan env + saveAPIKey := os.Getenv("K8S_WEBHOOK_API_KEY") + if len(saveAPIKey) < 1 { + zap.S().Error(ErrAPIKeyEnvNotSet) + return ErrAPIKeyEnvNotSet + } + + // invalid api key + if apiKey != saveAPIKey { + zap.S().Error(ErrUnAuthorized) + return ErrUnAuthorized + } + + return nil +} + // DecodeAdmissionReviewRequest reads the incoming admission request body, // decodes it and returns an AdmissionReviewRequest struct func (w *ValidatingWebhook) DecodeAdmissionReviewRequest(payload []byte) (admissionv1.AdmissionReview, error) { From 1d5e6bc368624aff8ab2470efd228e93d543cd31 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Sun, 14 Mar 2021 21:46:16 +0530 Subject: [PATCH 09/38] use AdmissionWebhook interface --- .../admission-webhook/validating-webhook.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index 24f59b0dd..42c2f44f9 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -39,8 +39,8 @@ type ValidatingWebhook struct { } // NewValidatingWebhook returns a new, empty ValidatingWebhook struct -func NewValidatingWebhook(configFile string) *ValidatingWebhook { - return &ValidatingWebhook{configFile: configFile} +func NewValidatingWebhook(configFile string) AdmissionWebhook { + return ValidatingWebhook{configFile: configFile} } var ( @@ -50,7 +50,7 @@ var ( ) // Authorize checks if the incoming webhooks have valid apiKey -func (w *ValidatingWebhook) Authorize(apiKey string) error { +func (w ValidatingWebhook) Authorize(apiKey string) error { // check if key exists in API request if len(apiKey) < 1 { @@ -76,7 +76,7 @@ func (w *ValidatingWebhook) Authorize(apiKey string) error { // DecodeAdmissionReviewRequest reads the incoming admission request body, // decodes it and returns an AdmissionReviewRequest struct -func (w *ValidatingWebhook) DecodeAdmissionReviewRequest(payload []byte) (admissionv1.AdmissionReview, error) { +func (w ValidatingWebhook) DecodeAdmissionReviewRequest(payload []byte) (admissionv1.AdmissionReview, error) { var ( scheme = runtimeK8s.NewScheme() @@ -99,7 +99,7 @@ func (w *ValidatingWebhook) DecodeAdmissionReviewRequest(payload []byte) (admiss // ProcessWebhook processes the incoming AdmissionReview and creates // a response -func (w *ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) (output *runtime.Output, allowed bool, denyViolations []*results.Violation, err error) { +func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) (output *runtime.Output, allowed bool, denyViolations []*results.Violation, err error) { // In case the object is nil => an operation of DELETE happened, just return 'allow' since there is nothing to check if len(review.Request.Object.Raw) < 1 { @@ -131,7 +131,7 @@ func (w *ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) ( return output, allowed, denyViolations, nil } -func (w *ValidatingWebhook) executeEngines(tempFile os.File) (*runtime.Output, error) { +func (w ValidatingWebhook) executeEngines(tempFile os.File) (*runtime.Output, error) { var executor *runtime.Executor var err error @@ -152,7 +152,7 @@ func (w *ValidatingWebhook) executeEngines(tempFile os.File) (*runtime.Output, e return &result, nil } -func (w *ValidatingWebhook) getDenyViolations(output runtime.Output) ([]*results.Violation, error) { +func (w ValidatingWebhook) getDenyViolations(output runtime.Output) ([]*results.Violation, error) { // Calcualte the deny violations according to the configuration specified in the config file configReader, err := config.NewTerrascanConfigReader(w.configFile) @@ -166,7 +166,7 @@ func (w *ValidatingWebhook) getDenyViolations(output runtime.Output) ([]*results return denyViolations, nil } -func (w *ValidatingWebhook) getDeniedViolations(violations results.ViolationStore, denyRules config.K8sDenyRules) []*results.Violation { +func (w ValidatingWebhook) getDeniedViolations(violations results.ViolationStore, denyRules config.K8sDenyRules) []*results.Violation { // Check whether one of the violations matches the deny violations configuration var denyViolations []*results.Violation @@ -210,7 +210,7 @@ func (g *webhookDenyRuleMatcher) match(violation results.Violation, denyRules co return false } -func (w *ValidatingWebhook) writeObjectToTempFile(objectBytes []byte) (*os.File, error) { +func (w ValidatingWebhook) writeObjectToTempFile(objectBytes []byte) (*os.File, error) { tempFile, err := ioutil.TempFile("", "terrascan-*.json") if err != nil { zap.S().Errorf("failed to create temp file: '%v'", err) From c91a31c7b33d041633e7128bbf5e5bb27145a211 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Mon, 15 Mar 2021 09:59:05 +0530 Subject: [PATCH 10/38] override default server port with port from env variable --- pkg/http-server/start.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/http-server/start.go b/pkg/http-server/start.go index 397d5f9d7..ab7cf24c4 100644 --- a/pkg/http-server/start.go +++ b/pkg/http-server/start.go @@ -37,7 +37,7 @@ func Start(port, configFile, certFile, privateKeyFile string) { routes := server.Routes(configFile) // get port - if os.Getenv(TerrascanServerPort) != "" { + if port == GatewayDefaultPort && os.Getenv(TerrascanServerPort) != "" { port = os.Getenv(TerrascanServerPort) } From 814b623a50d083dcb7e7fb154f547215e9fa9dbd Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Mon, 15 Mar 2021 10:53:46 +0530 Subject: [PATCH 11/38] fixing typos, handling pointers, refactoring some code --- pkg/http-server/webhook-scan.go | 39 +++++------ pkg/k8s/admission-webhook/interface.go | 2 +- .../admission-webhook/validating-webhook.go | 67 +++++++------------ pkg/utils/tempfile.go | 44 ++++++++++++ 4 files changed, 89 insertions(+), 63 deletions(-) create mode 100644 pkg/utils/tempfile.go diff --git a/pkg/http-server/webhook-scan.go b/pkg/http-server/webhook-scan.go index a85fcafa9..e81d9dbd4 100644 --- a/pkg/http-server/webhook-scan.go +++ b/pkg/http-server/webhook-scan.go @@ -24,7 +24,6 @@ import ( "time" admissionWebhook "github.com/accurics/terrascan/pkg/k8s/admission-webhook" - admissionwebhook "github.com/accurics/terrascan/pkg/k8s/admission-webhook" "github.com/accurics/terrascan/pkg/results" "github.com/accurics/terrascan/pkg/runtime" "github.com/gorilla/mux" @@ -49,26 +48,26 @@ func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) switch err { case admissionWebhook.ErrAPIKeyMissing: apiErrorResponse(w, err.Error(), http.StatusBadRequest) - case admissionwebhook.ErrAPIKeyEnvNotSet: - apiErrorResponse(w, err.Error(), http.StatusInternalServerError) - case admissionWebhook.ErrUnAuthorized: + case admissionWebhook.ErrUnauthorized: apiErrorResponse(w, err.Error(), http.StatusUnauthorized) + default: + apiErrorResponse(w, err.Error(), http.StatusInternalServerError) } return } // Read the request into byte array - payload, err := ioutil.ReadAll(r.Body) + body, err := ioutil.ReadAll(r.Body) if err != nil { - msg := fmt.Sprintf("failed to read validating admission webhook body, error: '%v'", err) + msg := fmt.Sprintf("failed to read validating admission webhook request body, error: '%v'", err) apiErrorResponse(w, msg, http.StatusBadRequest) return } - zap.S().Debugf("scanning configuration webhook request: %+v", string(payload)) + zap.S().Debugf("scanning configuration webhook request: %+v", string(body)) // decode incoming admission review request - requestedAdmissionReview, err := validatingWebhook.DecodeAdmissionReviewRequest(payload) + requestedAdmissionReview, err := validatingWebhook.DecodeAdmissionReviewRequest(body) if err != nil { apiErrorResponse(w, err.Error(), http.StatusBadRequest) return @@ -84,7 +83,7 @@ func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) logPath := g.getLogPath(r.Host, string(requestedAdmissionReview.Request.UID)) // Log the request in the DB - err = g.logWebhook(*output, string(requestedAdmissionReview.Request.UID), payload, denyViolations, currentTime, allowed) + err = g.logWebhook(output, string(requestedAdmissionReview.Request.UID), body, denyViolations, currentTime, allowed) if err != nil { apiErrorResponse(w, err.Error(), http.StatusInternalServerError) return @@ -97,7 +96,7 @@ func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, requestedAdmissionReview v1.AdmissionReview, allowed bool, - output *runtime.Output, + output runtime.Output, logPath string) { responseAdmissionReview := &v1.AdmissionReview{} responseAdmissionReview.SetGroupVersionKind(requestedAdmissionReview.GroupVersionKind()) @@ -107,17 +106,15 @@ func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, Allowed: allowed, } - if output != nil { - // Means we ran the engines and we have results - if allowed { - if len(output.Violations.ViolationStore.Violations) > 0 { - // In case there are no denial violations, just return the log URL as a warning - responseAdmissionReview.Response.Warnings = []string{logPath} - } - } else { - // In case the request was denied, return 403 and the log URL as an error message - responseAdmissionReview.Response.Result = &metav1.Status{Message: logPath, Code: 403} + // Means we ran the engines and we have results + if allowed { + if len(output.Violations.ViolationStore.Violations) > 0 { + // In case there are no denial violations, just return the log URL as a warning + responseAdmissionReview.Response.Warnings = []string{logPath} } + } else { + // In case the request was denied, return 403 and the log URL as an error message + responseAdmissionReview.Response.Result = &metav1.Status{Message: logPath, Code: 403} } respBytes, err := json.Marshal(responseAdmissionReview) @@ -135,7 +132,7 @@ func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, func (g *APIHandler) logWebhook(output runtime.Output, uid string, bytesAdmissionReview []byte, - denyViolations []*results.Violation, + denyViolations []results.Violation, currentTime time.Time, allowed bool) error { var deniedViolationsEncoded string diff --git a/pkg/k8s/admission-webhook/interface.go b/pkg/k8s/admission-webhook/interface.go index 07b826cd8..3b66859c0 100644 --- a/pkg/k8s/admission-webhook/interface.go +++ b/pkg/k8s/admission-webhook/interface.go @@ -35,5 +35,5 @@ type AdmissionWebhook interface { // ProcessWebhook processes the incoming AdmissionReview and creates // a AdmissionResponse - ProcessWebhook(review admissionv1.AdmissionReview) (*runtime.Output, bool, []*results.Violation, error) + ProcessWebhook(review admissionv1.AdmissionReview) (runtime.Output, bool, []results.Violation, error) } diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index 42c2f44f9..0a84a972c 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -18,7 +18,6 @@ package admissionwebhook import ( "fmt" - "io/ioutil" "os" "github.com/accurics/terrascan/pkg/config" @@ -46,7 +45,7 @@ func NewValidatingWebhook(configFile string) AdmissionWebhook { var ( ErrAPIKeyMissing = fmt.Errorf("apiKey is missing in validating admission webhook url") ErrAPIKeyEnvNotSet = fmt.Errorf("variable K8S_WEBHOOK_API_KEY not set in terrascan server environment") - ErrUnAuthorized = fmt.Errorf("invalid API key in validating admission webhook url") + ErrUnauthorized = fmt.Errorf("invalid API key in validating admission webhook url") ) // Authorize checks if the incoming webhooks have valid apiKey @@ -67,8 +66,8 @@ func (w ValidatingWebhook) Authorize(apiKey string) error { // invalid api key if apiKey != saveAPIKey { - zap.S().Error(ErrUnAuthorized) - return ErrUnAuthorized + zap.S().Error(ErrUnauthorized) + return ErrUnauthorized } return nil @@ -76,7 +75,7 @@ func (w ValidatingWebhook) Authorize(apiKey string) error { // DecodeAdmissionReviewRequest reads the incoming admission request body, // decodes it and returns an AdmissionReviewRequest struct -func (w ValidatingWebhook) DecodeAdmissionReviewRequest(payload []byte) (admissionv1.AdmissionReview, error) { +func (w ValidatingWebhook) DecodeAdmissionReviewRequest(requestBody []byte) (admissionv1.AdmissionReview, error) { var ( scheme = runtimeK8s.NewScheme() @@ -87,9 +86,9 @@ func (w ValidatingWebhook) DecodeAdmissionReviewRequest(payload []byte) (admissi admissionv1.AddToScheme(scheme) // decode incoming admission request - _, _, err := deserializer.Decode(payload, nil, &requestedAdmissionReview) + _, _, err := deserializer.Decode(requestBody, nil, &requestedAdmissionReview) if err != nil { - errMsg := "failed to decode validating admission webhook payload" + errMsg := "failed to decode validating admission webhook request body" zap.S().Error(errMsg, zap.Error(err)) return requestedAdmissionReview, fmt.Errorf("%s, error: %w", errMsg, err) } @@ -99,7 +98,7 @@ func (w ValidatingWebhook) DecodeAdmissionReviewRequest(payload []byte) (admissi // ProcessWebhook processes the incoming AdmissionReview and creates // a response -func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) (output *runtime.Output, allowed bool, denyViolations []*results.Violation, err error) { +func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) (output runtime.Output, allowed bool, denyViolations []results.Violation, err error) { // In case the object is nil => an operation of DELETE happened, just return 'allow' since there is nothing to check if len(review.Request.Object.Raw) < 1 { @@ -108,7 +107,7 @@ func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) (o } // Save the object into a temp file for the policy engines - tempFile, err := w.writeObjectToTempFile(review.Request.Object.Raw) + tempFile, err := utils.CreateTempFile(review.Request.Object.Raw, "json") defer os.Remove(tempFile.Name()) if err != nil { msg := "failed to create temp file for validating admission review request" @@ -117,7 +116,7 @@ func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) (o } // Run the policy engines - output, err = w.executeEngines(*tempFile) + output, err = w.scanK8sFile(tempFile.Name()) if err != nil { msg := "failed to evaluate terrascan policies" zap.S().Errorf(msg, zap.Error(err)) @@ -125,34 +124,38 @@ func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) (o } // Calculate if there are anydeny violations - denyViolations, err = w.getDenyViolations(*output) + denyViolations, err = w.getDenyViolations(output) allowed = len(denyViolations) < 1 return output, allowed, denyViolations, nil } -func (w ValidatingWebhook) executeEngines(tempFile os.File) (*runtime.Output, error) { - var executor *runtime.Executor - var err error +func (w ValidatingWebhook) scanK8sFile(filePath string) (runtime.Output, error) { + + var ( + executor *runtime.Executor + err error + result runtime.Output + ) executor, err = runtime.NewExecutor("k8s", "v1", []string{"k8s"}, - tempFile.Name(), "", w.configFile, []string{}, []string{}, []string{}, []string{}, "") + filePath, "", w.configFile, []string{}, []string{}, []string{}, []string{}, "") if err != nil { zap.S().Errorf("failed to create runtime executer: '%v'", err) - return nil, err + return result, err } - result, err := executor.Execute() + result, err = executor.Execute() if err != nil { zap.S().Error("failed to scan resource object. error: '%v'", err) - return nil, err + return result, err } - return &result, nil + return result, nil } -func (w ValidatingWebhook) getDenyViolations(output runtime.Output) ([]*results.Violation, error) { +func (w ValidatingWebhook) getDenyViolations(output runtime.Output) ([]results.Violation, error) { // Calcualte the deny violations according to the configuration specified in the config file configReader, err := config.NewTerrascanConfigReader(w.configFile) @@ -166,16 +169,16 @@ func (w ValidatingWebhook) getDenyViolations(output runtime.Output) ([]*results. return denyViolations, nil } -func (w ValidatingWebhook) getDeniedViolations(violations results.ViolationStore, denyRules config.K8sDenyRules) []*results.Violation { +func (w ValidatingWebhook) getDeniedViolations(violations results.ViolationStore, denyRules config.K8sDenyRules) []results.Violation { // Check whether one of the violations matches the deny violations configuration - var denyViolations []*results.Violation + var denyViolations []results.Violation denyRuleMatcher := webhookDenyRuleMatcher{} for _, violation := range violations.Violations { if denyRuleMatcher.match(*violation, denyRules) { - denyViolations = append(denyViolations, violation) + denyViolations = append(denyViolations, *violation) } } @@ -209,21 +212,3 @@ func (g *webhookDenyRuleMatcher) match(violation results.Violation, denyRules co return false } - -func (w ValidatingWebhook) writeObjectToTempFile(objectBytes []byte) (*os.File, error) { - tempFile, err := ioutil.TempFile("", "terrascan-*.json") - if err != nil { - zap.S().Errorf("failed to create temp file: '%v'", err) - return nil, err - } - - zap.S().Debugf("created temp config file at '%s'", tempFile.Name()) - - _, err = tempFile.Write(objectBytes) - if err != nil { - zap.S().Errorf("failed to write object to temp file: '%v'", err) - return nil, err - } - - return tempFile, nil -} diff --git a/pkg/utils/tempfile.go b/pkg/utils/tempfile.go new file mode 100644 index 000000000..d4e37c2aa --- /dev/null +++ b/pkg/utils/tempfile.go @@ -0,0 +1,44 @@ +/* + Copyright (C) 2020 Accurics, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package utils + +import ( + "fmt" + "io/ioutil" + "os" + + "go.uber.org/zap" +) + +// CreateTempFile creates a file with provided contents in the temp directory +func CreateTempFile(content []byte, ext string) (*os.File, error) { + tempFile, err := ioutil.TempFile("", fmt.Sprintf("terrascan-*.%s", ext)) + if err != nil { + zap.S().Errorf("failed to create temp file: '%v'", err) + return nil, err + } + + zap.S().Debugf("created temp config file at '%s'", tempFile.Name()) + + _, err = tempFile.Write(content) + if err != nil { + zap.S().Errorf("failed to write to temp file: '%v'", err) + return nil, err + } + + return tempFile, nil +} From fc640ece12dfbdadc9f125ddfb200c1006f9b3d1 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Mon, 15 Mar 2021 17:34:14 +0530 Subject: [PATCH 12/38] fixing unit tests --- ...upport-Admission-Controller-Webhooks.patch | 13107 ---------------- pkg/http-server/webhook-scan.go | 27 +- pkg/http-server/webhook-scan_test.go | 2 +- .../admission-webhook/validating-webhook.go | 11 +- .../webhook-deny-rule-matcher_test.go | 18 +- 5 files changed, 42 insertions(+), 13123 deletions(-) delete mode 100644 0001-K8S-Support-Admission-Controller-Webhooks.patch rename pkg/{http-server => k8s/admission-webhook}/webhook-deny-rule-matcher_test.go (80%) diff --git a/0001-K8S-Support-Admission-Controller-Webhooks.patch b/0001-K8S-Support-Admission-Controller-Webhooks.patch deleted file mode 100644 index fe3a1e5aa..000000000 --- a/0001-K8S-Support-Admission-Controller-Webhooks.patch +++ /dev/null @@ -1,13107 +0,0 @@ -From c64cc0c7d05a0fff5c2dc112ba7104f01788f87d Mon Sep 17 00:00:00 2001 -From: Shay Ya'ari -Date: Tue, 2 Mar 2021 11:01:05 +0200 -Subject: [PATCH] K8S Support Admission Controller Webhooks - -Use K8s Admission Review Objects - -Comments ---- - .gitignore | 5 +- - build/Dockerfile | 7 +- - deploy/docker-compose.yml | 1 + - .../admission-controller-webhooks-usage.md | 108 + - go.mod | 3 + - go.sum | 8 +- - pkg/cli/server.go | 12 +- - pkg/config/config-reader.go | 5 + - pkg/config/types.go | 9 +- - pkg/http-server/assets/icons.svg | 1 + - pkg/http-server/assets/jsonTree.css | 107 + - pkg/http-server/assets/jsonTree.js | 819 +++ - pkg/http-server/assets/moment.js | 5670 +++++++++++++++++ - pkg/http-server/assets/webhook-scan-logs.css | 33 + - pkg/http-server/assets/webhook-scan-logs.js | 44 + - pkg/http-server/constants.go | 3 + - pkg/http-server/handler.go | 7 +- - pkg/http-server/handler_test.go | 6 +- - pkg/http-server/health_test.go | 2 +- - .../k8s_testdata/config-deny-category.toml | 5 + - .../k8s_testdata/config-deny-high.toml | 5 + - .../config-deny-non-existing-category.toml | 8 + - .../k8s_testdata/config-medium-severity.toml | 2 + - .../k8s_testdata/config-specific-rule.toml | 5 + - pkg/http-server/k8s_testdata/empty.json | 0 - .../k8s_testdata/empty_object.json | 7 + - pkg/http-server/k8s_testdata/invalid.json | 1 + - .../k8s_testdata/risky_testconfig.json | 27 + - pkg/http-server/k8s_testdata/testconfig.json | 27 + - .../kubernetes_pod/AC-K8-CA-PO-H-0165.json | 21 + - .../kubernetes_pod/AC-K8-DS-PO-M-0176.json | 14 + - .../kubernetes_pod/AC-K8-DS-PO-M-0177.json | 14 + - .../kubernetes_pod/AC-K8-IA-PO-H-0106.json | 14 + - .../kubernetes_pod/AC-K8-IA-PO-H-0137.json | 14 + - .../kubernetes_pod/AC-K8-IA-PO-H-0138.json | 14 + - .../kubernetes_pod/AC-K8-IA-PO-H-0168.json | 21 + - .../kubernetes_pod/AC-K8-IA-PO-M-0105.json | 14 + - .../kubernetes_pod/AC-K8-IA-PO-M-0135.json | 14 + - .../kubernetes_pod/AC-K8-IA-PO-M-0139.json | 14 + - .../kubernetes_pod/AC-K8-IA-PO-M-0140.json | 21 + - .../kubernetes_pod/AC-K8-IA-PO-M-0141.json | 14 + - .../kubernetes_pod/AC-K8-IA-PO-M-0143.json | 22 + - .../kubernetes_pod/AC-K8-IA-PO-M-0162.json | 16 + - .../kubernetes_pod/AC-K8-IA-PS-M-0112.json | 15 + - .../kubernetes_pod/AC-K8-NS-PO-H-0117.json | 14 + - .../kubernetes_pod/AC-K8-NS-PO-H-0170.json | 14 + - .../kubernetes_pod/AC-K8-NS-PO-M-0122.json | 14 + - .../kubernetes_pod/AC-K8-NS-PO-M-0133.json | 14 + - .../kubernetes_pod/AC-K8-NS-PO-M-0163.json | 16 + - .../kubernetes_pod/AC-K8-NS-PO-M-0164.json | 16 + - .../kubernetes_pod/AC-K8-NS-PO-M-0171.json | 17 + - .../kubernetes_pod/AC-K8-NS-PO-M-0182.json | 14 + - .../kubernetes_pod/AC-K8-OE-PK-M-0034.json | 19 + - .../kubernetes_pod/AC-K8-OE-PK-M-0155.json | 21 + - .../kubernetes_pod/AC-K8-OE-PK-M-0156.json | 21 + - .../kubernetes_pod/AC-K8-OE-PK-M-0157.json | 21 + - .../kubernetes_pod/AC-K8-OE-PK-M-0158.json | 21 + - .../kubernetes_pod/AC-K8-OE-PO-L-0129.json | 16 + - .../kubernetes_pod/AC-K8-OE-PO-L-0130.json | 16 + - .../kubernetes_pod/AC-K8-OE-PO-L-0134.json | 14 + - .../kubernetes_pod/AC-K8-OE-PO-M-0166.json | 14 + - .../kubernetes_pod/allowedHostPath.rego | 107 + - .../kubernetes_pod/allowedProcMount.rego | 126 + - .../kubernetes_pod/allowedVolumes.rego | 58 + - .../accurics.kubernetes.IAM.73.json | 16 + - .../accurics.kubernetes.IAM.74.json | 16 + - .../accurics.kubernetes.IAM.75.json | 16 + - .../accurics.kubernetes.IAM.76.json | 16 + - .../accurics.kubernetes.IAM.77.json | 16 + - .../accurics.kubernetes.IAM.78.json | 16 + - .../accurics.kubernetes.IAM.79.json | 16 + - .../accurics.kubernetes.IAM.80.json | 16 + - .../accurics.kubernetes.IAM.81.json | 16 + - .../accurics.kubernetes.IAM.82.json | 16 + - .../accurics.kubernetes.IAM.83.json | 16 + - .../accurics.kubernetes.IAM.84.json | 16 + - .../accurics.kubernetes.IAM.85.json | 16 + - .../accurics.kubernetes.IAM.86.json | 16 + - .../accurics.kubernetes.IAM.87.json | 16 + - .../accurics.kubernetes.IAM.88.json | 16 + - .../containerHasAllowedCapabilities.rego | 119 + - .../kubernetes_pod/appArmorProfile.rego | 108 + - .../kubernetes_pod/autoMountTokenEnabled.rego | 33 + - .../kubernetes_pod/capSysAdminUsed.rego | 69 + - .../kubernetes_pod/capabilityUsed.rego | 74 + - .../kubernetes_pod/commandCheck.rego | 19 + - .../accurics.kubernetes.IAM.105.json | 16 + - .../accurics.kubernetes.IAM.106.json | 16 + - .../accurics.kubernetes.IAM.108.json | 16 + - .../accurics.kubernetes.IAM.109.json | 16 + - .../accurics.kubernetes.IAM.110.json | 16 + - .../accurics.kubernetes.IAM.111.json | 16 + - .../accurics.kubernetes.IAM.112.json | 16 + - .../accurics.kubernetes.IAM.113.json | 16 + - .../accurics.kubernetes.IAM.114.json | 16 + - .../accurics.kubernetes.IAM.115.json | 16 + - .../accurics.kubernetes.IAM.116.json | 16 + - .../accurics.kubernetes.IAM.117.json | 16 + - .../accurics.kubernetes.IAM.118.json | 16 + - .../accurics.kubernetes.IAM.119.json | 16 + - .../accurics.kubernetes.IAM.120.json | 16 + - .../containerResourcesNotDefined.rego | 111 + - .../accurics.kubernetes.EKM.57.json | 16 + - .../accurics.kubernetes.EKM.58.json | 16 + - .../accurics.kubernetes.EKM.59.json | 16 + - .../accurics.kubernetes.EKM.60.json | 16 + - .../accurics.kubernetes.EKM.61.json | 16 + - .../accurics.kubernetes.EKM.62.json | 16 + - .../accurics.kubernetes.EKM.63.json | 16 + - .../accurics.kubernetes.EKM.64.json | 16 + - .../accurics.kubernetes.EKM.65.json | 16 + - .../accurics.kubernetes.EKM.66.json | 16 + - .../accurics.kubernetes.EKM.67.json | 16 + - .../accurics.kubernetes.EKM.68.json | 16 + - .../accurics.kubernetes.EKM.69.json | 16 + - .../accurics.kubernetes.EKM.70.json | 16 + - .../accurics.kubernetes.EKM.71.json | 16 + - .../accurics.kubernetes.EKM.72.json | 16 + - .../containerUsesSecretsInEnvironmentVar.rego | 115 + - .../kubernetes_pod/containersAsHighUID.rego | 102 + - .../kubernetes_pod/disallowedSysCalls.rego | 51 + - .../AC-K8-DS-PO-M-0143.json | 14 + - .../disallowed_volumes/disAllowedVolumes.rego | 52 + - .../kubernetes_pod/dockerSockCheck.rego | 35 + - .../kubernetes_pod/imageWithLatestTag.rego | 196 + - .../kubernetes_pod/imageWithoutDigest.rego | 105 + - .../kubernetes_pod/kubeDashboardEnabled.rego | 6 + - .../kubernetes_pod/otherNamespace.rego | 20 + - .../priviledgedContainersEnabled.rego | 11 + - .../kubernetes_pod/probeCheck.rego | 68 + - .../kubernetes_pod/secCompProfile.rego | 153 + - .../kubernetes_pod/secretsAsEnvVariables.rego | 75 + - .../kubernetes_pod/securityContextCheck.rego | 76 + - .../kubernetes_pod/securityContextUsed.rego | 103 + - .../kubernetes_pod/specBoolCheck.rego | 36 + - .../kubernetes_pod/tillerDeployed.rego | 35 + - pkg/http-server/routes.go | 10 +- - pkg/http-server/routes_test.go | 2 +- - pkg/http-server/server.go | 3 +- - pkg/http-server/start.go | 31 +- - pkg/http-server/templates/index.html | 34 + - pkg/http-server/templates/show.html | 40 + - pkg/http-server/webhook-deny-rule-matcher.go | 35 + - .../webhook-deny-rule-matcher_test.go | 98 + - pkg/http-server/webhook-scan-logger.go | 197 + - pkg/http-server/webhook-scan-logger_test.go | 77 + - pkg/http-server/webhook-scan-logs.go | 242 + - pkg/http-server/webhook-scan.go | 297 + - pkg/http-server/webhook-scan_test.go | 244 + - pkg/initialize/run.go | 2 +- - pkg/runtime/executor_test.go | 3 - - .../scan-skip-rules-low-severity.toml | 2 +- - 152 files changed, 11577 insertions(+), 30 deletions(-) - create mode 100644 docs/getting-started/admission-controller-webhooks-usage.md - create mode 100644 pkg/http-server/assets/icons.svg - create mode 100644 pkg/http-server/assets/jsonTree.css - create mode 100644 pkg/http-server/assets/jsonTree.js - create mode 100644 pkg/http-server/assets/moment.js - create mode 100644 pkg/http-server/assets/webhook-scan-logs.css - create mode 100644 pkg/http-server/assets/webhook-scan-logs.js - create mode 100644 pkg/http-server/k8s_testdata/config-deny-category.toml - create mode 100644 pkg/http-server/k8s_testdata/config-deny-high.toml - create mode 100644 pkg/http-server/k8s_testdata/config-deny-non-existing-category.toml - create mode 100644 pkg/http-server/k8s_testdata/config-medium-severity.toml - create mode 100644 pkg/http-server/k8s_testdata/config-specific-rule.toml - create mode 100644 pkg/http-server/k8s_testdata/empty.json - create mode 100644 pkg/http-server/k8s_testdata/empty_object.json - create mode 100644 pkg/http-server/k8s_testdata/invalid.json - create mode 100644 pkg/http-server/k8s_testdata/risky_testconfig.json - create mode 100644 pkg/http-server/k8s_testdata/testconfig.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json - create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json - create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json - create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego - create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json - create mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego - create mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego - create mode 100644 pkg/http-server/templates/index.html - create mode 100644 pkg/http-server/templates/show.html - create mode 100644 pkg/http-server/webhook-deny-rule-matcher.go - create mode 100644 pkg/http-server/webhook-deny-rule-matcher_test.go - create mode 100644 pkg/http-server/webhook-scan-logger.go - create mode 100644 pkg/http-server/webhook-scan-logger_test.go - create mode 100644 pkg/http-server/webhook-scan-logs.go - create mode 100644 pkg/http-server/webhook-scan.go - create mode 100644 pkg/http-server/webhook-scan_test.go - -diff --git a/.gitignore b/.gitignore -index fc0803b..5dc0e2f 100644 ---- a/.gitignore -+++ b/.gitignore -@@ -23,6 +23,9 @@ docs/_build/ - #vscode - .vscode/ - -+#GoLand -+.idea -+ - /updatedFiles - - # Go binar -@@ -30,4 +33,4 @@ docs/_build/ - - .DS_Store - --vendor/ -\ No newline at end of file -+vendor/ -diff --git a/build/Dockerfile b/build/Dockerfile -index 376c117..7e06ea8 100644 ---- a/build/Dockerfile -+++ b/build/Dockerfile -@@ -3,13 +3,14 @@ FROM golang:alpine AS builder - - ARG GOOS_VAL=linux - ARG GOARCH_VAL=amd64 --ARG CGO_ENABLED_VAL=0 -+ARG CGO_ENABLED_VAL=1 - - WORKDIR $GOPATH/src/terrascan - - # download go dependencies - COPY go.mod go.sum ./ - RUN go mod download -+RUN apk add -U build-base - - # copy terrascan source - COPY . . -@@ -32,6 +33,10 @@ USER terrascan - # copy terrascan binary from build - COPY --from=builder /go/bin/terrascan /go/bin/terrascan - -+# Copy webhooks UI templates & assets -+COPY ./pkg/http-server/templates /go/terrascan -+COPY ./pkg/http-server/assets /go/terrascan/assets -+ - EXPOSE 9010 - - ENTRYPOINT ["/go/bin/terrascan"] -diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml -index 9210f5c..6a430a5 100644 ---- a/deploy/docker-compose.yml -+++ b/deploy/docker-compose.yml -@@ -4,3 +4,4 @@ services: - image: accurics/terrascan:${TAG:-latest} - ports: - - 9010:9010 -+ - 443:9443 -diff --git a/docs/getting-started/admission-controller-webhooks-usage.md b/docs/getting-started/admission-controller-webhooks-usage.md -new file mode 100644 -index 0000000..5db33da ---- /dev/null -+++ b/docs/getting-started/admission-controller-webhooks-usage.md -@@ -0,0 +1,108 @@ -+# Using Terrascan as a Kubernetes Admission Controller -+ -+## Overview -+Terrascan can be integrated with K8s [admissions webhooks](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/). -+It can be used as one of the validating webhooks to be used and scan new configurations. -+ -+In this guide, we'll demonstrate how Terrascan can be configured to: -+* Scan configuration changes policies when an object is being created or updated -+* Allow / reject the request in case a violation is detected -+ -+ -+## Installation Guide -+ -+### Create an instance -+Your Terrascan instance has the following requirements for being able to scan K8s configurations. -+ -+1. Be accessible via HTTPS. Make sure your cloud firewall is configured to allow this. -+1. Have a valid SSL certificate for the served domain name. To do that, choose one of our suggested methods: -+ 1. Use a subdomain of your choosing (e.g dev-terrascan-k8s.accurics.com) and create a valid certificate for this subdomain through your SSL certificate provider. [Let's Encrypt](https://letsencrypt.org/) is a free, simple to use certificate authority you can use. -+ 1. Use a reverse-proxy to serve SSL requests; for example, use Cloudflare Flexible to get a certificate by a trusted-CA to your [self-signed certificate](https://www.digitalocean.com/community/tutorials/openssl-essentials-working-with-ssl-certificates-private-keys-and-csrs). -+ 1. Generate a self-signed certificate and have your K8s cluster trust it. To add a trusted CA to ca-pemstore, as demonstrated in [paraspatidar's blog post](https://medium.com/@paraspatidar/add-ssl-tls-certificate-or-pem-file-to-kubernetes-pod-s-trusted-root-ca-store-7bed5cd683d). -+1. Use the Terrascan docker as demonstrated in this document, or run it from the sources. -+ -+### Run Terrascan webhook service -+Run Terrascan docker image in your server using the following command: -+ ```bash -+ sudo docker run -p 443:9443 -v :/data -u root -e K8S_WEBHOOK_API_KEY=> accurics/terrascan server --cert-path /data/cert.pem --key-path /data/key.pem -+ ``` -+`` is a key used for authentication between your K8s environment and the Terrascan server. Generate your preferred key and use it here. -+ -+`` is a directory path in your server where both the certificate and the private key .pem files are stored. -+In addition, this directory is used to write save the webhook logs. (An SQLite file) -+ -+You can specify a config file that specifies which policies to use in the scan and which violations should lead to rejection. -+ -+A config file example: ```my_terrscan_config.toml``` -+ ```bash -+[severity] -+level = "medium" -+[rules] -+ skip-rules = [ -+ "accurics.kubernetes.IAM.107" -+ ] -+ -+[k8s-deny-rules] -+ denied-categories = [ -+ "Network Ports Security" -+ ] -+ denied-severity = "high" -+ ``` -+ -+You can specify the following configurations: -+* **scan-rules** - one or more rules to scan -+* **skip-rules** - one or more rules to skip while scanning -+* **severity** - the minimal level of severity of the policies to be scanned -+ -+ -+* **k8s-deny-rules** - specify the rules that should cause a rejection of the admission request -+ * **denied-categories** - one or more policy categories that are not allowed in the detected violations -+ * **denied-severity** - the minimal level of severity that should cause a rejection -+ -+In order to use a configuration file, add it as a command line argument: -+ -+``` -c /data/my_terrscan_config.toml``` -+ -+ -+### Configure K8s to send webhooks -+Configure a new ```ValidatingWebhookConfiguration``` in your Kubernetes environment and specify your Terrascan server endpoint. -+ -+Example: -+ ```bash -+ cat </v1/k8s/webhooks//scan -+ sideEffects: None -+ admissionReviewVersions: ["v1"] -+ EOF -+ ``` -+ -+* You can modify the `rules` that trigger the webhook according to your preferences. -+* Update the ```clientConfig``` URL with your terrascan server address and the API key you generated before. -+ -+ -+### Test your settings -+Try to run a new pod / service. For example: -+``` Bash -+ kubectl run mynginx --image=nginx -+``` -+ -+Go to ```https:///k8s/webhooks//logs``` and verify your request is logged. -diff --git a/go.mod b/go.mod -index c76b2a7..2d86ab4 100644 ---- a/go.mod -+++ b/go.mod -@@ -19,6 +19,7 @@ require ( - github.com/hashicorp/terraform v0.14.4 - github.com/iancoleman/strcase v0.1.3 - github.com/mattn/go-isatty v0.0.12 -+ github.com/mattn/go-sqlite3 v1.14.6 - github.com/mitchellh/go-homedir v1.1.0 - github.com/onsi/ginkgo v1.12.1 - github.com/onsi/gomega v1.10.5 -@@ -35,5 +36,7 @@ require ( - gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 - helm.sh/helm/v3 v3.4.0 - honnef.co/go/tools v0.1.1 // indirect -+ k8s.io/api v0.19.2 -+ k8s.io/apimachinery v0.19.2 - sigs.k8s.io/kustomize/api v0.7.2 - ) -diff --git a/go.sum b/go.sum -index c589491..6b0fbb0 100644 ---- a/go.sum -+++ b/go.sum -@@ -700,7 +700,10 @@ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp - github.com/mattn/go-shellwords v1.0.4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= - github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= - github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -+github.com/mattn/go-sqlite3 v1.12.0 h1:u/x3mp++qUxvYfulZ4HKOvVO0JWhk7HtE8lWhbGz/Do= - github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -+github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= -+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= - github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= - github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -@@ -1317,7 +1320,6 @@ golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWc - golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= - golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= - golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= --golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= - golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= - golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= - golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -@@ -1325,8 +1327,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc - golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= - golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= - golang.org/x/tools v0.0.0-20201028111035-eafbe7b904eb/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= --golang.org/x/tools v0.0.0-20210115202250-e0d201561e39 h1:BTs2GMGSMWpgtCpv1CE7vkJTv7XcHdcLLnAMu7UbgTY= --golang.org/x/tools v0.0.0-20210115202250-e0d201561e39/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= - golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= - golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= - golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -@@ -1485,8 +1485,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh - honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= - honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= - honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= --honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c= --honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM= - honnef.co/go/tools v0.1.1 h1:EVDuO03OCZwpV2t/tLLxPmPiomagMoBOgfPt0FM+4IY= - honnef.co/go/tools v0.1.1/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= - k8s.io/api v0.19.0 h1:XyrFIJqTYZJ2DU7FBE/bSPz7b1HvbVBuBf07oeo6eTc= -diff --git a/pkg/cli/server.go b/pkg/cli/server.go -index edb4627..ea6040e 100644 ---- a/pkg/cli/server.go -+++ b/pkg/cli/server.go -@@ -21,6 +21,14 @@ import ( - "github.com/spf13/cobra" - ) - -+var ( -+ // CertFile Certificate file path, required in order to enable secure HTTP server -+ CertFile string -+ -+ // PrivateKeyFile Private key file path, required in order to enable secure HTTP server -+ PrivateKeyFile string -+) -+ - var serverCmd = &cobra.Command{ - Use: "server", - Short: "Run Terrascan as an API server", -@@ -35,9 +43,11 @@ Run Terrascan as an API server that inspects incoming IaC (Infrastructure-as-Cod - } - - func server(cmd *cobra.Command, args []string) { -- httpserver.Start() -+ httpserver.Start(ConfigFile, CertFile, PrivateKeyFile) - } - - func init() { -+ rootCmd.PersistentFlags().StringVarP(&PrivateKeyFile, "key-path", "", "", "private key file path") -+ rootCmd.PersistentFlags().StringVarP(&CertFile, "cert-path", "", "", "certificate file path") - RegisterCommand(rootCmd, serverCmd) - } -diff --git a/pkg/config/config-reader.go b/pkg/config/config-reader.go -index f33edf0..f37821f 100644 ---- a/pkg/config/config-reader.go -+++ b/pkg/config/config-reader.go -@@ -87,3 +87,8 @@ func (r TerrascanConfigReader) GetRules() Rules { - func (r TerrascanConfigReader) GetSeverity() Severity { - return r.config.Severity - } -+ -+// GetK8sDenyRules will return the k8s deny rules specified in the terrascan config file -+func (r TerrascanConfigReader) GetK8sDenyRules() K8sDenyRules { -+ return r.config.K8sDenyRules -+} -diff --git a/pkg/config/types.go b/pkg/config/types.go -index 9c51446..c83744f 100644 ---- a/pkg/config/types.go -+++ b/pkg/config/types.go -@@ -16,7 +16,7 @@ - - package config - --// Global initalizes GlobalConfig struct -+// Global initializes GlobalConfig struct - var Global *TerrascanConfig = &TerrascanConfig{} - - // TerrascanConfig struct defines global variables/configurations across terrascan -@@ -25,6 +25,7 @@ type TerrascanConfig struct { - Notifications map[string]Notifier `toml:"notifications,omitempty"` - Rules `toml:"rules,omitempty"` - Severity `toml:"severity,omitempty"` -+ K8sDenyRules `toml:"k8s-deny-rules,omitempty"` - } - - // Severity defines the minimum level of severity of violations that you want to be reported -@@ -54,3 +55,9 @@ type Rules struct { - ScanRules []string `toml:"scan-rules,omitempty"` - SkipRules []string `toml:"skip-rules,omitempty"` - } -+ -+// K8s deny rules in the terrascan config file -+type K8sDenyRules struct { -+ DeniedSeverity string `toml:"denied-severity,omitempty"` -+ Categories []string `toml:"denied-categories,omitempty"` -+} -diff --git a/pkg/http-server/assets/icons.svg b/pkg/http-server/assets/icons.svg -new file mode 100644 -index 0000000..cc8298a ---- /dev/null -+++ b/pkg/http-server/assets/icons.svg -@@ -0,0 +1 @@ -+ -\ No newline at end of file -diff --git a/pkg/http-server/assets/jsonTree.css b/pkg/http-server/assets/jsonTree.css -new file mode 100644 -index 0000000..ad17484 ---- /dev/null -+++ b/pkg/http-server/assets/jsonTree.css -@@ -0,0 +1,107 @@ -+/* -+ * JSON Tree Viewer -+ * http://github.com/summerstyle/jsonTreeViewer -+ * -+ * Copyright 2017 Vera Lobacheva (http://iamvera.com) -+ * Released under the MIT license (LICENSE.txt) -+ */ -+ -+/* Background for the tree. May use for element */ -+.jsontree_bg { -+ background: #FFF; -+} -+ -+/* Styles for the container of the tree (e.g. fonts, margins etc.) */ -+.jsontree_tree { -+ /*margin-left: 30px;*/ -+ font-family: 'PT Mono', monospace; -+ font-size: 14px; -+} -+ -+/* Styles for a list of child nodes */ -+.jsontree_child-nodes { -+ display: none; -+ margin-left: 35px; -+ margin-bottom: 5px; -+ line-height: 2; -+} -+.jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_child-nodes { -+ display: block; -+} -+ -+/* Styles for labels */ -+.jsontree_label-wrapper { -+ float: left; -+ margin-right: 8px; -+} -+.jsontree_label { -+ font-weight: normal; -+ vertical-align: top; -+ color: #000; -+ position: relative; -+ padding: 1px; -+ border-radius: 4px; -+ cursor: default; -+} -+.jsontree_node_marked > .jsontree_label-wrapper > .jsontree_label { -+ background: #fff2aa; -+} -+ -+/* Styles for values */ -+.jsontree_value-wrapper { -+ display: block; -+ /*overflow: hidden;*/ -+} -+.jsontree_node_complex > .jsontree_value-wrapper { -+ overflow: inherit; -+} -+.jsontree_value { -+ vertical-align: top; -+ display: inline; -+} -+.jsontree_value_null { -+ color: #777; -+ font-weight: bold; -+} -+.jsontree_value_string { -+ color: #025900; -+ font-weight: bold; -+} -+.jsontree_value_number { -+ color: #000E59; -+ font-weight: bold; -+} -+.jsontree_value_boolean { -+ color: #600100; -+ font-weight: bold; -+} -+ -+/* Styles for active elements */ -+.jsontree_expand-button { -+ position: absolute; -+ top: 3px; -+ left: -15px; -+ display: block; -+ width: 11px; -+ height: 11px; -+ background-image: url('icons.svg'); -+} -+.jsontree_node_expanded > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button { -+ background-position: 0 -11px; -+} -+.jsontree_show-more { -+ cursor: pointer; -+} -+.jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more { -+ display: none; -+} -+.jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button, -+.jsontree_node_empty > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more { -+ display: none !important; -+} -+.jsontree_node_complex > .jsontree_label-wrapper > .jsontree_label { -+ cursor: pointer; -+} -+.jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label { -+ cursor: default !important; -+} -diff --git a/pkg/http-server/assets/jsonTree.js b/pkg/http-server/assets/jsonTree.js -new file mode 100644 -index 0000000..51bbf01 ---- /dev/null -+++ b/pkg/http-server/assets/jsonTree.js -@@ -0,0 +1,819 @@ -+/** -+ * JSON Tree library (a part of jsonTreeViewer) -+ * http://github.com/summerstyle/jsonTreeViewer -+ * -+ * Copyright 2017 Vera Lobacheva (http://iamvera.com) -+ * Released under the MIT license (LICENSE.txt) -+ */ -+ -+var jsonTree = (function() { -+ -+ /* ---------- Utilities ---------- */ -+ var utils = { -+ -+ /* -+ * Returns js-"class" of value -+ * -+ * @param val {any type} - value -+ * @returns {string} - for example, "[object Function]" -+ */ -+ getClass : function(val) { -+ return Object.prototype.toString.call(val); -+ }, -+ -+ /** -+ * Checks for a type of value (for valid JSON data types). -+ * In other cases - throws an exception -+ * -+ * @param val {any type} - the value for new node -+ * @returns {string} ("object" | "array" | "null" | "boolean" | "number" | "string") -+ */ -+ getType : function(val) { -+ if (val === null) { -+ return 'null'; -+ } -+ -+ switch (typeof val) { -+ case 'number': -+ return 'number'; -+ -+ case 'string': -+ return 'string'; -+ -+ case 'boolean': -+ return 'boolean'; -+ } -+ -+ switch(utils.getClass(val)) { -+ case '[object Array]': -+ return 'array'; -+ -+ case '[object Object]': -+ return 'object'; -+ } -+ -+ throw new Error('Bad type: ' + utils.getClass(val)); -+ }, -+ -+ /** -+ * Applies for each item of list some function -+ * and checks for last element of the list -+ * -+ * @param obj {Object | Array} - a list or a dict with child nodes -+ * @param func {Function} - the function for each item -+ */ -+ forEachNode : function(obj, func) { -+ var type = utils.getType(obj), -+ isLast; -+ -+ switch (type) { -+ case 'array': -+ isLast = obj.length - 1; -+ -+ obj.forEach(function(item, i) { -+ func(i, item, i === isLast); -+ }); -+ -+ break; -+ -+ case 'object': -+ var keys = Object.keys(obj).sort(); -+ -+ isLast = keys.length - 1; -+ -+ keys.forEach(function(item, i) { -+ func(item, obj[item], i === isLast); -+ }); -+ -+ break; -+ } -+ -+ }, -+ -+ /** -+ * Implements the kind of an inheritance by -+ * using parent prototype and -+ * creating intermediate constructor -+ * -+ * @param Child {Function} - a child constructor -+ * @param Parent {Function} - a parent constructor -+ */ -+ inherits : (function() { -+ var F = function() {}; -+ -+ return function(Child, Parent) { -+ F.prototype = Parent.prototype; -+ Child.prototype = new F(); -+ Child.prototype.constructor = Child; -+ }; -+ })(), -+ -+ /* -+ * Checks for a valid type of root node* -+ * -+ * @param {any type} jsonObj - a value for root node -+ * @returns {boolean} - true for an object or an array, false otherwise -+ */ -+ isValidRoot : function(jsonObj) { -+ switch (utils.getType(jsonObj)) { -+ case 'object': -+ case 'array': -+ return true; -+ default: -+ return false; -+ } -+ }, -+ -+ /** -+ * Extends some object -+ */ -+ extend : function(targetObj, sourceObj) { -+ for (var prop in sourceObj) { -+ if (sourceObj.hasOwnProperty(prop)) { -+ targetObj[prop] = sourceObj[prop]; -+ } -+ } -+ } -+ }; -+ -+ -+ /* ---------- Node constructors ---------- */ -+ -+ /** -+ * The factory for creating nodes of defined type. -+ * -+ * ~~~ Node ~~~ is a structure element of an onject or an array -+ * with own label (a key of an object or an index of an array) -+ * and value of any json data type. The root object or array -+ * is a node without label. -+ * {... -+ * [+] "label": value, -+ * ...} -+ * -+ * Markup: -+ *
      • -+ * -+ * -+ * -+ * "label" -+ * -+ * : -+ * -+ * <(div|span) class="jsontree_value jsontree_value_(object|array|boolean|null|number|string)"> -+ * ... -+ * -+ *
      • -+ * -+ * @param label {string} - key name -+ * @param val {Object | Array | string | number | boolean | null} - a value of node -+ * @param isLast {boolean} - true if node is last in list of siblings -+ * -+ * @return {Node} -+ */ -+ function Node(label, val, isLast) { -+ var nodeType = utils.getType(val); -+ -+ if (nodeType in Node.CONSTRUCTORS) { -+ return new Node.CONSTRUCTORS[nodeType](label, val, isLast); -+ } else { -+ throw new Error('Bad type: ' + utils.getClass(val)); -+ } -+ } -+ -+ Node.CONSTRUCTORS = { -+ 'boolean' : NodeBoolean, -+ 'number' : NodeNumber, -+ 'string' : NodeString, -+ 'null' : NodeNull, -+ 'object' : NodeObject, -+ 'array' : NodeArray -+ }; -+ -+ -+ /* -+ * The constructor for simple types (string, number, boolean, null) -+ * {... -+ * [+] "label": value, -+ * ...} -+ * value = string || number || boolean || null -+ * -+ * Markup: -+ *
      • -+ * -+ * "age" -+ * : -+ * -+ * 25 -+ * , -+ *
      • -+ * -+ * @abstract -+ * @param label {string} - key name -+ * @param val {string | number | boolean | null} - a value of simple types -+ * @param isLast {boolean} - true if node is last in list of parent childNodes -+ */ -+ function _NodeSimple(label, val, isLast) { -+ if (this.constructor === _NodeSimple) { -+ throw new Error('This is abstract class'); -+ } -+ -+ var self = this, -+ el = document.createElement('li'), -+ labelEl, -+ template = function(label, val) { -+ var str = '\ -+ \ -+ "' + -+ label + -+ '" : \ -+ \ -+ \ -+ ' + -+ val + -+ '' + -+ (!isLast ? ',' : '') + -+ ''; -+ -+ return str; -+ }; -+ -+ self.label = label; -+ self.isComplex = false; -+ -+ el.classList.add('jsontree_node'); -+ el.innerHTML = template(label, val); -+ -+ self.el = el; -+ -+ labelEl = el.querySelector('.jsontree_label'); -+ -+ labelEl.addEventListener('click', function(e) { -+ if (e.altKey) { -+ self.toggleMarked(); -+ return; -+ } -+ -+ if (e.shiftKey) { -+ document.getSelection().removeAllRanges(); -+ alert(self.getJSONPath()); -+ return; -+ } -+ }, false); -+ } -+ -+ _NodeSimple.prototype = { -+ constructor : _NodeSimple, -+ -+ /** -+ * Mark node -+ */ -+ mark : function() { -+ this.el.classList.add('jsontree_node_marked'); -+ }, -+ -+ /** -+ * Unmark node -+ */ -+ unmark : function() { -+ this.el.classList.remove('jsontree_node_marked'); -+ }, -+ -+ /** -+ * Mark or unmark node -+ */ -+ toggleMarked : function() { -+ this.el.classList.toggle('jsontree_node_marked'); -+ }, -+ -+ /** -+ * Expands parent node of this node -+ * -+ * @param isRecursive {boolean} - if true, expands all parent nodes -+ * (from node to root) -+ */ -+ expandParent : function(isRecursive) { -+ if (!this.parent) { -+ return; -+ } -+ -+ this.parent.expand(); -+ this.parent.expandParent(isRecursive); -+ }, -+ -+ /** -+ * Returns JSON-path of this -+ * -+ * @param isInDotNotation {boolean} - kind of notation for returned json-path -+ * (by default, in bracket notation) -+ * @returns {string} -+ */ -+ getJSONPath : function(isInDotNotation) { -+ if (this.isRoot) { -+ return "$"; -+ } -+ -+ var currentPath; -+ -+ if (this.parent.type === 'array') { -+ currentPath = "[" + this.label + "]"; -+ } else { -+ currentPath = isInDotNotation ? "." + this.label : "['" + this.label + "']"; -+ } -+ -+ return this.parent.getJSONPath(isInDotNotation) + currentPath; -+ } -+ }; -+ -+ -+ /* -+ * The constructor for boolean values -+ * {... -+ * [+] "label": boolean, -+ * ...} -+ * boolean = true || false -+ * -+ * @constructor -+ * @param label {string} - key name -+ * @param val {boolean} - value of boolean type, true or false -+ * @param isLast {boolean} - true if node is last in list of parent childNodes -+ */ -+ function NodeBoolean(label, val, isLast) { -+ this.type = "boolean"; -+ -+ _NodeSimple.call(this, label, val, isLast); -+ } -+ utils.inherits(NodeBoolean,_NodeSimple); -+ -+ -+ /* -+ * The constructor for number values -+ * {... -+ * [+] "label": number, -+ * ...} -+ * number = 123 -+ * -+ * @constructor -+ * @param label {string} - key name -+ * @param val {number} - value of number type, for example 123 -+ * @param isLast {boolean} - true if node is last in list of parent childNodes -+ */ -+ function NodeNumber(label, val, isLast) { -+ this.type = "number"; -+ -+ _NodeSimple.call(this, label, val, isLast); -+ } -+ utils.inherits(NodeNumber,_NodeSimple); -+ -+ -+ /* -+ * The constructor for string values -+ * {... -+ * [+] "label": string, -+ * ...} -+ * string = "abc" -+ * -+ * @constructor -+ * @param label {string} - key name -+ * @param val {string} - value of string type, for example "abc" -+ * @param isLast {boolean} - true if node is last in list of parent childNodes -+ */ -+ function NodeString(label, val, isLast) { -+ this.type = "string"; -+ -+ _NodeSimple.call(this, label, '"' + val + '"', isLast); -+ } -+ utils.inherits(NodeString,_NodeSimple); -+ -+ -+ /* -+ * The constructor for null values -+ * {... -+ * [+] "label": null, -+ * ...} -+ * -+ * @constructor -+ * @param label {string} - key name -+ * @param val {null} - value (only null) -+ * @param isLast {boolean} - true if node is last in list of parent childNodes -+ */ -+ function NodeNull(label, val, isLast) { -+ this.type = "null"; -+ -+ _NodeSimple.call(this, label, val, isLast); -+ } -+ utils.inherits(NodeNull,_NodeSimple); -+ -+ -+ /* -+ * The constructor for complex types (object, array) -+ * {... -+ * [+] "label": value, -+ * ...} -+ * value = object || array -+ * -+ * Markup: -+ *
      • -+ * -+ * -+ * -+ * "label" -+ * -+ * : -+ * -+ *
        -+ * { -+ *
          -+ * } -+ * , -+ *
        -+ *
      • -+ * -+ * @abstract -+ * @param label {string} - key name -+ * @param val {Object | Array} - a value of complex types, object or array -+ * @param isLast {boolean} - true if node is last in list of parent childNodes -+ */ -+ function _NodeComplex(label, val, isLast) { -+ if (this.constructor === _NodeComplex) { -+ throw new Error('This is abstract class'); -+ } -+ -+ var self = this, -+ el = document.createElement('li'), -+ template = function(label, sym) { -+ var comma = (!isLast) ? ',' : '', -+ str = '\ -+
        \ -+
        \ -+ ' + sym[0] + '\ -+ \ -+
          \ -+ ' + sym[1] + '' + -+ '
          ' + comma + -+ '
          '; -+ -+ if (label !== null) { -+ str = '\ -+ \ -+ ' + -+ '' + -+ '"' + label + -+ '" : \ -+ ' + str; -+ } -+ -+ return str; -+ }, -+ childNodesUl, -+ labelEl, -+ moreContentEl, -+ childNodes = []; -+ -+ self.label = label; -+ self.isComplex = true; -+ -+ el.classList.add('jsontree_node'); -+ el.classList.add('jsontree_node_complex'); -+ el.innerHTML = template(label, self.sym); -+ -+ childNodesUl = el.querySelector('.jsontree_child-nodes'); -+ -+ if (label !== null) { -+ labelEl = el.querySelector('.jsontree_label'); -+ moreContentEl = el.querySelector('.jsontree_show-more'); -+ -+ labelEl.addEventListener('click', function(e) { -+ if (e.altKey) { -+ self.toggleMarked(); -+ return; -+ } -+ -+ if (e.shiftKey) { -+ document.getSelection().removeAllRanges(); -+ alert(self.getJSONPath()); -+ return; -+ } -+ -+ self.toggle(e.ctrlKey || e.metaKey); -+ }, false); -+ -+ moreContentEl.addEventListener('click', function(e) { -+ self.toggle(e.ctrlKey || e.metaKey); -+ }, false); -+ -+ self.isRoot = false; -+ } else { -+ self.isRoot = true; -+ self.parent = null; -+ -+ el.classList.add('jsontree_node_expanded'); -+ } -+ -+ self.el = el; -+ self.childNodes = childNodes; -+ self.childNodesUl = childNodesUl; -+ -+ utils.forEachNode(val, function(label, node, isLast) { -+ self.addChild(new Node(label, node, isLast)); -+ }); -+ -+ self.isEmpty = !Boolean(childNodes.length); -+ if (self.isEmpty) { -+ el.classList.add('jsontree_node_empty'); -+ } -+ } -+ -+ utils.inherits(_NodeComplex, _NodeSimple); -+ -+ utils.extend(_NodeComplex.prototype, { -+ constructor : _NodeComplex, -+ -+ /* -+ * Add child node to list of child nodes -+ * -+ * @param child {Node} - child node -+ */ -+ addChild : function(child) { -+ this.childNodes.push(child); -+ this.childNodesUl.appendChild(child.el); -+ child.parent = this; -+ }, -+ -+ /* -+ * Expands this list of node child nodes -+ * -+ * @param isRecursive {boolean} - if true, expands all child nodes -+ */ -+ expand : function(isRecursive){ -+ if (this.isEmpty) { -+ return; -+ } -+ -+ if (!this.isRoot) { -+ this.el.classList.add('jsontree_node_expanded'); -+ } -+ -+ if (isRecursive) { -+ this.childNodes.forEach(function(item, i) { -+ if (item.isComplex) { -+ item.expand(isRecursive); -+ } -+ }); -+ } -+ }, -+ -+ /* -+ * Collapses this list of node child nodes -+ * -+ * @param isRecursive {boolean} - if true, collapses all child nodes -+ */ -+ collapse : function(isRecursive) { -+ if (this.isEmpty) { -+ return; -+ } -+ -+ if (!this.isRoot) { -+ this.el.classList.remove('jsontree_node_expanded'); -+ } -+ -+ if (isRecursive) { -+ this.childNodes.forEach(function(item, i) { -+ if (item.isComplex) { -+ item.collapse(isRecursive); -+ } -+ }); -+ } -+ }, -+ -+ /* -+ * Expands collapsed or collapses expanded node -+ * -+ * @param {boolean} isRecursive - Expand all child nodes if this node is expanded -+ * and collapse it otherwise -+ */ -+ toggle : function(isRecursive) { -+ if (this.isEmpty) { -+ return; -+ } -+ -+ this.el.classList.toggle('jsontree_node_expanded'); -+ -+ if (isRecursive) { -+ var isExpanded = this.el.classList.contains('jsontree_node_expanded'); -+ -+ this.childNodes.forEach(function(item, i) { -+ if (item.isComplex) { -+ item[isExpanded ? 'expand' : 'collapse'](isRecursive); -+ } -+ }); -+ } -+ }, -+ -+ /** -+ * Find child nodes that match some conditions and handle it -+ * -+ * @param {Function} matcher -+ * @param {Function} handler -+ * @param {boolean} isRecursive -+ */ -+ findChildren : function(matcher, handler, isRecursive) { -+ if (this.isEmpty) { -+ return; -+ } -+ -+ this.childNodes.forEach(function(item, i) { -+ if (matcher(item)) { -+ handler(item); -+ } -+ -+ if (item.isComplex && isRecursive) { -+ item.findChildren(matcher, handler, isRecursive); -+ } -+ }); -+ } -+ }); -+ -+ -+ /* -+ * The constructor for object values -+ * {... -+ * [+] "label": object, -+ * ...} -+ * object = {"abc": "def"} -+ * -+ * @constructor -+ * @param label {string} - key name -+ * @param val {Object} - value of object type, {"abc": "def"} -+ * @param isLast {boolean} - true if node is last in list of siblings -+ */ -+ function NodeObject(label, val, isLast) { -+ this.sym = ['{', '}']; -+ this.type = "object"; -+ -+ _NodeComplex.call(this, label, val, isLast); -+ } -+ utils.inherits(NodeObject,_NodeComplex); -+ -+ -+ /* -+ * The constructor for array values -+ * {... -+ * [+] "label": array, -+ * ...} -+ * array = [1,2,3] -+ * -+ * @constructor -+ * @param label {string} - key name -+ * @param val {Array} - value of array type, [1,2,3] -+ * @param isLast {boolean} - true if node is last in list of siblings -+ */ -+ function NodeArray(label, val, isLast) { -+ this.sym = ['[', ']']; -+ this.type = "array"; -+ -+ _NodeComplex.call(this, label, val, isLast); -+ } -+ utils.inherits(NodeArray, _NodeComplex); -+ -+ -+ /* ---------- The tree constructor ---------- */ -+ -+ /* -+ * The constructor for json tree. -+ * It contains only one Node (Array or Object), without property name. -+ * CSS-styles of .tree define main tree styles like font-family, -+ * font-size and own margins. -+ * -+ * Markup: -+ *
            -+ * {Node} -+ *
          -+ * -+ * @constructor -+ * @param jsonObj {Object | Array} - data for tree -+ * @param domEl {DOMElement} - DOM-element, wrapper for tree -+ */ -+ function Tree(jsonObj, domEl) { -+ this.wrapper = document.createElement('ul'); -+ this.wrapper.className = 'jsontree_tree clearfix'; -+ -+ this.rootNode = null; -+ -+ this.sourceJSONObj = jsonObj; -+ -+ this.loadData(jsonObj); -+ this.appendTo(domEl); -+ } -+ -+ Tree.prototype = { -+ constructor : Tree, -+ -+ /** -+ * Fill new data in current json tree -+ * -+ * @param {Object | Array} jsonObj - json-data -+ */ -+ loadData : function(jsonObj) { -+ if (!utils.isValidRoot(jsonObj)) { -+ alert('The root should be an object or an array'); -+ return; -+ } -+ -+ this.sourceJSONObj = jsonObj; -+ -+ this.rootNode = new Node(null, jsonObj, 'last'); -+ this.wrapper.innerHTML = ''; -+ this.wrapper.appendChild(this.rootNode.el); -+ }, -+ -+ /** -+ * Appends tree to DOM-element (or move it to new place) -+ * -+ * @param {DOMElement} domEl -+ */ -+ appendTo : function(domEl) { -+ domEl.appendChild(this.wrapper); -+ }, -+ -+ /** -+ * Expands all tree nodes (objects or arrays) recursively -+ * -+ * @param {Function} filterFunc - 'true' if this node should be expanded -+ */ -+ expand : function(filterFunc) { -+ if (this.rootNode.isComplex) { -+ if (typeof filterFunc == 'function') { -+ this.rootNode.childNodes.forEach(function(item, i) { -+ if (item.isComplex && filterFunc(item)) { -+ item.expand(); -+ } -+ }); -+ } else { -+ this.rootNode.expand('recursive'); -+ } -+ } -+ }, -+ -+ /** -+ * Collapses all tree nodes (objects or arrays) recursively -+ */ -+ collapse : function() { -+ if (typeof this.rootNode.collapse === 'function') { -+ this.rootNode.collapse('recursive'); -+ } -+ }, -+ -+ /** -+ * Returns the source json-string (pretty-printed) -+ * -+ * @param {boolean} isPrettyPrinted - 'true' for pretty-printed string -+ * @returns {string} - for exemple, '{"a":2,"b":3}' -+ */ -+ toSourceJSON : function(isPrettyPrinted) { -+ if (!isPrettyPrinted) { -+ return JSON.stringify(this.sourceJSONObj); -+ } -+ -+ var DELIMETER = "[%^$#$%^%]", -+ jsonStr = JSON.stringify(this.sourceJSONObj, null, DELIMETER); -+ -+ jsonStr = jsonStr.split("\n").join("
          "); -+ jsonStr = jsonStr.split(DELIMETER).join("    "); -+ -+ return jsonStr; -+ }, -+ -+ /** -+ * Find all nodes that match some conditions and handle it -+ */ -+ findAndHandle : function(matcher, handler) { -+ this.rootNode.findChildren(matcher, handler, 'isRecursive'); -+ }, -+ -+ /** -+ * Unmark all nodes -+ */ -+ unmarkAll : function() { -+ this.rootNode.findChildren(function(node) { -+ return true; -+ }, function(node) { -+ node.unmark(); -+ }, 'isRecursive'); -+ } -+ }; -+ -+ -+ /* ---------- Public methods ---------- */ -+ return { -+ /** -+ * Creates new tree by data and appends it to the DOM-element -+ * -+ * @param jsonObj {Object | Array} - json-data -+ * @param domEl {DOMElement} - the wrapper element -+ * @returns {Tree} -+ */ -+ create : function(jsonObj, domEl) { -+ return new Tree(jsonObj, domEl); -+ } -+ }; -+})(); -diff --git a/pkg/http-server/assets/moment.js b/pkg/http-server/assets/moment.js -new file mode 100644 -index 0000000..43bb380 ---- /dev/null -+++ b/pkg/http-server/assets/moment.js -@@ -0,0 +1,5670 @@ -+//! moment.js -+//! version : 2.29.1 -+//! authors : Tim Wood, Iskren Chernev, Moment.js contributors -+//! license : MIT -+//! momentjs.com -+ -+;(function (global, factory) { -+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : -+ typeof define === 'function' && define.amd ? define(factory) : -+ global.moment = factory() -+}(this, (function () { 'use strict'; -+ -+ var hookCallback; -+ -+ function hooks() { -+ return hookCallback.apply(null, arguments); -+ } -+ -+ // This is done to register the method called with moment() -+ // without creating circular dependencies. -+ function setHookCallback(callback) { -+ hookCallback = callback; -+ } -+ -+ function isArray(input) { -+ return ( -+ input instanceof Array || -+ Object.prototype.toString.call(input) === '[object Array]' -+ ); -+ } -+ -+ function isObject(input) { -+ // IE8 will treat undefined and null as object if it wasn't for -+ // input != null -+ return ( -+ input != null && -+ Object.prototype.toString.call(input) === '[object Object]' -+ ); -+ } -+ -+ function hasOwnProp(a, b) { -+ return Object.prototype.hasOwnProperty.call(a, b); -+ } -+ -+ function isObjectEmpty(obj) { -+ if (Object.getOwnPropertyNames) { -+ return Object.getOwnPropertyNames(obj).length === 0; -+ } else { -+ var k; -+ for (k in obj) { -+ if (hasOwnProp(obj, k)) { -+ return false; -+ } -+ } -+ return true; -+ } -+ } -+ -+ function isUndefined(input) { -+ return input === void 0; -+ } -+ -+ function isNumber(input) { -+ return ( -+ typeof input === 'number' || -+ Object.prototype.toString.call(input) === '[object Number]' -+ ); -+ } -+ -+ function isDate(input) { -+ return ( -+ input instanceof Date || -+ Object.prototype.toString.call(input) === '[object Date]' -+ ); -+ } -+ -+ function map(arr, fn) { -+ var res = [], -+ i; -+ for (i = 0; i < arr.length; ++i) { -+ res.push(fn(arr[i], i)); -+ } -+ return res; -+ } -+ -+ function extend(a, b) { -+ for (var i in b) { -+ if (hasOwnProp(b, i)) { -+ a[i] = b[i]; -+ } -+ } -+ -+ if (hasOwnProp(b, 'toString')) { -+ a.toString = b.toString; -+ } -+ -+ if (hasOwnProp(b, 'valueOf')) { -+ a.valueOf = b.valueOf; -+ } -+ -+ return a; -+ } -+ -+ function createUTC(input, format, locale, strict) { -+ return createLocalOrUTC(input, format, locale, strict, true).utc(); -+ } -+ -+ function defaultParsingFlags() { -+ // We need to deep clone this object. -+ return { -+ empty: false, -+ unusedTokens: [], -+ unusedInput: [], -+ overflow: -2, -+ charsLeftOver: 0, -+ nullInput: false, -+ invalidEra: null, -+ invalidMonth: null, -+ invalidFormat: false, -+ userInvalidated: false, -+ iso: false, -+ parsedDateParts: [], -+ era: null, -+ meridiem: null, -+ rfc2822: false, -+ weekdayMismatch: false, -+ }; -+ } -+ -+ function getParsingFlags(m) { -+ if (m._pf == null) { -+ m._pf = defaultParsingFlags(); -+ } -+ return m._pf; -+ } -+ -+ var some; -+ if (Array.prototype.some) { -+ some = Array.prototype.some; -+ } else { -+ some = function (fun) { -+ var t = Object(this), -+ len = t.length >>> 0, -+ i; -+ -+ for (i = 0; i < len; i++) { -+ if (i in t && fun.call(this, t[i], i, t)) { -+ return true; -+ } -+ } -+ -+ return false; -+ }; -+ } -+ -+ function isValid(m) { -+ if (m._isValid == null) { -+ var flags = getParsingFlags(m), -+ parsedParts = some.call(flags.parsedDateParts, function (i) { -+ return i != null; -+ }), -+ isNowValid = -+ !isNaN(m._d.getTime()) && -+ flags.overflow < 0 && -+ !flags.empty && -+ !flags.invalidEra && -+ !flags.invalidMonth && -+ !flags.invalidWeekday && -+ !flags.weekdayMismatch && -+ !flags.nullInput && -+ !flags.invalidFormat && -+ !flags.userInvalidated && -+ (!flags.meridiem || (flags.meridiem && parsedParts)); -+ -+ if (m._strict) { -+ isNowValid = -+ isNowValid && -+ flags.charsLeftOver === 0 && -+ flags.unusedTokens.length === 0 && -+ flags.bigHour === undefined; -+ } -+ -+ if (Object.isFrozen == null || !Object.isFrozen(m)) { -+ m._isValid = isNowValid; -+ } else { -+ return isNowValid; -+ } -+ } -+ return m._isValid; -+ } -+ -+ function createInvalid(flags) { -+ var m = createUTC(NaN); -+ if (flags != null) { -+ extend(getParsingFlags(m), flags); -+ } else { -+ getParsingFlags(m).userInvalidated = true; -+ } -+ -+ return m; -+ } -+ -+ // Plugins that add properties should also add the key here (null value), -+ // so we can properly clone ourselves. -+ var momentProperties = (hooks.momentProperties = []), -+ updateInProgress = false; -+ -+ function copyConfig(to, from) { -+ var i, prop, val; -+ -+ if (!isUndefined(from._isAMomentObject)) { -+ to._isAMomentObject = from._isAMomentObject; -+ } -+ if (!isUndefined(from._i)) { -+ to._i = from._i; -+ } -+ if (!isUndefined(from._f)) { -+ to._f = from._f; -+ } -+ if (!isUndefined(from._l)) { -+ to._l = from._l; -+ } -+ if (!isUndefined(from._strict)) { -+ to._strict = from._strict; -+ } -+ if (!isUndefined(from._tzm)) { -+ to._tzm = from._tzm; -+ } -+ if (!isUndefined(from._isUTC)) { -+ to._isUTC = from._isUTC; -+ } -+ if (!isUndefined(from._offset)) { -+ to._offset = from._offset; -+ } -+ if (!isUndefined(from._pf)) { -+ to._pf = getParsingFlags(from); -+ } -+ if (!isUndefined(from._locale)) { -+ to._locale = from._locale; -+ } -+ -+ if (momentProperties.length > 0) { -+ for (i = 0; i < momentProperties.length; i++) { -+ prop = momentProperties[i]; -+ val = from[prop]; -+ if (!isUndefined(val)) { -+ to[prop] = val; -+ } -+ } -+ } -+ -+ return to; -+ } -+ -+ // Moment prototype object -+ function Moment(config) { -+ copyConfig(this, config); -+ this._d = new Date(config._d != null ? config._d.getTime() : NaN); -+ if (!this.isValid()) { -+ this._d = new Date(NaN); -+ } -+ // Prevent infinite loop in case updateOffset creates new moment -+ // objects. -+ if (updateInProgress === false) { -+ updateInProgress = true; -+ hooks.updateOffset(this); -+ updateInProgress = false; -+ } -+ } -+ -+ function isMoment(obj) { -+ return ( -+ obj instanceof Moment || (obj != null && obj._isAMomentObject != null) -+ ); -+ } -+ -+ function warn(msg) { -+ if ( -+ hooks.suppressDeprecationWarnings === false && -+ typeof console !== 'undefined' && -+ console.warn -+ ) { -+ console.warn('Deprecation warning: ' + msg); -+ } -+ } -+ -+ function deprecate(msg, fn) { -+ var firstTime = true; -+ -+ return extend(function () { -+ if (hooks.deprecationHandler != null) { -+ hooks.deprecationHandler(null, msg); -+ } -+ if (firstTime) { -+ var args = [], -+ arg, -+ i, -+ key; -+ for (i = 0; i < arguments.length; i++) { -+ arg = ''; -+ if (typeof arguments[i] === 'object') { -+ arg += '\n[' + i + '] '; -+ for (key in arguments[0]) { -+ if (hasOwnProp(arguments[0], key)) { -+ arg += key + ': ' + arguments[0][key] + ', '; -+ } -+ } -+ arg = arg.slice(0, -2); // Remove trailing comma and space -+ } else { -+ arg = arguments[i]; -+ } -+ args.push(arg); -+ } -+ warn( -+ msg + -+ '\nArguments: ' + -+ Array.prototype.slice.call(args).join('') + -+ '\n' + -+ new Error().stack -+ ); -+ firstTime = false; -+ } -+ return fn.apply(this, arguments); -+ }, fn); -+ } -+ -+ var deprecations = {}; -+ -+ function deprecateSimple(name, msg) { -+ if (hooks.deprecationHandler != null) { -+ hooks.deprecationHandler(name, msg); -+ } -+ if (!deprecations[name]) { -+ warn(msg); -+ deprecations[name] = true; -+ } -+ } -+ -+ hooks.suppressDeprecationWarnings = false; -+ hooks.deprecationHandler = null; -+ -+ function isFunction(input) { -+ return ( -+ (typeof Function !== 'undefined' && input instanceof Function) || -+ Object.prototype.toString.call(input) === '[object Function]' -+ ); -+ } -+ -+ function set(config) { -+ var prop, i; -+ for (i in config) { -+ if (hasOwnProp(config, i)) { -+ prop = config[i]; -+ if (isFunction(prop)) { -+ this[i] = prop; -+ } else { -+ this['_' + i] = prop; -+ } -+ } -+ } -+ this._config = config; -+ // Lenient ordinal parsing accepts just a number in addition to -+ // number + (possibly) stuff coming from _dayOfMonthOrdinalParse. -+ // TODO: Remove "ordinalParse" fallback in next major release. -+ this._dayOfMonthOrdinalParseLenient = new RegExp( -+ (this._dayOfMonthOrdinalParse.source || this._ordinalParse.source) + -+ '|' + -+ /\d{1,2}/.source -+ ); -+ } -+ -+ function mergeConfigs(parentConfig, childConfig) { -+ var res = extend({}, parentConfig), -+ prop; -+ for (prop in childConfig) { -+ if (hasOwnProp(childConfig, prop)) { -+ if (isObject(parentConfig[prop]) && isObject(childConfig[prop])) { -+ res[prop] = {}; -+ extend(res[prop], parentConfig[prop]); -+ extend(res[prop], childConfig[prop]); -+ } else if (childConfig[prop] != null) { -+ res[prop] = childConfig[prop]; -+ } else { -+ delete res[prop]; -+ } -+ } -+ } -+ for (prop in parentConfig) { -+ if ( -+ hasOwnProp(parentConfig, prop) && -+ !hasOwnProp(childConfig, prop) && -+ isObject(parentConfig[prop]) -+ ) { -+ // make sure changes to properties don't modify parent config -+ res[prop] = extend({}, res[prop]); -+ } -+ } -+ return res; -+ } -+ -+ function Locale(config) { -+ if (config != null) { -+ this.set(config); -+ } -+ } -+ -+ var keys; -+ -+ if (Object.keys) { -+ keys = Object.keys; -+ } else { -+ keys = function (obj) { -+ var i, -+ res = []; -+ for (i in obj) { -+ if (hasOwnProp(obj, i)) { -+ res.push(i); -+ } -+ } -+ return res; -+ }; -+ } -+ -+ var defaultCalendar = { -+ sameDay: '[Today at] LT', -+ nextDay: '[Tomorrow at] LT', -+ nextWeek: 'dddd [at] LT', -+ lastDay: '[Yesterday at] LT', -+ lastWeek: '[Last] dddd [at] LT', -+ sameElse: 'L', -+ }; -+ -+ function calendar(key, mom, now) { -+ var output = this._calendar[key] || this._calendar['sameElse']; -+ return isFunction(output) ? output.call(mom, now) : output; -+ } -+ -+ function zeroFill(number, targetLength, forceSign) { -+ var absNumber = '' + Math.abs(number), -+ zerosToFill = targetLength - absNumber.length, -+ sign = number >= 0; -+ return ( -+ (sign ? (forceSign ? '+' : '') : '-') + -+ Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + -+ absNumber -+ ); -+ } -+ -+ var formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g, -+ localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, -+ formatFunctions = {}, -+ formatTokenFunctions = {}; -+ -+ // token: 'M' -+ // padded: ['MM', 2] -+ // ordinal: 'Mo' -+ // callback: function () { this.month() + 1 } -+ function addFormatToken(token, padded, ordinal, callback) { -+ var func = callback; -+ if (typeof callback === 'string') { -+ func = function () { -+ return this[callback](); -+ }; -+ } -+ if (token) { -+ formatTokenFunctions[token] = func; -+ } -+ if (padded) { -+ formatTokenFunctions[padded[0]] = function () { -+ return zeroFill(func.apply(this, arguments), padded[1], padded[2]); -+ }; -+ } -+ if (ordinal) { -+ formatTokenFunctions[ordinal] = function () { -+ return this.localeData().ordinal( -+ func.apply(this, arguments), -+ token -+ ); -+ }; -+ } -+ } -+ -+ function removeFormattingTokens(input) { -+ if (input.match(/\[[\s\S]/)) { -+ return input.replace(/^\[|\]$/g, ''); -+ } -+ return input.replace(/\\/g, ''); -+ } -+ -+ function makeFormatFunction(format) { -+ var array = format.match(formattingTokens), -+ i, -+ length; -+ -+ for (i = 0, length = array.length; i < length; i++) { -+ if (formatTokenFunctions[array[i]]) { -+ array[i] = formatTokenFunctions[array[i]]; -+ } else { -+ array[i] = removeFormattingTokens(array[i]); -+ } -+ } -+ -+ return function (mom) { -+ var output = '', -+ i; -+ for (i = 0; i < length; i++) { -+ output += isFunction(array[i]) -+ ? array[i].call(mom, format) -+ : array[i]; -+ } -+ return output; -+ }; -+ } -+ -+ // format date using native date object -+ function formatMoment(m, format) { -+ if (!m.isValid()) { -+ return m.localeData().invalidDate(); -+ } -+ -+ format = expandFormat(format, m.localeData()); -+ formatFunctions[format] = -+ formatFunctions[format] || makeFormatFunction(format); -+ -+ return formatFunctions[format](m); -+ } -+ -+ function expandFormat(format, locale) { -+ var i = 5; -+ -+ function replaceLongDateFormatTokens(input) { -+ return locale.longDateFormat(input) || input; -+ } -+ -+ localFormattingTokens.lastIndex = 0; -+ while (i >= 0 && localFormattingTokens.test(format)) { -+ format = format.replace( -+ localFormattingTokens, -+ replaceLongDateFormatTokens -+ ); -+ localFormattingTokens.lastIndex = 0; -+ i -= 1; -+ } -+ -+ return format; -+ } -+ -+ var defaultLongDateFormat = { -+ LTS: 'h:mm:ss A', -+ LT: 'h:mm A', -+ L: 'MM/DD/YYYY', -+ LL: 'MMMM D, YYYY', -+ LLL: 'MMMM D, YYYY h:mm A', -+ LLLL: 'dddd, MMMM D, YYYY h:mm A', -+ }; -+ -+ function longDateFormat(key) { -+ var format = this._longDateFormat[key], -+ formatUpper = this._longDateFormat[key.toUpperCase()]; -+ -+ if (format || !formatUpper) { -+ return format; -+ } -+ -+ this._longDateFormat[key] = formatUpper -+ .match(formattingTokens) -+ .map(function (tok) { -+ if ( -+ tok === 'MMMM' || -+ tok === 'MM' || -+ tok === 'DD' || -+ tok === 'dddd' -+ ) { -+ return tok.slice(1); -+ } -+ return tok; -+ }) -+ .join(''); -+ -+ return this._longDateFormat[key]; -+ } -+ -+ var defaultInvalidDate = 'Invalid date'; -+ -+ function invalidDate() { -+ return this._invalidDate; -+ } -+ -+ var defaultOrdinal = '%d', -+ defaultDayOfMonthOrdinalParse = /\d{1,2}/; -+ -+ function ordinal(number) { -+ return this._ordinal.replace('%d', number); -+ } -+ -+ var defaultRelativeTime = { -+ future: 'in %s', -+ past: '%s ago', -+ s: 'a few seconds', -+ ss: '%d seconds', -+ m: 'a minute', -+ mm: '%d minutes', -+ h: 'an hour', -+ hh: '%d hours', -+ d: 'a day', -+ dd: '%d days', -+ w: 'a week', -+ ww: '%d weeks', -+ M: 'a month', -+ MM: '%d months', -+ y: 'a year', -+ yy: '%d years', -+ }; -+ -+ function relativeTime(number, withoutSuffix, string, isFuture) { -+ var output = this._relativeTime[string]; -+ return isFunction(output) -+ ? output(number, withoutSuffix, string, isFuture) -+ : output.replace(/%d/i, number); -+ } -+ -+ function pastFuture(diff, output) { -+ var format = this._relativeTime[diff > 0 ? 'future' : 'past']; -+ return isFunction(format) ? format(output) : format.replace(/%s/i, output); -+ } -+ -+ var aliases = {}; -+ -+ function addUnitAlias(unit, shorthand) { -+ var lowerCase = unit.toLowerCase(); -+ aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; -+ } -+ -+ function normalizeUnits(units) { -+ return typeof units === 'string' -+ ? aliases[units] || aliases[units.toLowerCase()] -+ : undefined; -+ } -+ -+ function normalizeObjectUnits(inputObject) { -+ var normalizedInput = {}, -+ normalizedProp, -+ prop; -+ -+ for (prop in inputObject) { -+ if (hasOwnProp(inputObject, prop)) { -+ normalizedProp = normalizeUnits(prop); -+ if (normalizedProp) { -+ normalizedInput[normalizedProp] = inputObject[prop]; -+ } -+ } -+ } -+ -+ return normalizedInput; -+ } -+ -+ var priorities = {}; -+ -+ function addUnitPriority(unit, priority) { -+ priorities[unit] = priority; -+ } -+ -+ function getPrioritizedUnits(unitsObj) { -+ var units = [], -+ u; -+ for (u in unitsObj) { -+ if (hasOwnProp(unitsObj, u)) { -+ units.push({ unit: u, priority: priorities[u] }); -+ } -+ } -+ units.sort(function (a, b) { -+ return a.priority - b.priority; -+ }); -+ return units; -+ } -+ -+ function isLeapYear(year) { -+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; -+ } -+ -+ function absFloor(number) { -+ if (number < 0) { -+ // -0 -> 0 -+ return Math.ceil(number) || 0; -+ } else { -+ return Math.floor(number); -+ } -+ } -+ -+ function toInt(argumentForCoercion) { -+ var coercedNumber = +argumentForCoercion, -+ value = 0; -+ -+ if (coercedNumber !== 0 && isFinite(coercedNumber)) { -+ value = absFloor(coercedNumber); -+ } -+ -+ return value; -+ } -+ -+ function makeGetSet(unit, keepTime) { -+ return function (value) { -+ if (value != null) { -+ set$1(this, unit, value); -+ hooks.updateOffset(this, keepTime); -+ return this; -+ } else { -+ return get(this, unit); -+ } -+ }; -+ } -+ -+ function get(mom, unit) { -+ return mom.isValid() -+ ? mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]() -+ : NaN; -+ } -+ -+ function set$1(mom, unit, value) { -+ if (mom.isValid() && !isNaN(value)) { -+ if ( -+ unit === 'FullYear' && -+ isLeapYear(mom.year()) && -+ mom.month() === 1 && -+ mom.date() === 29 -+ ) { -+ value = toInt(value); -+ mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit]( -+ value, -+ mom.month(), -+ daysInMonth(value, mom.month()) -+ ); -+ } else { -+ mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); -+ } -+ } -+ } -+ -+ // MOMENTS -+ -+ function stringGet(units) { -+ units = normalizeUnits(units); -+ if (isFunction(this[units])) { -+ return this[units](); -+ } -+ return this; -+ } -+ -+ function stringSet(units, value) { -+ if (typeof units === 'object') { -+ units = normalizeObjectUnits(units); -+ var prioritized = getPrioritizedUnits(units), -+ i; -+ for (i = 0; i < prioritized.length; i++) { -+ this[prioritized[i].unit](units[prioritized[i].unit]); -+ } -+ } else { -+ units = normalizeUnits(units); -+ if (isFunction(this[units])) { -+ return this[units](value); -+ } -+ } -+ return this; -+ } -+ -+ var match1 = /\d/, // 0 - 9 -+ match2 = /\d\d/, // 00 - 99 -+ match3 = /\d{3}/, // 000 - 999 -+ match4 = /\d{4}/, // 0000 - 9999 -+ match6 = /[+-]?\d{6}/, // -999999 - 999999 -+ match1to2 = /\d\d?/, // 0 - 99 -+ match3to4 = /\d\d\d\d?/, // 999 - 9999 -+ match5to6 = /\d\d\d\d\d\d?/, // 99999 - 999999 -+ match1to3 = /\d{1,3}/, // 0 - 999 -+ match1to4 = /\d{1,4}/, // 0 - 9999 -+ match1to6 = /[+-]?\d{1,6}/, // -999999 - 999999 -+ matchUnsigned = /\d+/, // 0 - inf -+ matchSigned = /[+-]?\d+/, // -inf - inf -+ matchOffset = /Z|[+-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z -+ matchShortOffset = /Z|[+-]\d\d(?::?\d\d)?/gi, // +00 -00 +00:00 -00:00 +0000 -0000 or Z -+ matchTimestamp = /[+-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123 -+ // any word (or two) characters or numbers including two/three word month in arabic. -+ // includes scottish gaelic two word and hyphenated months -+ matchWord = /[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i, -+ regexes; -+ -+ regexes = {}; -+ -+ function addRegexToken(token, regex, strictRegex) { -+ regexes[token] = isFunction(regex) -+ ? regex -+ : function (isStrict, localeData) { -+ return isStrict && strictRegex ? strictRegex : regex; -+ }; -+ } -+ -+ function getParseRegexForToken(token, config) { -+ if (!hasOwnProp(regexes, token)) { -+ return new RegExp(unescapeFormat(token)); -+ } -+ -+ return regexes[token](config._strict, config._locale); -+ } -+ -+ // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript -+ function unescapeFormat(s) { -+ return regexEscape( -+ s -+ .replace('\\', '') -+ .replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function ( -+ matched, -+ p1, -+ p2, -+ p3, -+ p4 -+ ) { -+ return p1 || p2 || p3 || p4; -+ }) -+ ); -+ } -+ -+ function regexEscape(s) { -+ return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); -+ } -+ -+ var tokens = {}; -+ -+ function addParseToken(token, callback) { -+ var i, -+ func = callback; -+ if (typeof token === 'string') { -+ token = [token]; -+ } -+ if (isNumber(callback)) { -+ func = function (input, array) { -+ array[callback] = toInt(input); -+ }; -+ } -+ for (i = 0; i < token.length; i++) { -+ tokens[token[i]] = func; -+ } -+ } -+ -+ function addWeekParseToken(token, callback) { -+ addParseToken(token, function (input, array, config, token) { -+ config._w = config._w || {}; -+ callback(input, config._w, config, token); -+ }); -+ } -+ -+ function addTimeToArrayFromToken(token, input, config) { -+ if (input != null && hasOwnProp(tokens, token)) { -+ tokens[token](input, config._a, config, token); -+ } -+ } -+ -+ var YEAR = 0, -+ MONTH = 1, -+ DATE = 2, -+ HOUR = 3, -+ MINUTE = 4, -+ SECOND = 5, -+ MILLISECOND = 6, -+ WEEK = 7, -+ WEEKDAY = 8; -+ -+ function mod(n, x) { -+ return ((n % x) + x) % x; -+ } -+ -+ var indexOf; -+ -+ if (Array.prototype.indexOf) { -+ indexOf = Array.prototype.indexOf; -+ } else { -+ indexOf = function (o) { -+ // I know -+ var i; -+ for (i = 0; i < this.length; ++i) { -+ if (this[i] === o) { -+ return i; -+ } -+ } -+ return -1; -+ }; -+ } -+ -+ function daysInMonth(year, month) { -+ if (isNaN(year) || isNaN(month)) { -+ return NaN; -+ } -+ var modMonth = mod(month, 12); -+ year += (month - modMonth) / 12; -+ return modMonth === 1 -+ ? isLeapYear(year) -+ ? 29 -+ : 28 -+ : 31 - ((modMonth % 7) % 2); -+ } -+ -+ // FORMATTING -+ -+ addFormatToken('M', ['MM', 2], 'Mo', function () { -+ return this.month() + 1; -+ }); -+ -+ addFormatToken('MMM', 0, 0, function (format) { -+ return this.localeData().monthsShort(this, format); -+ }); -+ -+ addFormatToken('MMMM', 0, 0, function (format) { -+ return this.localeData().months(this, format); -+ }); -+ -+ // ALIASES -+ -+ addUnitAlias('month', 'M'); -+ -+ // PRIORITY -+ -+ addUnitPriority('month', 8); -+ -+ // PARSING -+ -+ addRegexToken('M', match1to2); -+ addRegexToken('MM', match1to2, match2); -+ addRegexToken('MMM', function (isStrict, locale) { -+ return locale.monthsShortRegex(isStrict); -+ }); -+ addRegexToken('MMMM', function (isStrict, locale) { -+ return locale.monthsRegex(isStrict); -+ }); -+ -+ addParseToken(['M', 'MM'], function (input, array) { -+ array[MONTH] = toInt(input) - 1; -+ }); -+ -+ addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { -+ var month = config._locale.monthsParse(input, token, config._strict); -+ // if we didn't find a month name, mark the date as invalid. -+ if (month != null) { -+ array[MONTH] = month; -+ } else { -+ getParsingFlags(config).invalidMonth = input; -+ } -+ }); -+ -+ // LOCALES -+ -+ var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split( -+ '_' -+ ), -+ defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split( -+ '_' -+ ), -+ MONTHS_IN_FORMAT = /D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/, -+ defaultMonthsShortRegex = matchWord, -+ defaultMonthsRegex = matchWord; -+ -+ function localeMonths(m, format) { -+ if (!m) { -+ return isArray(this._months) -+ ? this._months -+ : this._months['standalone']; -+ } -+ return isArray(this._months) -+ ? this._months[m.month()] -+ : this._months[ -+ (this._months.isFormat || MONTHS_IN_FORMAT).test(format) -+ ? 'format' -+ : 'standalone' -+ ][m.month()]; -+ } -+ -+ function localeMonthsShort(m, format) { -+ if (!m) { -+ return isArray(this._monthsShort) -+ ? this._monthsShort -+ : this._monthsShort['standalone']; -+ } -+ return isArray(this._monthsShort) -+ ? this._monthsShort[m.month()] -+ : this._monthsShort[ -+ MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone' -+ ][m.month()]; -+ } -+ -+ function handleStrictParse(monthName, format, strict) { -+ var i, -+ ii, -+ mom, -+ llc = monthName.toLocaleLowerCase(); -+ if (!this._monthsParse) { -+ // this is not used -+ this._monthsParse = []; -+ this._longMonthsParse = []; -+ this._shortMonthsParse = []; -+ for (i = 0; i < 12; ++i) { -+ mom = createUTC([2000, i]); -+ this._shortMonthsParse[i] = this.monthsShort( -+ mom, -+ '' -+ ).toLocaleLowerCase(); -+ this._longMonthsParse[i] = this.months(mom, '').toLocaleLowerCase(); -+ } -+ } -+ -+ if (strict) { -+ if (format === 'MMM') { -+ ii = indexOf.call(this._shortMonthsParse, llc); -+ return ii !== -1 ? ii : null; -+ } else { -+ ii = indexOf.call(this._longMonthsParse, llc); -+ return ii !== -1 ? ii : null; -+ } -+ } else { -+ if (format === 'MMM') { -+ ii = indexOf.call(this._shortMonthsParse, llc); -+ if (ii !== -1) { -+ return ii; -+ } -+ ii = indexOf.call(this._longMonthsParse, llc); -+ return ii !== -1 ? ii : null; -+ } else { -+ ii = indexOf.call(this._longMonthsParse, llc); -+ if (ii !== -1) { -+ return ii; -+ } -+ ii = indexOf.call(this._shortMonthsParse, llc); -+ return ii !== -1 ? ii : null; -+ } -+ } -+ } -+ -+ function localeMonthsParse(monthName, format, strict) { -+ var i, mom, regex; -+ -+ if (this._monthsParseExact) { -+ return handleStrictParse.call(this, monthName, format, strict); -+ } -+ -+ if (!this._monthsParse) { -+ this._monthsParse = []; -+ this._longMonthsParse = []; -+ this._shortMonthsParse = []; -+ } -+ -+ // TODO: add sorting -+ // Sorting makes sure if one month (or abbr) is a prefix of another -+ // see sorting in computeMonthsParse -+ for (i = 0; i < 12; i++) { -+ // make the regex if we don't have it already -+ mom = createUTC([2000, i]); -+ if (strict && !this._longMonthsParse[i]) { -+ this._longMonthsParse[i] = new RegExp( -+ '^' + this.months(mom, '').replace('.', '') + '$', -+ 'i' -+ ); -+ this._shortMonthsParse[i] = new RegExp( -+ '^' + this.monthsShort(mom, '').replace('.', '') + '$', -+ 'i' -+ ); -+ } -+ if (!strict && !this._monthsParse[i]) { -+ regex = -+ '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); -+ this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); -+ } -+ // test the regex -+ if ( -+ strict && -+ format === 'MMMM' && -+ this._longMonthsParse[i].test(monthName) -+ ) { -+ return i; -+ } else if ( -+ strict && -+ format === 'MMM' && -+ this._shortMonthsParse[i].test(monthName) -+ ) { -+ return i; -+ } else if (!strict && this._monthsParse[i].test(monthName)) { -+ return i; -+ } -+ } -+ } -+ -+ // MOMENTS -+ -+ function setMonth(mom, value) { -+ var dayOfMonth; -+ -+ if (!mom.isValid()) { -+ // No op -+ return mom; -+ } -+ -+ if (typeof value === 'string') { -+ if (/^\d+$/.test(value)) { -+ value = toInt(value); -+ } else { -+ value = mom.localeData().monthsParse(value); -+ // TODO: Another silent failure? -+ if (!isNumber(value)) { -+ return mom; -+ } -+ } -+ } -+ -+ dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); -+ mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); -+ return mom; -+ } -+ -+ function getSetMonth(value) { -+ if (value != null) { -+ setMonth(this, value); -+ hooks.updateOffset(this, true); -+ return this; -+ } else { -+ return get(this, 'Month'); -+ } -+ } -+ -+ function getDaysInMonth() { -+ return daysInMonth(this.year(), this.month()); -+ } -+ -+ function monthsShortRegex(isStrict) { -+ if (this._monthsParseExact) { -+ if (!hasOwnProp(this, '_monthsRegex')) { -+ computeMonthsParse.call(this); -+ } -+ if (isStrict) { -+ return this._monthsShortStrictRegex; -+ } else { -+ return this._monthsShortRegex; -+ } -+ } else { -+ if (!hasOwnProp(this, '_monthsShortRegex')) { -+ this._monthsShortRegex = defaultMonthsShortRegex; -+ } -+ return this._monthsShortStrictRegex && isStrict -+ ? this._monthsShortStrictRegex -+ : this._monthsShortRegex; -+ } -+ } -+ -+ function monthsRegex(isStrict) { -+ if (this._monthsParseExact) { -+ if (!hasOwnProp(this, '_monthsRegex')) { -+ computeMonthsParse.call(this); -+ } -+ if (isStrict) { -+ return this._monthsStrictRegex; -+ } else { -+ return this._monthsRegex; -+ } -+ } else { -+ if (!hasOwnProp(this, '_monthsRegex')) { -+ this._monthsRegex = defaultMonthsRegex; -+ } -+ return this._monthsStrictRegex && isStrict -+ ? this._monthsStrictRegex -+ : this._monthsRegex; -+ } -+ } -+ -+ function computeMonthsParse() { -+ function cmpLenRev(a, b) { -+ return b.length - a.length; -+ } -+ -+ var shortPieces = [], -+ longPieces = [], -+ mixedPieces = [], -+ i, -+ mom; -+ for (i = 0; i < 12; i++) { -+ // make the regex if we don't have it already -+ mom = createUTC([2000, i]); -+ shortPieces.push(this.monthsShort(mom, '')); -+ longPieces.push(this.months(mom, '')); -+ mixedPieces.push(this.months(mom, '')); -+ mixedPieces.push(this.monthsShort(mom, '')); -+ } -+ // Sorting makes sure if one month (or abbr) is a prefix of another it -+ // will match the longer piece. -+ shortPieces.sort(cmpLenRev); -+ longPieces.sort(cmpLenRev); -+ mixedPieces.sort(cmpLenRev); -+ for (i = 0; i < 12; i++) { -+ shortPieces[i] = regexEscape(shortPieces[i]); -+ longPieces[i] = regexEscape(longPieces[i]); -+ } -+ for (i = 0; i < 24; i++) { -+ mixedPieces[i] = regexEscape(mixedPieces[i]); -+ } -+ -+ this._monthsRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); -+ this._monthsShortRegex = this._monthsRegex; -+ this._monthsStrictRegex = new RegExp( -+ '^(' + longPieces.join('|') + ')', -+ 'i' -+ ); -+ this._monthsShortStrictRegex = new RegExp( -+ '^(' + shortPieces.join('|') + ')', -+ 'i' -+ ); -+ } -+ -+ // FORMATTING -+ -+ addFormatToken('Y', 0, 0, function () { -+ var y = this.year(); -+ return y <= 9999 ? zeroFill(y, 4) : '+' + y; -+ }); -+ -+ addFormatToken(0, ['YY', 2], 0, function () { -+ return this.year() % 100; -+ }); -+ -+ addFormatToken(0, ['YYYY', 4], 0, 'year'); -+ addFormatToken(0, ['YYYYY', 5], 0, 'year'); -+ addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); -+ -+ // ALIASES -+ -+ addUnitAlias('year', 'y'); -+ -+ // PRIORITIES -+ -+ addUnitPriority('year', 1); -+ -+ // PARSING -+ -+ addRegexToken('Y', matchSigned); -+ addRegexToken('YY', match1to2, match2); -+ addRegexToken('YYYY', match1to4, match4); -+ addRegexToken('YYYYY', match1to6, match6); -+ addRegexToken('YYYYYY', match1to6, match6); -+ -+ addParseToken(['YYYYY', 'YYYYYY'], YEAR); -+ addParseToken('YYYY', function (input, array) { -+ array[YEAR] = -+ input.length === 2 ? hooks.parseTwoDigitYear(input) : toInt(input); -+ }); -+ addParseToken('YY', function (input, array) { -+ array[YEAR] = hooks.parseTwoDigitYear(input); -+ }); -+ addParseToken('Y', function (input, array) { -+ array[YEAR] = parseInt(input, 10); -+ }); -+ -+ // HELPERS -+ -+ function daysInYear(year) { -+ return isLeapYear(year) ? 366 : 365; -+ } -+ -+ // HOOKS -+ -+ hooks.parseTwoDigitYear = function (input) { -+ return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); -+ }; -+ -+ // MOMENTS -+ -+ var getSetYear = makeGetSet('FullYear', true); -+ -+ function getIsLeapYear() { -+ return isLeapYear(this.year()); -+ } -+ -+ function createDate(y, m, d, h, M, s, ms) { -+ // can't just apply() to create a date: -+ // https://stackoverflow.com/q/181348 -+ var date; -+ // the date constructor remaps years 0-99 to 1900-1999 -+ if (y < 100 && y >= 0) { -+ // preserve leap years using a full 400 year cycle, then reset -+ date = new Date(y + 400, m, d, h, M, s, ms); -+ if (isFinite(date.getFullYear())) { -+ date.setFullYear(y); -+ } -+ } else { -+ date = new Date(y, m, d, h, M, s, ms); -+ } -+ -+ return date; -+ } -+ -+ function createUTCDate(y) { -+ var date, args; -+ // the Date.UTC function remaps years 0-99 to 1900-1999 -+ if (y < 100 && y >= 0) { -+ args = Array.prototype.slice.call(arguments); -+ // preserve leap years using a full 400 year cycle, then reset -+ args[0] = y + 400; -+ date = new Date(Date.UTC.apply(null, args)); -+ if (isFinite(date.getUTCFullYear())) { -+ date.setUTCFullYear(y); -+ } -+ } else { -+ date = new Date(Date.UTC.apply(null, arguments)); -+ } -+ -+ return date; -+ } -+ -+ // start-of-first-week - start-of-year -+ function firstWeekOffset(year, dow, doy) { -+ var // first-week day -- which january is always in the first week (4 for iso, 1 for other) -+ fwd = 7 + dow - doy, -+ // first-week day local weekday -- which local weekday is fwd -+ fwdlw = (7 + createUTCDate(year, 0, fwd).getUTCDay() - dow) % 7; -+ -+ return -fwdlw + fwd - 1; -+ } -+ -+ // https://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday -+ function dayOfYearFromWeeks(year, week, weekday, dow, doy) { -+ var localWeekday = (7 + weekday - dow) % 7, -+ weekOffset = firstWeekOffset(year, dow, doy), -+ dayOfYear = 1 + 7 * (week - 1) + localWeekday + weekOffset, -+ resYear, -+ resDayOfYear; -+ -+ if (dayOfYear <= 0) { -+ resYear = year - 1; -+ resDayOfYear = daysInYear(resYear) + dayOfYear; -+ } else if (dayOfYear > daysInYear(year)) { -+ resYear = year + 1; -+ resDayOfYear = dayOfYear - daysInYear(year); -+ } else { -+ resYear = year; -+ resDayOfYear = dayOfYear; -+ } -+ -+ return { -+ year: resYear, -+ dayOfYear: resDayOfYear, -+ }; -+ } -+ -+ function weekOfYear(mom, dow, doy) { -+ var weekOffset = firstWeekOffset(mom.year(), dow, doy), -+ week = Math.floor((mom.dayOfYear() - weekOffset - 1) / 7) + 1, -+ resWeek, -+ resYear; -+ -+ if (week < 1) { -+ resYear = mom.year() - 1; -+ resWeek = week + weeksInYear(resYear, dow, doy); -+ } else if (week > weeksInYear(mom.year(), dow, doy)) { -+ resWeek = week - weeksInYear(mom.year(), dow, doy); -+ resYear = mom.year() + 1; -+ } else { -+ resYear = mom.year(); -+ resWeek = week; -+ } -+ -+ return { -+ week: resWeek, -+ year: resYear, -+ }; -+ } -+ -+ function weeksInYear(year, dow, doy) { -+ var weekOffset = firstWeekOffset(year, dow, doy), -+ weekOffsetNext = firstWeekOffset(year + 1, dow, doy); -+ return (daysInYear(year) - weekOffset + weekOffsetNext) / 7; -+ } -+ -+ // FORMATTING -+ -+ addFormatToken('w', ['ww', 2], 'wo', 'week'); -+ addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); -+ -+ // ALIASES -+ -+ addUnitAlias('week', 'w'); -+ addUnitAlias('isoWeek', 'W'); -+ -+ // PRIORITIES -+ -+ addUnitPriority('week', 5); -+ addUnitPriority('isoWeek', 5); -+ -+ // PARSING -+ -+ addRegexToken('w', match1to2); -+ addRegexToken('ww', match1to2, match2); -+ addRegexToken('W', match1to2); -+ addRegexToken('WW', match1to2, match2); -+ -+ addWeekParseToken(['w', 'ww', 'W', 'WW'], function ( -+ input, -+ week, -+ config, -+ token -+ ) { -+ week[token.substr(0, 1)] = toInt(input); -+ }); -+ -+ // HELPERS -+ -+ // LOCALES -+ -+ function localeWeek(mom) { -+ return weekOfYear(mom, this._week.dow, this._week.doy).week; -+ } -+ -+ var defaultLocaleWeek = { -+ dow: 0, // Sunday is the first day of the week. -+ doy: 6, // The week that contains Jan 6th is the first week of the year. -+ }; -+ -+ function localeFirstDayOfWeek() { -+ return this._week.dow; -+ } -+ -+ function localeFirstDayOfYear() { -+ return this._week.doy; -+ } -+ -+ // MOMENTS -+ -+ function getSetWeek(input) { -+ var week = this.localeData().week(this); -+ return input == null ? week : this.add((input - week) * 7, 'd'); -+ } -+ -+ function getSetISOWeek(input) { -+ var week = weekOfYear(this, 1, 4).week; -+ return input == null ? week : this.add((input - week) * 7, 'd'); -+ } -+ -+ // FORMATTING -+ -+ addFormatToken('d', 0, 'do', 'day'); -+ -+ addFormatToken('dd', 0, 0, function (format) { -+ return this.localeData().weekdaysMin(this, format); -+ }); -+ -+ addFormatToken('ddd', 0, 0, function (format) { -+ return this.localeData().weekdaysShort(this, format); -+ }); -+ -+ addFormatToken('dddd', 0, 0, function (format) { -+ return this.localeData().weekdays(this, format); -+ }); -+ -+ addFormatToken('e', 0, 0, 'weekday'); -+ addFormatToken('E', 0, 0, 'isoWeekday'); -+ -+ // ALIASES -+ -+ addUnitAlias('day', 'd'); -+ addUnitAlias('weekday', 'e'); -+ addUnitAlias('isoWeekday', 'E'); -+ -+ // PRIORITY -+ addUnitPriority('day', 11); -+ addUnitPriority('weekday', 11); -+ addUnitPriority('isoWeekday', 11); -+ -+ // PARSING -+ -+ addRegexToken('d', match1to2); -+ addRegexToken('e', match1to2); -+ addRegexToken('E', match1to2); -+ addRegexToken('dd', function (isStrict, locale) { -+ return locale.weekdaysMinRegex(isStrict); -+ }); -+ addRegexToken('ddd', function (isStrict, locale) { -+ return locale.weekdaysShortRegex(isStrict); -+ }); -+ addRegexToken('dddd', function (isStrict, locale) { -+ return locale.weekdaysRegex(isStrict); -+ }); -+ -+ addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config, token) { -+ var weekday = config._locale.weekdaysParse(input, token, config._strict); -+ // if we didn't get a weekday name, mark the date as invalid -+ if (weekday != null) { -+ week.d = weekday; -+ } else { -+ getParsingFlags(config).invalidWeekday = input; -+ } -+ }); -+ -+ addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { -+ week[token] = toInt(input); -+ }); -+ -+ // HELPERS -+ -+ function parseWeekday(input, locale) { -+ if (typeof input !== 'string') { -+ return input; -+ } -+ -+ if (!isNaN(input)) { -+ return parseInt(input, 10); -+ } -+ -+ input = locale.weekdaysParse(input); -+ if (typeof input === 'number') { -+ return input; -+ } -+ -+ return null; -+ } -+ -+ function parseIsoWeekday(input, locale) { -+ if (typeof input === 'string') { -+ return locale.weekdaysParse(input) % 7 || 7; -+ } -+ return isNaN(input) ? null : input; -+ } -+ -+ // LOCALES -+ function shiftWeekdays(ws, n) { -+ return ws.slice(n, 7).concat(ws.slice(0, n)); -+ } -+ -+ var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split( -+ '_' -+ ), -+ defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), -+ defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), -+ defaultWeekdaysRegex = matchWord, -+ defaultWeekdaysShortRegex = matchWord, -+ defaultWeekdaysMinRegex = matchWord; -+ -+ function localeWeekdays(m, format) { -+ var weekdays = isArray(this._weekdays) -+ ? this._weekdays -+ : this._weekdays[ -+ m && m !== true && this._weekdays.isFormat.test(format) -+ ? 'format' -+ : 'standalone' -+ ]; -+ return m === true -+ ? shiftWeekdays(weekdays, this._week.dow) -+ : m -+ ? weekdays[m.day()] -+ : weekdays; -+ } -+ -+ function localeWeekdaysShort(m) { -+ return m === true -+ ? shiftWeekdays(this._weekdaysShort, this._week.dow) -+ : m -+ ? this._weekdaysShort[m.day()] -+ : this._weekdaysShort; -+ } -+ -+ function localeWeekdaysMin(m) { -+ return m === true -+ ? shiftWeekdays(this._weekdaysMin, this._week.dow) -+ : m -+ ? this._weekdaysMin[m.day()] -+ : this._weekdaysMin; -+ } -+ -+ function handleStrictParse$1(weekdayName, format, strict) { -+ var i, -+ ii, -+ mom, -+ llc = weekdayName.toLocaleLowerCase(); -+ if (!this._weekdaysParse) { -+ this._weekdaysParse = []; -+ this._shortWeekdaysParse = []; -+ this._minWeekdaysParse = []; -+ -+ for (i = 0; i < 7; ++i) { -+ mom = createUTC([2000, 1]).day(i); -+ this._minWeekdaysParse[i] = this.weekdaysMin( -+ mom, -+ '' -+ ).toLocaleLowerCase(); -+ this._shortWeekdaysParse[i] = this.weekdaysShort( -+ mom, -+ '' -+ ).toLocaleLowerCase(); -+ this._weekdaysParse[i] = this.weekdays(mom, '').toLocaleLowerCase(); -+ } -+ } -+ -+ if (strict) { -+ if (format === 'dddd') { -+ ii = indexOf.call(this._weekdaysParse, llc); -+ return ii !== -1 ? ii : null; -+ } else if (format === 'ddd') { -+ ii = indexOf.call(this._shortWeekdaysParse, llc); -+ return ii !== -1 ? ii : null; -+ } else { -+ ii = indexOf.call(this._minWeekdaysParse, llc); -+ return ii !== -1 ? ii : null; -+ } -+ } else { -+ if (format === 'dddd') { -+ ii = indexOf.call(this._weekdaysParse, llc); -+ if (ii !== -1) { -+ return ii; -+ } -+ ii = indexOf.call(this._shortWeekdaysParse, llc); -+ if (ii !== -1) { -+ return ii; -+ } -+ ii = indexOf.call(this._minWeekdaysParse, llc); -+ return ii !== -1 ? ii : null; -+ } else if (format === 'ddd') { -+ ii = indexOf.call(this._shortWeekdaysParse, llc); -+ if (ii !== -1) { -+ return ii; -+ } -+ ii = indexOf.call(this._weekdaysParse, llc); -+ if (ii !== -1) { -+ return ii; -+ } -+ ii = indexOf.call(this._minWeekdaysParse, llc); -+ return ii !== -1 ? ii : null; -+ } else { -+ ii = indexOf.call(this._minWeekdaysParse, llc); -+ if (ii !== -1) { -+ return ii; -+ } -+ ii = indexOf.call(this._weekdaysParse, llc); -+ if (ii !== -1) { -+ return ii; -+ } -+ ii = indexOf.call(this._shortWeekdaysParse, llc); -+ return ii !== -1 ? ii : null; -+ } -+ } -+ } -+ -+ function localeWeekdaysParse(weekdayName, format, strict) { -+ var i, mom, regex; -+ -+ if (this._weekdaysParseExact) { -+ return handleStrictParse$1.call(this, weekdayName, format, strict); -+ } -+ -+ if (!this._weekdaysParse) { -+ this._weekdaysParse = []; -+ this._minWeekdaysParse = []; -+ this._shortWeekdaysParse = []; -+ this._fullWeekdaysParse = []; -+ } -+ -+ for (i = 0; i < 7; i++) { -+ // make the regex if we don't have it already -+ -+ mom = createUTC([2000, 1]).day(i); -+ if (strict && !this._fullWeekdaysParse[i]) { -+ this._fullWeekdaysParse[i] = new RegExp( -+ '^' + this.weekdays(mom, '').replace('.', '\\.?') + '$', -+ 'i' -+ ); -+ this._shortWeekdaysParse[i] = new RegExp( -+ '^' + this.weekdaysShort(mom, '').replace('.', '\\.?') + '$', -+ 'i' -+ ); -+ this._minWeekdaysParse[i] = new RegExp( -+ '^' + this.weekdaysMin(mom, '').replace('.', '\\.?') + '$', -+ 'i' -+ ); -+ } -+ if (!this._weekdaysParse[i]) { -+ regex = -+ '^' + -+ this.weekdays(mom, '') + -+ '|^' + -+ this.weekdaysShort(mom, '') + -+ '|^' + -+ this.weekdaysMin(mom, ''); -+ this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); -+ } -+ // test the regex -+ if ( -+ strict && -+ format === 'dddd' && -+ this._fullWeekdaysParse[i].test(weekdayName) -+ ) { -+ return i; -+ } else if ( -+ strict && -+ format === 'ddd' && -+ this._shortWeekdaysParse[i].test(weekdayName) -+ ) { -+ return i; -+ } else if ( -+ strict && -+ format === 'dd' && -+ this._minWeekdaysParse[i].test(weekdayName) -+ ) { -+ return i; -+ } else if (!strict && this._weekdaysParse[i].test(weekdayName)) { -+ return i; -+ } -+ } -+ } -+ -+ // MOMENTS -+ -+ function getSetDayOfWeek(input) { -+ if (!this.isValid()) { -+ return input != null ? this : NaN; -+ } -+ var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); -+ if (input != null) { -+ input = parseWeekday(input, this.localeData()); -+ return this.add(input - day, 'd'); -+ } else { -+ return day; -+ } -+ } -+ -+ function getSetLocaleDayOfWeek(input) { -+ if (!this.isValid()) { -+ return input != null ? this : NaN; -+ } -+ var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; -+ return input == null ? weekday : this.add(input - weekday, 'd'); -+ } -+ -+ function getSetISODayOfWeek(input) { -+ if (!this.isValid()) { -+ return input != null ? this : NaN; -+ } -+ -+ // behaves the same as moment#day except -+ // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) -+ // as a setter, sunday should belong to the previous week. -+ -+ if (input != null) { -+ var weekday = parseIsoWeekday(input, this.localeData()); -+ return this.day(this.day() % 7 ? weekday : weekday - 7); -+ } else { -+ return this.day() || 7; -+ } -+ } -+ -+ function weekdaysRegex(isStrict) { -+ if (this._weekdaysParseExact) { -+ if (!hasOwnProp(this, '_weekdaysRegex')) { -+ computeWeekdaysParse.call(this); -+ } -+ if (isStrict) { -+ return this._weekdaysStrictRegex; -+ } else { -+ return this._weekdaysRegex; -+ } -+ } else { -+ if (!hasOwnProp(this, '_weekdaysRegex')) { -+ this._weekdaysRegex = defaultWeekdaysRegex; -+ } -+ return this._weekdaysStrictRegex && isStrict -+ ? this._weekdaysStrictRegex -+ : this._weekdaysRegex; -+ } -+ } -+ -+ function weekdaysShortRegex(isStrict) { -+ if (this._weekdaysParseExact) { -+ if (!hasOwnProp(this, '_weekdaysRegex')) { -+ computeWeekdaysParse.call(this); -+ } -+ if (isStrict) { -+ return this._weekdaysShortStrictRegex; -+ } else { -+ return this._weekdaysShortRegex; -+ } -+ } else { -+ if (!hasOwnProp(this, '_weekdaysShortRegex')) { -+ this._weekdaysShortRegex = defaultWeekdaysShortRegex; -+ } -+ return this._weekdaysShortStrictRegex && isStrict -+ ? this._weekdaysShortStrictRegex -+ : this._weekdaysShortRegex; -+ } -+ } -+ -+ function weekdaysMinRegex(isStrict) { -+ if (this._weekdaysParseExact) { -+ if (!hasOwnProp(this, '_weekdaysRegex')) { -+ computeWeekdaysParse.call(this); -+ } -+ if (isStrict) { -+ return this._weekdaysMinStrictRegex; -+ } else { -+ return this._weekdaysMinRegex; -+ } -+ } else { -+ if (!hasOwnProp(this, '_weekdaysMinRegex')) { -+ this._weekdaysMinRegex = defaultWeekdaysMinRegex; -+ } -+ return this._weekdaysMinStrictRegex && isStrict -+ ? this._weekdaysMinStrictRegex -+ : this._weekdaysMinRegex; -+ } -+ } -+ -+ function computeWeekdaysParse() { -+ function cmpLenRev(a, b) { -+ return b.length - a.length; -+ } -+ -+ var minPieces = [], -+ shortPieces = [], -+ longPieces = [], -+ mixedPieces = [], -+ i, -+ mom, -+ minp, -+ shortp, -+ longp; -+ for (i = 0; i < 7; i++) { -+ // make the regex if we don't have it already -+ mom = createUTC([2000, 1]).day(i); -+ minp = regexEscape(this.weekdaysMin(mom, '')); -+ shortp = regexEscape(this.weekdaysShort(mom, '')); -+ longp = regexEscape(this.weekdays(mom, '')); -+ minPieces.push(minp); -+ shortPieces.push(shortp); -+ longPieces.push(longp); -+ mixedPieces.push(minp); -+ mixedPieces.push(shortp); -+ mixedPieces.push(longp); -+ } -+ // Sorting makes sure if one weekday (or abbr) is a prefix of another it -+ // will match the longer piece. -+ minPieces.sort(cmpLenRev); -+ shortPieces.sort(cmpLenRev); -+ longPieces.sort(cmpLenRev); -+ mixedPieces.sort(cmpLenRev); -+ -+ this._weekdaysRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); -+ this._weekdaysShortRegex = this._weekdaysRegex; -+ this._weekdaysMinRegex = this._weekdaysRegex; -+ -+ this._weekdaysStrictRegex = new RegExp( -+ '^(' + longPieces.join('|') + ')', -+ 'i' -+ ); -+ this._weekdaysShortStrictRegex = new RegExp( -+ '^(' + shortPieces.join('|') + ')', -+ 'i' -+ ); -+ this._weekdaysMinStrictRegex = new RegExp( -+ '^(' + minPieces.join('|') + ')', -+ 'i' -+ ); -+ } -+ -+ // FORMATTING -+ -+ function hFormat() { -+ return this.hours() % 12 || 12; -+ } -+ -+ function kFormat() { -+ return this.hours() || 24; -+ } -+ -+ addFormatToken('H', ['HH', 2], 0, 'hour'); -+ addFormatToken('h', ['hh', 2], 0, hFormat); -+ addFormatToken('k', ['kk', 2], 0, kFormat); -+ -+ addFormatToken('hmm', 0, 0, function () { -+ return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2); -+ }); -+ -+ addFormatToken('hmmss', 0, 0, function () { -+ return ( -+ '' + -+ hFormat.apply(this) + -+ zeroFill(this.minutes(), 2) + -+ zeroFill(this.seconds(), 2) -+ ); -+ }); -+ -+ addFormatToken('Hmm', 0, 0, function () { -+ return '' + this.hours() + zeroFill(this.minutes(), 2); -+ }); -+ -+ addFormatToken('Hmmss', 0, 0, function () { -+ return ( -+ '' + -+ this.hours() + -+ zeroFill(this.minutes(), 2) + -+ zeroFill(this.seconds(), 2) -+ ); -+ }); -+ -+ function meridiem(token, lowercase) { -+ addFormatToken(token, 0, 0, function () { -+ return this.localeData().meridiem( -+ this.hours(), -+ this.minutes(), -+ lowercase -+ ); -+ }); -+ } -+ -+ meridiem('a', true); -+ meridiem('A', false); -+ -+ // ALIASES -+ -+ addUnitAlias('hour', 'h'); -+ -+ // PRIORITY -+ addUnitPriority('hour', 13); -+ -+ // PARSING -+ -+ function matchMeridiem(isStrict, locale) { -+ return locale._meridiemParse; -+ } -+ -+ addRegexToken('a', matchMeridiem); -+ addRegexToken('A', matchMeridiem); -+ addRegexToken('H', match1to2); -+ addRegexToken('h', match1to2); -+ addRegexToken('k', match1to2); -+ addRegexToken('HH', match1to2, match2); -+ addRegexToken('hh', match1to2, match2); -+ addRegexToken('kk', match1to2, match2); -+ -+ addRegexToken('hmm', match3to4); -+ addRegexToken('hmmss', match5to6); -+ addRegexToken('Hmm', match3to4); -+ addRegexToken('Hmmss', match5to6); -+ -+ addParseToken(['H', 'HH'], HOUR); -+ addParseToken(['k', 'kk'], function (input, array, config) { -+ var kInput = toInt(input); -+ array[HOUR] = kInput === 24 ? 0 : kInput; -+ }); -+ addParseToken(['a', 'A'], function (input, array, config) { -+ config._isPm = config._locale.isPM(input); -+ config._meridiem = input; -+ }); -+ addParseToken(['h', 'hh'], function (input, array, config) { -+ array[HOUR] = toInt(input); -+ getParsingFlags(config).bigHour = true; -+ }); -+ addParseToken('hmm', function (input, array, config) { -+ var pos = input.length - 2; -+ array[HOUR] = toInt(input.substr(0, pos)); -+ array[MINUTE] = toInt(input.substr(pos)); -+ getParsingFlags(config).bigHour = true; -+ }); -+ addParseToken('hmmss', function (input, array, config) { -+ var pos1 = input.length - 4, -+ pos2 = input.length - 2; -+ array[HOUR] = toInt(input.substr(0, pos1)); -+ array[MINUTE] = toInt(input.substr(pos1, 2)); -+ array[SECOND] = toInt(input.substr(pos2)); -+ getParsingFlags(config).bigHour = true; -+ }); -+ addParseToken('Hmm', function (input, array, config) { -+ var pos = input.length - 2; -+ array[HOUR] = toInt(input.substr(0, pos)); -+ array[MINUTE] = toInt(input.substr(pos)); -+ }); -+ addParseToken('Hmmss', function (input, array, config) { -+ var pos1 = input.length - 4, -+ pos2 = input.length - 2; -+ array[HOUR] = toInt(input.substr(0, pos1)); -+ array[MINUTE] = toInt(input.substr(pos1, 2)); -+ array[SECOND] = toInt(input.substr(pos2)); -+ }); -+ -+ // LOCALES -+ -+ function localeIsPM(input) { -+ // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays -+ // Using charAt should be more compatible. -+ return (input + '').toLowerCase().charAt(0) === 'p'; -+ } -+ -+ var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i, -+ // Setting the hour should keep the time, because the user explicitly -+ // specified which hour they want. So trying to maintain the same hour (in -+ // a new timezone) makes sense. Adding/subtracting hours does not follow -+ // this rule. -+ getSetHour = makeGetSet('Hours', true); -+ -+ function localeMeridiem(hours, minutes, isLower) { -+ if (hours > 11) { -+ return isLower ? 'pm' : 'PM'; -+ } else { -+ return isLower ? 'am' : 'AM'; -+ } -+ } -+ -+ var baseConfig = { -+ calendar: defaultCalendar, -+ longDateFormat: defaultLongDateFormat, -+ invalidDate: defaultInvalidDate, -+ ordinal: defaultOrdinal, -+ dayOfMonthOrdinalParse: defaultDayOfMonthOrdinalParse, -+ relativeTime: defaultRelativeTime, -+ -+ months: defaultLocaleMonths, -+ monthsShort: defaultLocaleMonthsShort, -+ -+ week: defaultLocaleWeek, -+ -+ weekdays: defaultLocaleWeekdays, -+ weekdaysMin: defaultLocaleWeekdaysMin, -+ weekdaysShort: defaultLocaleWeekdaysShort, -+ -+ meridiemParse: defaultLocaleMeridiemParse, -+ }; -+ -+ // internal storage for locale config files -+ var locales = {}, -+ localeFamilies = {}, -+ globalLocale; -+ -+ function commonPrefix(arr1, arr2) { -+ var i, -+ minl = Math.min(arr1.length, arr2.length); -+ for (i = 0; i < minl; i += 1) { -+ if (arr1[i] !== arr2[i]) { -+ return i; -+ } -+ } -+ return minl; -+ } -+ -+ function normalizeLocale(key) { -+ return key ? key.toLowerCase().replace('_', '-') : key; -+ } -+ -+ // pick the locale from the array -+ // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each -+ // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root -+ function chooseLocale(names) { -+ var i = 0, -+ j, -+ next, -+ locale, -+ split; -+ -+ while (i < names.length) { -+ split = normalizeLocale(names[i]).split('-'); -+ j = split.length; -+ next = normalizeLocale(names[i + 1]); -+ next = next ? next.split('-') : null; -+ while (j > 0) { -+ locale = loadLocale(split.slice(0, j).join('-')); -+ if (locale) { -+ return locale; -+ } -+ if ( -+ next && -+ next.length >= j && -+ commonPrefix(split, next) >= j - 1 -+ ) { -+ //the next array item is better than a shallower substring of this one -+ break; -+ } -+ j--; -+ } -+ i++; -+ } -+ return globalLocale; -+ } -+ -+ function loadLocale(name) { -+ var oldLocale = null, -+ aliasedRequire; -+ // TODO: Find a better way to register and load all the locales in Node -+ if ( -+ locales[name] === undefined && -+ typeof module !== 'undefined' && -+ module && -+ module.exports -+ ) { -+ try { -+ oldLocale = globalLocale._abbr; -+ aliasedRequire = require; -+ aliasedRequire('./locale/' + name); -+ getSetGlobalLocale(oldLocale); -+ } catch (e) { -+ // mark as not found to avoid repeating expensive file require call causing high CPU -+ // when trying to find en-US, en_US, en-us for every format call -+ locales[name] = null; // null means not found -+ } -+ } -+ return locales[name]; -+ } -+ -+ // This function will load locale and then set the global locale. If -+ // no arguments are passed in, it will simply return the current global -+ // locale key. -+ function getSetGlobalLocale(key, values) { -+ var data; -+ if (key) { -+ if (isUndefined(values)) { -+ data = getLocale(key); -+ } else { -+ data = defineLocale(key, values); -+ } -+ -+ if (data) { -+ // moment.duration._locale = moment._locale = data; -+ globalLocale = data; -+ } else { -+ if (typeof console !== 'undefined' && console.warn) { -+ //warn user if arguments are passed but the locale could not be set -+ console.warn( -+ 'Locale ' + key + ' not found. Did you forget to load it?' -+ ); -+ } -+ } -+ } -+ -+ return globalLocale._abbr; -+ } -+ -+ function defineLocale(name, config) { -+ if (config !== null) { -+ var locale, -+ parentConfig = baseConfig; -+ config.abbr = name; -+ if (locales[name] != null) { -+ deprecateSimple( -+ 'defineLocaleOverride', -+ 'use moment.updateLocale(localeName, config) to change ' + -+ 'an existing locale. moment.defineLocale(localeName, ' + -+ 'config) should only be used for creating a new locale ' + -+ 'See http://momentjs.com/guides/#/warnings/define-locale/ for more info.' -+ ); -+ parentConfig = locales[name]._config; -+ } else if (config.parentLocale != null) { -+ if (locales[config.parentLocale] != null) { -+ parentConfig = locales[config.parentLocale]._config; -+ } else { -+ locale = loadLocale(config.parentLocale); -+ if (locale != null) { -+ parentConfig = locale._config; -+ } else { -+ if (!localeFamilies[config.parentLocale]) { -+ localeFamilies[config.parentLocale] = []; -+ } -+ localeFamilies[config.parentLocale].push({ -+ name: name, -+ config: config, -+ }); -+ return null; -+ } -+ } -+ } -+ locales[name] = new Locale(mergeConfigs(parentConfig, config)); -+ -+ if (localeFamilies[name]) { -+ localeFamilies[name].forEach(function (x) { -+ defineLocale(x.name, x.config); -+ }); -+ } -+ -+ // backwards compat for now: also set the locale -+ // make sure we set the locale AFTER all child locales have been -+ // created, so we won't end up with the child locale set. -+ getSetGlobalLocale(name); -+ -+ return locales[name]; -+ } else { -+ // useful for testing -+ delete locales[name]; -+ return null; -+ } -+ } -+ -+ function updateLocale(name, config) { -+ if (config != null) { -+ var locale, -+ tmpLocale, -+ parentConfig = baseConfig; -+ -+ if (locales[name] != null && locales[name].parentLocale != null) { -+ // Update existing child locale in-place to avoid memory-leaks -+ locales[name].set(mergeConfigs(locales[name]._config, config)); -+ } else { -+ // MERGE -+ tmpLocale = loadLocale(name); -+ if (tmpLocale != null) { -+ parentConfig = tmpLocale._config; -+ } -+ config = mergeConfigs(parentConfig, config); -+ if (tmpLocale == null) { -+ // updateLocale is called for creating a new locale -+ // Set abbr so it will have a name (getters return -+ // undefined otherwise). -+ config.abbr = name; -+ } -+ locale = new Locale(config); -+ locale.parentLocale = locales[name]; -+ locales[name] = locale; -+ } -+ -+ // backwards compat for now: also set the locale -+ getSetGlobalLocale(name); -+ } else { -+ // pass null for config to unupdate, useful for tests -+ if (locales[name] != null) { -+ if (locales[name].parentLocale != null) { -+ locales[name] = locales[name].parentLocale; -+ if (name === getSetGlobalLocale()) { -+ getSetGlobalLocale(name); -+ } -+ } else if (locales[name] != null) { -+ delete locales[name]; -+ } -+ } -+ } -+ return locales[name]; -+ } -+ -+ // returns locale data -+ function getLocale(key) { -+ var locale; -+ -+ if (key && key._locale && key._locale._abbr) { -+ key = key._locale._abbr; -+ } -+ -+ if (!key) { -+ return globalLocale; -+ } -+ -+ if (!isArray(key)) { -+ //short-circuit everything else -+ locale = loadLocale(key); -+ if (locale) { -+ return locale; -+ } -+ key = [key]; -+ } -+ -+ return chooseLocale(key); -+ } -+ -+ function listLocales() { -+ return keys(locales); -+ } -+ -+ function checkOverflow(m) { -+ var overflow, -+ a = m._a; -+ -+ if (a && getParsingFlags(m).overflow === -2) { -+ overflow = -+ a[MONTH] < 0 || a[MONTH] > 11 -+ ? MONTH -+ : a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) -+ ? DATE -+ : a[HOUR] < 0 || -+ a[HOUR] > 24 || -+ (a[HOUR] === 24 && -+ (a[MINUTE] !== 0 || -+ a[SECOND] !== 0 || -+ a[MILLISECOND] !== 0)) -+ ? HOUR -+ : a[MINUTE] < 0 || a[MINUTE] > 59 -+ ? MINUTE -+ : a[SECOND] < 0 || a[SECOND] > 59 -+ ? SECOND -+ : a[MILLISECOND] < 0 || a[MILLISECOND] > 999 -+ ? MILLISECOND -+ : -1; -+ -+ if ( -+ getParsingFlags(m)._overflowDayOfYear && -+ (overflow < YEAR || overflow > DATE) -+ ) { -+ overflow = DATE; -+ } -+ if (getParsingFlags(m)._overflowWeeks && overflow === -1) { -+ overflow = WEEK; -+ } -+ if (getParsingFlags(m)._overflowWeekday && overflow === -1) { -+ overflow = WEEKDAY; -+ } -+ -+ getParsingFlags(m).overflow = overflow; -+ } -+ -+ return m; -+ } -+ -+ // iso 8601 regex -+ // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00) -+ var extendedIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/, -+ basicIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d|))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/, -+ tzRegex = /Z|[+-]\d\d(?::?\d\d)?/, -+ isoDates = [ -+ ['YYYYYY-MM-DD', /[+-]\d{6}-\d\d-\d\d/], -+ ['YYYY-MM-DD', /\d{4}-\d\d-\d\d/], -+ ['GGGG-[W]WW-E', /\d{4}-W\d\d-\d/], -+ ['GGGG-[W]WW', /\d{4}-W\d\d/, false], -+ ['YYYY-DDD', /\d{4}-\d{3}/], -+ ['YYYY-MM', /\d{4}-\d\d/, false], -+ ['YYYYYYMMDD', /[+-]\d{10}/], -+ ['YYYYMMDD', /\d{8}/], -+ ['GGGG[W]WWE', /\d{4}W\d{3}/], -+ ['GGGG[W]WW', /\d{4}W\d{2}/, false], -+ ['YYYYDDD', /\d{7}/], -+ ['YYYYMM', /\d{6}/, false], -+ ['YYYY', /\d{4}/, false], -+ ], -+ // iso time formats and regexes -+ isoTimes = [ -+ ['HH:mm:ss.SSSS', /\d\d:\d\d:\d\d\.\d+/], -+ ['HH:mm:ss,SSSS', /\d\d:\d\d:\d\d,\d+/], -+ ['HH:mm:ss', /\d\d:\d\d:\d\d/], -+ ['HH:mm', /\d\d:\d\d/], -+ ['HHmmss.SSSS', /\d\d\d\d\d\d\.\d+/], -+ ['HHmmss,SSSS', /\d\d\d\d\d\d,\d+/], -+ ['HHmmss', /\d\d\d\d\d\d/], -+ ['HHmm', /\d\d\d\d/], -+ ['HH', /\d\d/], -+ ], -+ aspNetJsonRegex = /^\/?Date\((-?\d+)/i, -+ // RFC 2822 regex: For details see https://tools.ietf.org/html/rfc2822#section-3.3 -+ rfc2822 = /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/, -+ obsOffsets = { -+ UT: 0, -+ GMT: 0, -+ EDT: -4 * 60, -+ EST: -5 * 60, -+ CDT: -5 * 60, -+ CST: -6 * 60, -+ MDT: -6 * 60, -+ MST: -7 * 60, -+ PDT: -7 * 60, -+ PST: -8 * 60, -+ }; -+ -+ // date from iso format -+ function configFromISO(config) { -+ var i, -+ l, -+ string = config._i, -+ match = extendedIsoRegex.exec(string) || basicIsoRegex.exec(string), -+ allowTime, -+ dateFormat, -+ timeFormat, -+ tzFormat; -+ -+ if (match) { -+ getParsingFlags(config).iso = true; -+ -+ for (i = 0, l = isoDates.length; i < l; i++) { -+ if (isoDates[i][1].exec(match[1])) { -+ dateFormat = isoDates[i][0]; -+ allowTime = isoDates[i][2] !== false; -+ break; -+ } -+ } -+ if (dateFormat == null) { -+ config._isValid = false; -+ return; -+ } -+ if (match[3]) { -+ for (i = 0, l = isoTimes.length; i < l; i++) { -+ if (isoTimes[i][1].exec(match[3])) { -+ // match[2] should be 'T' or space -+ timeFormat = (match[2] || ' ') + isoTimes[i][0]; -+ break; -+ } -+ } -+ if (timeFormat == null) { -+ config._isValid = false; -+ return; -+ } -+ } -+ if (!allowTime && timeFormat != null) { -+ config._isValid = false; -+ return; -+ } -+ if (match[4]) { -+ if (tzRegex.exec(match[4])) { -+ tzFormat = 'Z'; -+ } else { -+ config._isValid = false; -+ return; -+ } -+ } -+ config._f = dateFormat + (timeFormat || '') + (tzFormat || ''); -+ configFromStringAndFormat(config); -+ } else { -+ config._isValid = false; -+ } -+ } -+ -+ function extractFromRFC2822Strings( -+ yearStr, -+ monthStr, -+ dayStr, -+ hourStr, -+ minuteStr, -+ secondStr -+ ) { -+ var result = [ -+ untruncateYear(yearStr), -+ defaultLocaleMonthsShort.indexOf(monthStr), -+ parseInt(dayStr, 10), -+ parseInt(hourStr, 10), -+ parseInt(minuteStr, 10), -+ ]; -+ -+ if (secondStr) { -+ result.push(parseInt(secondStr, 10)); -+ } -+ -+ return result; -+ } -+ -+ function untruncateYear(yearStr) { -+ var year = parseInt(yearStr, 10); -+ if (year <= 49) { -+ return 2000 + year; -+ } else if (year <= 999) { -+ return 1900 + year; -+ } -+ return year; -+ } -+ -+ function preprocessRFC2822(s) { -+ // Remove comments and folding whitespace and replace multiple-spaces with a single space -+ return s -+ .replace(/\([^)]*\)|[\n\t]/g, ' ') -+ .replace(/(\s\s+)/g, ' ') -+ .replace(/^\s\s*/, '') -+ .replace(/\s\s*$/, ''); -+ } -+ -+ function checkWeekday(weekdayStr, parsedInput, config) { -+ if (weekdayStr) { -+ // TODO: Replace the vanilla JS Date object with an independent day-of-week check. -+ var weekdayProvided = defaultLocaleWeekdaysShort.indexOf(weekdayStr), -+ weekdayActual = new Date( -+ parsedInput[0], -+ parsedInput[1], -+ parsedInput[2] -+ ).getDay(); -+ if (weekdayProvided !== weekdayActual) { -+ getParsingFlags(config).weekdayMismatch = true; -+ config._isValid = false; -+ return false; -+ } -+ } -+ return true; -+ } -+ -+ function calculateOffset(obsOffset, militaryOffset, numOffset) { -+ if (obsOffset) { -+ return obsOffsets[obsOffset]; -+ } else if (militaryOffset) { -+ // the only allowed military tz is Z -+ return 0; -+ } else { -+ var hm = parseInt(numOffset, 10), -+ m = hm % 100, -+ h = (hm - m) / 100; -+ return h * 60 + m; -+ } -+ } -+ -+ // date and time from ref 2822 format -+ function configFromRFC2822(config) { -+ var match = rfc2822.exec(preprocessRFC2822(config._i)), -+ parsedArray; -+ if (match) { -+ parsedArray = extractFromRFC2822Strings( -+ match[4], -+ match[3], -+ match[2], -+ match[5], -+ match[6], -+ match[7] -+ ); -+ if (!checkWeekday(match[1], parsedArray, config)) { -+ return; -+ } -+ -+ config._a = parsedArray; -+ config._tzm = calculateOffset(match[8], match[9], match[10]); -+ -+ config._d = createUTCDate.apply(null, config._a); -+ config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); -+ -+ getParsingFlags(config).rfc2822 = true; -+ } else { -+ config._isValid = false; -+ } -+ } -+ -+ // date from 1) ASP.NET, 2) ISO, 3) RFC 2822 formats, or 4) optional fallback if parsing isn't strict -+ function configFromString(config) { -+ var matched = aspNetJsonRegex.exec(config._i); -+ if (matched !== null) { -+ config._d = new Date(+matched[1]); -+ return; -+ } -+ -+ configFromISO(config); -+ if (config._isValid === false) { -+ delete config._isValid; -+ } else { -+ return; -+ } -+ -+ configFromRFC2822(config); -+ if (config._isValid === false) { -+ delete config._isValid; -+ } else { -+ return; -+ } -+ -+ if (config._strict) { -+ config._isValid = false; -+ } else { -+ // Final attempt, use Input Fallback -+ hooks.createFromInputFallback(config); -+ } -+ } -+ -+ hooks.createFromInputFallback = deprecate( -+ 'value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), ' + -+ 'which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are ' + -+ 'discouraged. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.', -+ function (config) { -+ config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); -+ } -+ ); -+ -+ // Pick the first defined of two or three arguments. -+ function defaults(a, b, c) { -+ if (a != null) { -+ return a; -+ } -+ if (b != null) { -+ return b; -+ } -+ return c; -+ } -+ -+ function currentDateArray(config) { -+ // hooks is actually the exported moment object -+ var nowValue = new Date(hooks.now()); -+ if (config._useUTC) { -+ return [ -+ nowValue.getUTCFullYear(), -+ nowValue.getUTCMonth(), -+ nowValue.getUTCDate(), -+ ]; -+ } -+ return [nowValue.getFullYear(), nowValue.getMonth(), nowValue.getDate()]; -+ } -+ -+ // convert an array to a date. -+ // the array should mirror the parameters below -+ // note: all values past the year are optional and will default to the lowest possible value. -+ // [year, month, day , hour, minute, second, millisecond] -+ function configFromArray(config) { -+ var i, -+ date, -+ input = [], -+ currentDate, -+ expectedWeekday, -+ yearToUse; -+ -+ if (config._d) { -+ return; -+ } -+ -+ currentDate = currentDateArray(config); -+ -+ //compute day of the year from weeks and weekdays -+ if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { -+ dayOfYearFromWeekInfo(config); -+ } -+ -+ //if the day of the year is set, figure out what it is -+ if (config._dayOfYear != null) { -+ yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); -+ -+ if ( -+ config._dayOfYear > daysInYear(yearToUse) || -+ config._dayOfYear === 0 -+ ) { -+ getParsingFlags(config)._overflowDayOfYear = true; -+ } -+ -+ date = createUTCDate(yearToUse, 0, config._dayOfYear); -+ config._a[MONTH] = date.getUTCMonth(); -+ config._a[DATE] = date.getUTCDate(); -+ } -+ -+ // Default to current date. -+ // * if no year, month, day of month are given, default to today -+ // * if day of month is given, default month and year -+ // * if month is given, default only year -+ // * if year is given, don't default anything -+ for (i = 0; i < 3 && config._a[i] == null; ++i) { -+ config._a[i] = input[i] = currentDate[i]; -+ } -+ -+ // Zero out whatever was not defaulted, including time -+ for (; i < 7; i++) { -+ config._a[i] = input[i] = -+ config._a[i] == null ? (i === 2 ? 1 : 0) : config._a[i]; -+ } -+ -+ // Check for 24:00:00.000 -+ if ( -+ config._a[HOUR] === 24 && -+ config._a[MINUTE] === 0 && -+ config._a[SECOND] === 0 && -+ config._a[MILLISECOND] === 0 -+ ) { -+ config._nextDay = true; -+ config._a[HOUR] = 0; -+ } -+ -+ config._d = (config._useUTC ? createUTCDate : createDate).apply( -+ null, -+ input -+ ); -+ expectedWeekday = config._useUTC -+ ? config._d.getUTCDay() -+ : config._d.getDay(); -+ -+ // Apply timezone offset from input. The actual utcOffset can be changed -+ // with parseZone. -+ if (config._tzm != null) { -+ config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); -+ } -+ -+ if (config._nextDay) { -+ config._a[HOUR] = 24; -+ } -+ -+ // check for mismatching day of week -+ if ( -+ config._w && -+ typeof config._w.d !== 'undefined' && -+ config._w.d !== expectedWeekday -+ ) { -+ getParsingFlags(config).weekdayMismatch = true; -+ } -+ } -+ -+ function dayOfYearFromWeekInfo(config) { -+ var w, weekYear, week, weekday, dow, doy, temp, weekdayOverflow, curWeek; -+ -+ w = config._w; -+ if (w.GG != null || w.W != null || w.E != null) { -+ dow = 1; -+ doy = 4; -+ -+ // TODO: We need to take the current isoWeekYear, but that depends on -+ // how we interpret now (local, utc, fixed offset). So create -+ // a now version of current config (take local/utc/offset flags, and -+ // create now). -+ weekYear = defaults( -+ w.GG, -+ config._a[YEAR], -+ weekOfYear(createLocal(), 1, 4).year -+ ); -+ week = defaults(w.W, 1); -+ weekday = defaults(w.E, 1); -+ if (weekday < 1 || weekday > 7) { -+ weekdayOverflow = true; -+ } -+ } else { -+ dow = config._locale._week.dow; -+ doy = config._locale._week.doy; -+ -+ curWeek = weekOfYear(createLocal(), dow, doy); -+ -+ weekYear = defaults(w.gg, config._a[YEAR], curWeek.year); -+ -+ // Default to current week. -+ week = defaults(w.w, curWeek.week); -+ -+ if (w.d != null) { -+ // weekday -- low day numbers are considered next week -+ weekday = w.d; -+ if (weekday < 0 || weekday > 6) { -+ weekdayOverflow = true; -+ } -+ } else if (w.e != null) { -+ // local weekday -- counting starts from beginning of week -+ weekday = w.e + dow; -+ if (w.e < 0 || w.e > 6) { -+ weekdayOverflow = true; -+ } -+ } else { -+ // default to beginning of week -+ weekday = dow; -+ } -+ } -+ if (week < 1 || week > weeksInYear(weekYear, dow, doy)) { -+ getParsingFlags(config)._overflowWeeks = true; -+ } else if (weekdayOverflow != null) { -+ getParsingFlags(config)._overflowWeekday = true; -+ } else { -+ temp = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy); -+ config._a[YEAR] = temp.year; -+ config._dayOfYear = temp.dayOfYear; -+ } -+ } -+ -+ // constant that refers to the ISO standard -+ hooks.ISO_8601 = function () {}; -+ -+ // constant that refers to the RFC 2822 form -+ hooks.RFC_2822 = function () {}; -+ -+ // date from string and format string -+ function configFromStringAndFormat(config) { -+ // TODO: Move this to another part of the creation flow to prevent circular deps -+ if (config._f === hooks.ISO_8601) { -+ configFromISO(config); -+ return; -+ } -+ if (config._f === hooks.RFC_2822) { -+ configFromRFC2822(config); -+ return; -+ } -+ config._a = []; -+ getParsingFlags(config).empty = true; -+ -+ // This array is used to make a Date, either with `new Date` or `Date.UTC` -+ var string = '' + config._i, -+ i, -+ parsedInput, -+ tokens, -+ token, -+ skipped, -+ stringLength = string.length, -+ totalParsedInputLength = 0, -+ era; -+ -+ tokens = -+ expandFormat(config._f, config._locale).match(formattingTokens) || []; -+ -+ for (i = 0; i < tokens.length; i++) { -+ token = tokens[i]; -+ parsedInput = (string.match(getParseRegexForToken(token, config)) || -+ [])[0]; -+ if (parsedInput) { -+ skipped = string.substr(0, string.indexOf(parsedInput)); -+ if (skipped.length > 0) { -+ getParsingFlags(config).unusedInput.push(skipped); -+ } -+ string = string.slice( -+ string.indexOf(parsedInput) + parsedInput.length -+ ); -+ totalParsedInputLength += parsedInput.length; -+ } -+ // don't parse if it's not a known token -+ if (formatTokenFunctions[token]) { -+ if (parsedInput) { -+ getParsingFlags(config).empty = false; -+ } else { -+ getParsingFlags(config).unusedTokens.push(token); -+ } -+ addTimeToArrayFromToken(token, parsedInput, config); -+ } else if (config._strict && !parsedInput) { -+ getParsingFlags(config).unusedTokens.push(token); -+ } -+ } -+ -+ // add remaining unparsed input length to the string -+ getParsingFlags(config).charsLeftOver = -+ stringLength - totalParsedInputLength; -+ if (string.length > 0) { -+ getParsingFlags(config).unusedInput.push(string); -+ } -+ -+ // clear _12h flag if hour is <= 12 -+ if ( -+ config._a[HOUR] <= 12 && -+ getParsingFlags(config).bigHour === true && -+ config._a[HOUR] > 0 -+ ) { -+ getParsingFlags(config).bigHour = undefined; -+ } -+ -+ getParsingFlags(config).parsedDateParts = config._a.slice(0); -+ getParsingFlags(config).meridiem = config._meridiem; -+ // handle meridiem -+ config._a[HOUR] = meridiemFixWrap( -+ config._locale, -+ config._a[HOUR], -+ config._meridiem -+ ); -+ -+ // handle era -+ era = getParsingFlags(config).era; -+ if (era !== null) { -+ config._a[YEAR] = config._locale.erasConvertYear(era, config._a[YEAR]); -+ } -+ -+ configFromArray(config); -+ checkOverflow(config); -+ } -+ -+ function meridiemFixWrap(locale, hour, meridiem) { -+ var isPm; -+ -+ if (meridiem == null) { -+ // nothing to do -+ return hour; -+ } -+ if (locale.meridiemHour != null) { -+ return locale.meridiemHour(hour, meridiem); -+ } else if (locale.isPM != null) { -+ // Fallback -+ isPm = locale.isPM(meridiem); -+ if (isPm && hour < 12) { -+ hour += 12; -+ } -+ if (!isPm && hour === 12) { -+ hour = 0; -+ } -+ return hour; -+ } else { -+ // this is not supposed to happen -+ return hour; -+ } -+ } -+ -+ // date from string and array of format strings -+ function configFromStringAndArray(config) { -+ var tempConfig, -+ bestMoment, -+ scoreToBeat, -+ i, -+ currentScore, -+ validFormatFound, -+ bestFormatIsValid = false; -+ -+ if (config._f.length === 0) { -+ getParsingFlags(config).invalidFormat = true; -+ config._d = new Date(NaN); -+ return; -+ } -+ -+ for (i = 0; i < config._f.length; i++) { -+ currentScore = 0; -+ validFormatFound = false; -+ tempConfig = copyConfig({}, config); -+ if (config._useUTC != null) { -+ tempConfig._useUTC = config._useUTC; -+ } -+ tempConfig._f = config._f[i]; -+ configFromStringAndFormat(tempConfig); -+ -+ if (isValid(tempConfig)) { -+ validFormatFound = true; -+ } -+ -+ // if there is any input that was not parsed add a penalty for that format -+ currentScore += getParsingFlags(tempConfig).charsLeftOver; -+ -+ //or tokens -+ currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; -+ -+ getParsingFlags(tempConfig).score = currentScore; -+ -+ if (!bestFormatIsValid) { -+ if ( -+ scoreToBeat == null || -+ currentScore < scoreToBeat || -+ validFormatFound -+ ) { -+ scoreToBeat = currentScore; -+ bestMoment = tempConfig; -+ if (validFormatFound) { -+ bestFormatIsValid = true; -+ } -+ } -+ } else { -+ if (currentScore < scoreToBeat) { -+ scoreToBeat = currentScore; -+ bestMoment = tempConfig; -+ } -+ } -+ } -+ -+ extend(config, bestMoment || tempConfig); -+ } -+ -+ function configFromObject(config) { -+ if (config._d) { -+ return; -+ } -+ -+ var i = normalizeObjectUnits(config._i), -+ dayOrDate = i.day === undefined ? i.date : i.day; -+ config._a = map( -+ [i.year, i.month, dayOrDate, i.hour, i.minute, i.second, i.millisecond], -+ function (obj) { -+ return obj && parseInt(obj, 10); -+ } -+ ); -+ -+ configFromArray(config); -+ } -+ -+ function createFromConfig(config) { -+ var res = new Moment(checkOverflow(prepareConfig(config))); -+ if (res._nextDay) { -+ // Adding is smart enough around DST -+ res.add(1, 'd'); -+ res._nextDay = undefined; -+ } -+ -+ return res; -+ } -+ -+ function prepareConfig(config) { -+ var input = config._i, -+ format = config._f; -+ -+ config._locale = config._locale || getLocale(config._l); -+ -+ if (input === null || (format === undefined && input === '')) { -+ return createInvalid({ nullInput: true }); -+ } -+ -+ if (typeof input === 'string') { -+ config._i = input = config._locale.preparse(input); -+ } -+ -+ if (isMoment(input)) { -+ return new Moment(checkOverflow(input)); -+ } else if (isDate(input)) { -+ config._d = input; -+ } else if (isArray(format)) { -+ configFromStringAndArray(config); -+ } else if (format) { -+ configFromStringAndFormat(config); -+ } else { -+ configFromInput(config); -+ } -+ -+ if (!isValid(config)) { -+ config._d = null; -+ } -+ -+ return config; -+ } -+ -+ function configFromInput(config) { -+ var input = config._i; -+ if (isUndefined(input)) { -+ config._d = new Date(hooks.now()); -+ } else if (isDate(input)) { -+ config._d = new Date(input.valueOf()); -+ } else if (typeof input === 'string') { -+ configFromString(config); -+ } else if (isArray(input)) { -+ config._a = map(input.slice(0), function (obj) { -+ return parseInt(obj, 10); -+ }); -+ configFromArray(config); -+ } else if (isObject(input)) { -+ configFromObject(config); -+ } else if (isNumber(input)) { -+ // from milliseconds -+ config._d = new Date(input); -+ } else { -+ hooks.createFromInputFallback(config); -+ } -+ } -+ -+ function createLocalOrUTC(input, format, locale, strict, isUTC) { -+ var c = {}; -+ -+ if (format === true || format === false) { -+ strict = format; -+ format = undefined; -+ } -+ -+ if (locale === true || locale === false) { -+ strict = locale; -+ locale = undefined; -+ } -+ -+ if ( -+ (isObject(input) && isObjectEmpty(input)) || -+ (isArray(input) && input.length === 0) -+ ) { -+ input = undefined; -+ } -+ // object construction must be done this way. -+ // https://github.com/moment/moment/issues/1423 -+ c._isAMomentObject = true; -+ c._useUTC = c._isUTC = isUTC; -+ c._l = locale; -+ c._i = input; -+ c._f = format; -+ c._strict = strict; -+ -+ return createFromConfig(c); -+ } -+ -+ function createLocal(input, format, locale, strict) { -+ return createLocalOrUTC(input, format, locale, strict, false); -+ } -+ -+ var prototypeMin = deprecate( -+ 'moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/', -+ function () { -+ var other = createLocal.apply(null, arguments); -+ if (this.isValid() && other.isValid()) { -+ return other < this ? this : other; -+ } else { -+ return createInvalid(); -+ } -+ } -+ ), -+ prototypeMax = deprecate( -+ 'moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/', -+ function () { -+ var other = createLocal.apply(null, arguments); -+ if (this.isValid() && other.isValid()) { -+ return other > this ? this : other; -+ } else { -+ return createInvalid(); -+ } -+ } -+ ); -+ -+ // Pick a moment m from moments so that m[fn](other) is true for all -+ // other. This relies on the function fn to be transitive. -+ // -+ // moments should either be an array of moment objects or an array, whose -+ // first element is an array of moment objects. -+ function pickBy(fn, moments) { -+ var res, i; -+ if (moments.length === 1 && isArray(moments[0])) { -+ moments = moments[0]; -+ } -+ if (!moments.length) { -+ return createLocal(); -+ } -+ res = moments[0]; -+ for (i = 1; i < moments.length; ++i) { -+ if (!moments[i].isValid() || moments[i][fn](res)) { -+ res = moments[i]; -+ } -+ } -+ return res; -+ } -+ -+ // TODO: Use [].sort instead? -+ function min() { -+ var args = [].slice.call(arguments, 0); -+ -+ return pickBy('isBefore', args); -+ } -+ -+ function max() { -+ var args = [].slice.call(arguments, 0); -+ -+ return pickBy('isAfter', args); -+ } -+ -+ var now = function () { -+ return Date.now ? Date.now() : +new Date(); -+ }; -+ -+ var ordering = [ -+ 'year', -+ 'quarter', -+ 'month', -+ 'week', -+ 'day', -+ 'hour', -+ 'minute', -+ 'second', -+ 'millisecond', -+ ]; -+ -+ function isDurationValid(m) { -+ var key, -+ unitHasDecimal = false, -+ i; -+ for (key in m) { -+ if ( -+ hasOwnProp(m, key) && -+ !( -+ indexOf.call(ordering, key) !== -1 && -+ (m[key] == null || !isNaN(m[key])) -+ ) -+ ) { -+ return false; -+ } -+ } -+ -+ for (i = 0; i < ordering.length; ++i) { -+ if (m[ordering[i]]) { -+ if (unitHasDecimal) { -+ return false; // only allow non-integers for smallest unit -+ } -+ if (parseFloat(m[ordering[i]]) !== toInt(m[ordering[i]])) { -+ unitHasDecimal = true; -+ } -+ } -+ } -+ -+ return true; -+ } -+ -+ function isValid$1() { -+ return this._isValid; -+ } -+ -+ function createInvalid$1() { -+ return createDuration(NaN); -+ } -+ -+ function Duration(duration) { -+ var normalizedInput = normalizeObjectUnits(duration), -+ years = normalizedInput.year || 0, -+ quarters = normalizedInput.quarter || 0, -+ months = normalizedInput.month || 0, -+ weeks = normalizedInput.week || normalizedInput.isoWeek || 0, -+ days = normalizedInput.day || 0, -+ hours = normalizedInput.hour || 0, -+ minutes = normalizedInput.minute || 0, -+ seconds = normalizedInput.second || 0, -+ milliseconds = normalizedInput.millisecond || 0; -+ -+ this._isValid = isDurationValid(normalizedInput); -+ -+ // representation for dateAddRemove -+ this._milliseconds = -+ +milliseconds + -+ seconds * 1e3 + // 1000 -+ minutes * 6e4 + // 1000 * 60 -+ hours * 1000 * 60 * 60; //using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978 -+ // Because of dateAddRemove treats 24 hours as different from a -+ // day when working around DST, we need to store them separately -+ this._days = +days + weeks * 7; -+ // It is impossible to translate months into days without knowing -+ // which months you are are talking about, so we have to store -+ // it separately. -+ this._months = +months + quarters * 3 + years * 12; -+ -+ this._data = {}; -+ -+ this._locale = getLocale(); -+ -+ this._bubble(); -+ } -+ -+ function isDuration(obj) { -+ return obj instanceof Duration; -+ } -+ -+ function absRound(number) { -+ if (number < 0) { -+ return Math.round(-1 * number) * -1; -+ } else { -+ return Math.round(number); -+ } -+ } -+ -+ // compare two arrays, return the number of differences -+ function compareArrays(array1, array2, dontConvert) { -+ var len = Math.min(array1.length, array2.length), -+ lengthDiff = Math.abs(array1.length - array2.length), -+ diffs = 0, -+ i; -+ for (i = 0; i < len; i++) { -+ if ( -+ (dontConvert && array1[i] !== array2[i]) || -+ (!dontConvert && toInt(array1[i]) !== toInt(array2[i])) -+ ) { -+ diffs++; -+ } -+ } -+ return diffs + lengthDiff; -+ } -+ -+ // FORMATTING -+ -+ function offset(token, separator) { -+ addFormatToken(token, 0, 0, function () { -+ var offset = this.utcOffset(), -+ sign = '+'; -+ if (offset < 0) { -+ offset = -offset; -+ sign = '-'; -+ } -+ return ( -+ sign + -+ zeroFill(~~(offset / 60), 2) + -+ separator + -+ zeroFill(~~offset % 60, 2) -+ ); -+ }); -+ } -+ -+ offset('Z', ':'); -+ offset('ZZ', ''); -+ -+ // PARSING -+ -+ addRegexToken('Z', matchShortOffset); -+ addRegexToken('ZZ', matchShortOffset); -+ addParseToken(['Z', 'ZZ'], function (input, array, config) { -+ config._useUTC = true; -+ config._tzm = offsetFromString(matchShortOffset, input); -+ }); -+ -+ // HELPERS -+ -+ // timezone chunker -+ // '+10:00' > ['10', '00'] -+ // '-1530' > ['-15', '30'] -+ var chunkOffset = /([\+\-]|\d\d)/gi; -+ -+ function offsetFromString(matcher, string) { -+ var matches = (string || '').match(matcher), -+ chunk, -+ parts, -+ minutes; -+ -+ if (matches === null) { -+ return null; -+ } -+ -+ chunk = matches[matches.length - 1] || []; -+ parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; -+ minutes = +(parts[1] * 60) + toInt(parts[2]); -+ -+ return minutes === 0 ? 0 : parts[0] === '+' ? minutes : -minutes; -+ } -+ -+ // Return a moment from input, that is local/utc/zone equivalent to model. -+ function cloneWithOffset(input, model) { -+ var res, diff; -+ if (model._isUTC) { -+ res = model.clone(); -+ diff = -+ (isMoment(input) || isDate(input) -+ ? input.valueOf() -+ : createLocal(input).valueOf()) - res.valueOf(); -+ // Use low-level api, because this fn is low-level api. -+ res._d.setTime(res._d.valueOf() + diff); -+ hooks.updateOffset(res, false); -+ return res; -+ } else { -+ return createLocal(input).local(); -+ } -+ } -+ -+ function getDateOffset(m) { -+ // On Firefox.24 Date#getTimezoneOffset returns a floating point. -+ // https://github.com/moment/moment/pull/1871 -+ return -Math.round(m._d.getTimezoneOffset()); -+ } -+ -+ // HOOKS -+ -+ // This function will be called whenever a moment is mutated. -+ // It is intended to keep the offset in sync with the timezone. -+ hooks.updateOffset = function () {}; -+ -+ // MOMENTS -+ -+ // keepLocalTime = true means only change the timezone, without -+ // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> -+ // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset -+ // +0200, so we adjust the time as needed, to be valid. -+ // -+ // Keeping the time actually adds/subtracts (one hour) -+ // from the actual represented time. That is why we call updateOffset -+ // a second time. In case it wants us to change the offset again -+ // _changeInProgress == true case, then we have to adjust, because -+ // there is no such time in the given timezone. -+ function getSetOffset(input, keepLocalTime, keepMinutes) { -+ var offset = this._offset || 0, -+ localAdjust; -+ if (!this.isValid()) { -+ return input != null ? this : NaN; -+ } -+ if (input != null) { -+ if (typeof input === 'string') { -+ input = offsetFromString(matchShortOffset, input); -+ if (input === null) { -+ return this; -+ } -+ } else if (Math.abs(input) < 16 && !keepMinutes) { -+ input = input * 60; -+ } -+ if (!this._isUTC && keepLocalTime) { -+ localAdjust = getDateOffset(this); -+ } -+ this._offset = input; -+ this._isUTC = true; -+ if (localAdjust != null) { -+ this.add(localAdjust, 'm'); -+ } -+ if (offset !== input) { -+ if (!keepLocalTime || this._changeInProgress) { -+ addSubtract( -+ this, -+ createDuration(input - offset, 'm'), -+ 1, -+ false -+ ); -+ } else if (!this._changeInProgress) { -+ this._changeInProgress = true; -+ hooks.updateOffset(this, true); -+ this._changeInProgress = null; -+ } -+ } -+ return this; -+ } else { -+ return this._isUTC ? offset : getDateOffset(this); -+ } -+ } -+ -+ function getSetZone(input, keepLocalTime) { -+ if (input != null) { -+ if (typeof input !== 'string') { -+ input = -input; -+ } -+ -+ this.utcOffset(input, keepLocalTime); -+ -+ return this; -+ } else { -+ return -this.utcOffset(); -+ } -+ } -+ -+ function setOffsetToUTC(keepLocalTime) { -+ return this.utcOffset(0, keepLocalTime); -+ } -+ -+ function setOffsetToLocal(keepLocalTime) { -+ if (this._isUTC) { -+ this.utcOffset(0, keepLocalTime); -+ this._isUTC = false; -+ -+ if (keepLocalTime) { -+ this.subtract(getDateOffset(this), 'm'); -+ } -+ } -+ return this; -+ } -+ -+ function setOffsetToParsedOffset() { -+ if (this._tzm != null) { -+ this.utcOffset(this._tzm, false, true); -+ } else if (typeof this._i === 'string') { -+ var tZone = offsetFromString(matchOffset, this._i); -+ if (tZone != null) { -+ this.utcOffset(tZone); -+ } else { -+ this.utcOffset(0, true); -+ } -+ } -+ return this; -+ } -+ -+ function hasAlignedHourOffset(input) { -+ if (!this.isValid()) { -+ return false; -+ } -+ input = input ? createLocal(input).utcOffset() : 0; -+ -+ return (this.utcOffset() - input) % 60 === 0; -+ } -+ -+ function isDaylightSavingTime() { -+ return ( -+ this.utcOffset() > this.clone().month(0).utcOffset() || -+ this.utcOffset() > this.clone().month(5).utcOffset() -+ ); -+ } -+ -+ function isDaylightSavingTimeShifted() { -+ if (!isUndefined(this._isDSTShifted)) { -+ return this._isDSTShifted; -+ } -+ -+ var c = {}, -+ other; -+ -+ copyConfig(c, this); -+ c = prepareConfig(c); -+ -+ if (c._a) { -+ other = c._isUTC ? createUTC(c._a) : createLocal(c._a); -+ this._isDSTShifted = -+ this.isValid() && compareArrays(c._a, other.toArray()) > 0; -+ } else { -+ this._isDSTShifted = false; -+ } -+ -+ return this._isDSTShifted; -+ } -+ -+ function isLocal() { -+ return this.isValid() ? !this._isUTC : false; -+ } -+ -+ function isUtcOffset() { -+ return this.isValid() ? this._isUTC : false; -+ } -+ -+ function isUtc() { -+ return this.isValid() ? this._isUTC && this._offset === 0 : false; -+ } -+ -+ // ASP.NET json date format regex -+ var aspNetRegex = /^(-|\+)?(?:(\d*)[. ])?(\d+):(\d+)(?::(\d+)(\.\d*)?)?$/, -+ // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html -+ // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere -+ // and further modified to allow for strings containing both week and day -+ isoRegex = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/; -+ -+ function createDuration(input, key) { -+ var duration = input, -+ // matching against regexp is expensive, do it on demand -+ match = null, -+ sign, -+ ret, -+ diffRes; -+ -+ if (isDuration(input)) { -+ duration = { -+ ms: input._milliseconds, -+ d: input._days, -+ M: input._months, -+ }; -+ } else if (isNumber(input) || !isNaN(+input)) { -+ duration = {}; -+ if (key) { -+ duration[key] = +input; -+ } else { -+ duration.milliseconds = +input; -+ } -+ } else if ((match = aspNetRegex.exec(input))) { -+ sign = match[1] === '-' ? -1 : 1; -+ duration = { -+ y: 0, -+ d: toInt(match[DATE]) * sign, -+ h: toInt(match[HOUR]) * sign, -+ m: toInt(match[MINUTE]) * sign, -+ s: toInt(match[SECOND]) * sign, -+ ms: toInt(absRound(match[MILLISECOND] * 1000)) * sign, // the millisecond decimal point is included in the match -+ }; -+ } else if ((match = isoRegex.exec(input))) { -+ sign = match[1] === '-' ? -1 : 1; -+ duration = { -+ y: parseIso(match[2], sign), -+ M: parseIso(match[3], sign), -+ w: parseIso(match[4], sign), -+ d: parseIso(match[5], sign), -+ h: parseIso(match[6], sign), -+ m: parseIso(match[7], sign), -+ s: parseIso(match[8], sign), -+ }; -+ } else if (duration == null) { -+ // checks for null or undefined -+ duration = {}; -+ } else if ( -+ typeof duration === 'object' && -+ ('from' in duration || 'to' in duration) -+ ) { -+ diffRes = momentsDifference( -+ createLocal(duration.from), -+ createLocal(duration.to) -+ ); -+ -+ duration = {}; -+ duration.ms = diffRes.milliseconds; -+ duration.M = diffRes.months; -+ } -+ -+ ret = new Duration(duration); -+ -+ if (isDuration(input) && hasOwnProp(input, '_locale')) { -+ ret._locale = input._locale; -+ } -+ -+ if (isDuration(input) && hasOwnProp(input, '_isValid')) { -+ ret._isValid = input._isValid; -+ } -+ -+ return ret; -+ } -+ -+ createDuration.fn = Duration.prototype; -+ createDuration.invalid = createInvalid$1; -+ -+ function parseIso(inp, sign) { -+ // We'd normally use ~~inp for this, but unfortunately it also -+ // converts floats to ints. -+ // inp may be undefined, so careful calling replace on it. -+ var res = inp && parseFloat(inp.replace(',', '.')); -+ // apply sign while we're at it -+ return (isNaN(res) ? 0 : res) * sign; -+ } -+ -+ function positiveMomentsDifference(base, other) { -+ var res = {}; -+ -+ res.months = -+ other.month() - base.month() + (other.year() - base.year()) * 12; -+ if (base.clone().add(res.months, 'M').isAfter(other)) { -+ --res.months; -+ } -+ -+ res.milliseconds = +other - +base.clone().add(res.months, 'M'); -+ -+ return res; -+ } -+ -+ function momentsDifference(base, other) { -+ var res; -+ if (!(base.isValid() && other.isValid())) { -+ return { milliseconds: 0, months: 0 }; -+ } -+ -+ other = cloneWithOffset(other, base); -+ if (base.isBefore(other)) { -+ res = positiveMomentsDifference(base, other); -+ } else { -+ res = positiveMomentsDifference(other, base); -+ res.milliseconds = -res.milliseconds; -+ res.months = -res.months; -+ } -+ -+ return res; -+ } -+ -+ // TODO: remove 'name' arg after deprecation is removed -+ function createAdder(direction, name) { -+ return function (val, period) { -+ var dur, tmp; -+ //invert the arguments, but complain about it -+ if (period !== null && !isNaN(+period)) { -+ deprecateSimple( -+ name, -+ 'moment().' + -+ name + -+ '(period, number) is deprecated. Please use moment().' + -+ name + -+ '(number, period). ' + -+ 'See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info.' -+ ); -+ tmp = val; -+ val = period; -+ period = tmp; -+ } -+ -+ dur = createDuration(val, period); -+ addSubtract(this, dur, direction); -+ return this; -+ }; -+ } -+ -+ function addSubtract(mom, duration, isAdding, updateOffset) { -+ var milliseconds = duration._milliseconds, -+ days = absRound(duration._days), -+ months = absRound(duration._months); -+ -+ if (!mom.isValid()) { -+ // No op -+ return; -+ } -+ -+ updateOffset = updateOffset == null ? true : updateOffset; -+ -+ if (months) { -+ setMonth(mom, get(mom, 'Month') + months * isAdding); -+ } -+ if (days) { -+ set$1(mom, 'Date', get(mom, 'Date') + days * isAdding); -+ } -+ if (milliseconds) { -+ mom._d.setTime(mom._d.valueOf() + milliseconds * isAdding); -+ } -+ if (updateOffset) { -+ hooks.updateOffset(mom, days || months); -+ } -+ } -+ -+ var add = createAdder(1, 'add'), -+ subtract = createAdder(-1, 'subtract'); -+ -+ function isString(input) { -+ return typeof input === 'string' || input instanceof String; -+ } -+ -+ // type MomentInput = Moment | Date | string | number | (number | string)[] | MomentInputObject | void; // null | undefined -+ function isMomentInput(input) { -+ return ( -+ isMoment(input) || -+ isDate(input) || -+ isString(input) || -+ isNumber(input) || -+ isNumberOrStringArray(input) || -+ isMomentInputObject(input) || -+ input === null || -+ input === undefined -+ ); -+ } -+ -+ function isMomentInputObject(input) { -+ var objectTest = isObject(input) && !isObjectEmpty(input), -+ propertyTest = false, -+ properties = [ -+ 'years', -+ 'year', -+ 'y', -+ 'months', -+ 'month', -+ 'M', -+ 'days', -+ 'day', -+ 'd', -+ 'dates', -+ 'date', -+ 'D', -+ 'hours', -+ 'hour', -+ 'h', -+ 'minutes', -+ 'minute', -+ 'm', -+ 'seconds', -+ 'second', -+ 's', -+ 'milliseconds', -+ 'millisecond', -+ 'ms', -+ ], -+ i, -+ property; -+ -+ for (i = 0; i < properties.length; i += 1) { -+ property = properties[i]; -+ propertyTest = propertyTest || hasOwnProp(input, property); -+ } -+ -+ return objectTest && propertyTest; -+ } -+ -+ function isNumberOrStringArray(input) { -+ var arrayTest = isArray(input), -+ dataTypeTest = false; -+ if (arrayTest) { -+ dataTypeTest = -+ input.filter(function (item) { -+ return !isNumber(item) && isString(input); -+ }).length === 0; -+ } -+ return arrayTest && dataTypeTest; -+ } -+ -+ function isCalendarSpec(input) { -+ var objectTest = isObject(input) && !isObjectEmpty(input), -+ propertyTest = false, -+ properties = [ -+ 'sameDay', -+ 'nextDay', -+ 'lastDay', -+ 'nextWeek', -+ 'lastWeek', -+ 'sameElse', -+ ], -+ i, -+ property; -+ -+ for (i = 0; i < properties.length; i += 1) { -+ property = properties[i]; -+ propertyTest = propertyTest || hasOwnProp(input, property); -+ } -+ -+ return objectTest && propertyTest; -+ } -+ -+ function getCalendarFormat(myMoment, now) { -+ var diff = myMoment.diff(now, 'days', true); -+ return diff < -6 -+ ? 'sameElse' -+ : diff < -1 -+ ? 'lastWeek' -+ : diff < 0 -+ ? 'lastDay' -+ : diff < 1 -+ ? 'sameDay' -+ : diff < 2 -+ ? 'nextDay' -+ : diff < 7 -+ ? 'nextWeek' -+ : 'sameElse'; -+ } -+ -+ function calendar$1(time, formats) { -+ // Support for single parameter, formats only overload to the calendar function -+ if (arguments.length === 1) { -+ if (!arguments[0]) { -+ time = undefined; -+ formats = undefined; -+ } else if (isMomentInput(arguments[0])) { -+ time = arguments[0]; -+ formats = undefined; -+ } else if (isCalendarSpec(arguments[0])) { -+ formats = arguments[0]; -+ time = undefined; -+ } -+ } -+ // We want to compare the start of today, vs this. -+ // Getting start-of-today depends on whether we're local/utc/offset or not. -+ var now = time || createLocal(), -+ sod = cloneWithOffset(now, this).startOf('day'), -+ format = hooks.calendarFormat(this, sod) || 'sameElse', -+ output = -+ formats && -+ (isFunction(formats[format]) -+ ? formats[format].call(this, now) -+ : formats[format]); -+ -+ return this.format( -+ output || this.localeData().calendar(format, this, createLocal(now)) -+ ); -+ } -+ -+ function clone() { -+ return new Moment(this); -+ } -+ -+ function isAfter(input, units) { -+ var localInput = isMoment(input) ? input : createLocal(input); -+ if (!(this.isValid() && localInput.isValid())) { -+ return false; -+ } -+ units = normalizeUnits(units) || 'millisecond'; -+ if (units === 'millisecond') { -+ return this.valueOf() > localInput.valueOf(); -+ } else { -+ return localInput.valueOf() < this.clone().startOf(units).valueOf(); -+ } -+ } -+ -+ function isBefore(input, units) { -+ var localInput = isMoment(input) ? input : createLocal(input); -+ if (!(this.isValid() && localInput.isValid())) { -+ return false; -+ } -+ units = normalizeUnits(units) || 'millisecond'; -+ if (units === 'millisecond') { -+ return this.valueOf() < localInput.valueOf(); -+ } else { -+ return this.clone().endOf(units).valueOf() < localInput.valueOf(); -+ } -+ } -+ -+ function isBetween(from, to, units, inclusivity) { -+ var localFrom = isMoment(from) ? from : createLocal(from), -+ localTo = isMoment(to) ? to : createLocal(to); -+ if (!(this.isValid() && localFrom.isValid() && localTo.isValid())) { -+ return false; -+ } -+ inclusivity = inclusivity || '()'; -+ return ( -+ (inclusivity[0] === '(' -+ ? this.isAfter(localFrom, units) -+ : !this.isBefore(localFrom, units)) && -+ (inclusivity[1] === ')' -+ ? this.isBefore(localTo, units) -+ : !this.isAfter(localTo, units)) -+ ); -+ } -+ -+ function isSame(input, units) { -+ var localInput = isMoment(input) ? input : createLocal(input), -+ inputMs; -+ if (!(this.isValid() && localInput.isValid())) { -+ return false; -+ } -+ units = normalizeUnits(units) || 'millisecond'; -+ if (units === 'millisecond') { -+ return this.valueOf() === localInput.valueOf(); -+ } else { -+ inputMs = localInput.valueOf(); -+ return ( -+ this.clone().startOf(units).valueOf() <= inputMs && -+ inputMs <= this.clone().endOf(units).valueOf() -+ ); -+ } -+ } -+ -+ function isSameOrAfter(input, units) { -+ return this.isSame(input, units) || this.isAfter(input, units); -+ } -+ -+ function isSameOrBefore(input, units) { -+ return this.isSame(input, units) || this.isBefore(input, units); -+ } -+ -+ function diff(input, units, asFloat) { -+ var that, zoneDelta, output; -+ -+ if (!this.isValid()) { -+ return NaN; -+ } -+ -+ that = cloneWithOffset(input, this); -+ -+ if (!that.isValid()) { -+ return NaN; -+ } -+ -+ zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4; -+ -+ units = normalizeUnits(units); -+ -+ switch (units) { -+ case 'year': -+ output = monthDiff(this, that) / 12; -+ break; -+ case 'month': -+ output = monthDiff(this, that); -+ break; -+ case 'quarter': -+ output = monthDiff(this, that) / 3; -+ break; -+ case 'second': -+ output = (this - that) / 1e3; -+ break; // 1000 -+ case 'minute': -+ output = (this - that) / 6e4; -+ break; // 1000 * 60 -+ case 'hour': -+ output = (this - that) / 36e5; -+ break; // 1000 * 60 * 60 -+ case 'day': -+ output = (this - that - zoneDelta) / 864e5; -+ break; // 1000 * 60 * 60 * 24, negate dst -+ case 'week': -+ output = (this - that - zoneDelta) / 6048e5; -+ break; // 1000 * 60 * 60 * 24 * 7, negate dst -+ default: -+ output = this - that; -+ } -+ -+ return asFloat ? output : absFloor(output); -+ } -+ -+ function monthDiff(a, b) { -+ if (a.date() < b.date()) { -+ // end-of-month calculations work correct when the start month has more -+ // days than the end month. -+ return -monthDiff(b, a); -+ } -+ // difference in months -+ var wholeMonthDiff = (b.year() - a.year()) * 12 + (b.month() - a.month()), -+ // b is in (anchor - 1 month, anchor + 1 month) -+ anchor = a.clone().add(wholeMonthDiff, 'months'), -+ anchor2, -+ adjust; -+ -+ if (b - anchor < 0) { -+ anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); -+ // linear across the month -+ adjust = (b - anchor) / (anchor - anchor2); -+ } else { -+ anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); -+ // linear across the month -+ adjust = (b - anchor) / (anchor2 - anchor); -+ } -+ -+ //check for negative zero, return zero if negative zero -+ return -(wholeMonthDiff + adjust) || 0; -+ } -+ -+ hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; -+ hooks.defaultFormatUtc = 'YYYY-MM-DDTHH:mm:ss[Z]'; -+ -+ function toString() { -+ return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); -+ } -+ -+ function toISOString(keepOffset) { -+ if (!this.isValid()) { -+ return null; -+ } -+ var utc = keepOffset !== true, -+ m = utc ? this.clone().utc() : this; -+ if (m.year() < 0 || m.year() > 9999) { -+ return formatMoment( -+ m, -+ utc -+ ? 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]' -+ : 'YYYYYY-MM-DD[T]HH:mm:ss.SSSZ' -+ ); -+ } -+ if (isFunction(Date.prototype.toISOString)) { -+ // native implementation is ~50x faster, use it when we can -+ if (utc) { -+ return this.toDate().toISOString(); -+ } else { -+ return new Date(this.valueOf() + this.utcOffset() * 60 * 1000) -+ .toISOString() -+ .replace('Z', formatMoment(m, 'Z')); -+ } -+ } -+ return formatMoment( -+ m, -+ utc ? 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]' : 'YYYY-MM-DD[T]HH:mm:ss.SSSZ' -+ ); -+ } -+ -+ /** -+ * Return a human readable representation of a moment that can -+ * also be evaluated to get a new moment which is the same -+ * -+ * @link https://nodejs.org/dist/latest/docs/api/util.html#util_custom_inspect_function_on_objects -+ */ -+ function inspect() { -+ if (!this.isValid()) { -+ return 'moment.invalid(/* ' + this._i + ' */)'; -+ } -+ var func = 'moment', -+ zone = '', -+ prefix, -+ year, -+ datetime, -+ suffix; -+ if (!this.isLocal()) { -+ func = this.utcOffset() === 0 ? 'moment.utc' : 'moment.parseZone'; -+ zone = 'Z'; -+ } -+ prefix = '[' + func + '("]'; -+ year = 0 <= this.year() && this.year() <= 9999 ? 'YYYY' : 'YYYYYY'; -+ datetime = '-MM-DD[T]HH:mm:ss.SSS'; -+ suffix = zone + '[")]'; -+ -+ return this.format(prefix + year + datetime + suffix); -+ } -+ -+ function format(inputString) { -+ if (!inputString) { -+ inputString = this.isUtc() -+ ? hooks.defaultFormatUtc -+ : hooks.defaultFormat; -+ } -+ var output = formatMoment(this, inputString); -+ return this.localeData().postformat(output); -+ } -+ -+ function from(time, withoutSuffix) { -+ if ( -+ this.isValid() && -+ ((isMoment(time) && time.isValid()) || createLocal(time).isValid()) -+ ) { -+ return createDuration({ to: this, from: time }) -+ .locale(this.locale()) -+ .humanize(!withoutSuffix); -+ } else { -+ return this.localeData().invalidDate(); -+ } -+ } -+ -+ function fromNow(withoutSuffix) { -+ return this.from(createLocal(), withoutSuffix); -+ } -+ -+ function to(time, withoutSuffix) { -+ if ( -+ this.isValid() && -+ ((isMoment(time) && time.isValid()) || createLocal(time).isValid()) -+ ) { -+ return createDuration({ from: this, to: time }) -+ .locale(this.locale()) -+ .humanize(!withoutSuffix); -+ } else { -+ return this.localeData().invalidDate(); -+ } -+ } -+ -+ function toNow(withoutSuffix) { -+ return this.to(createLocal(), withoutSuffix); -+ } -+ -+ // If passed a locale key, it will set the locale for this -+ // instance. Otherwise, it will return the locale configuration -+ // variables for this instance. -+ function locale(key) { -+ var newLocaleData; -+ -+ if (key === undefined) { -+ return this._locale._abbr; -+ } else { -+ newLocaleData = getLocale(key); -+ if (newLocaleData != null) { -+ this._locale = newLocaleData; -+ } -+ return this; -+ } -+ } -+ -+ var lang = deprecate( -+ 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', -+ function (key) { -+ if (key === undefined) { -+ return this.localeData(); -+ } else { -+ return this.locale(key); -+ } -+ } -+ ); -+ -+ function localeData() { -+ return this._locale; -+ } -+ -+ var MS_PER_SECOND = 1000, -+ MS_PER_MINUTE = 60 * MS_PER_SECOND, -+ MS_PER_HOUR = 60 * MS_PER_MINUTE, -+ MS_PER_400_YEARS = (365 * 400 + 97) * 24 * MS_PER_HOUR; -+ -+ // actual modulo - handles negative numbers (for dates before 1970): -+ function mod$1(dividend, divisor) { -+ return ((dividend % divisor) + divisor) % divisor; -+ } -+ -+ function localStartOfDate(y, m, d) { -+ // the date constructor remaps years 0-99 to 1900-1999 -+ if (y < 100 && y >= 0) { -+ // preserve leap years using a full 400 year cycle, then reset -+ return new Date(y + 400, m, d) - MS_PER_400_YEARS; -+ } else { -+ return new Date(y, m, d).valueOf(); -+ } -+ } -+ -+ function utcStartOfDate(y, m, d) { -+ // Date.UTC remaps years 0-99 to 1900-1999 -+ if (y < 100 && y >= 0) { -+ // preserve leap years using a full 400 year cycle, then reset -+ return Date.UTC(y + 400, m, d) - MS_PER_400_YEARS; -+ } else { -+ return Date.UTC(y, m, d); -+ } -+ } -+ -+ function startOf(units) { -+ var time, startOfDate; -+ units = normalizeUnits(units); -+ if (units === undefined || units === 'millisecond' || !this.isValid()) { -+ return this; -+ } -+ -+ startOfDate = this._isUTC ? utcStartOfDate : localStartOfDate; -+ -+ switch (units) { -+ case 'year': -+ time = startOfDate(this.year(), 0, 1); -+ break; -+ case 'quarter': -+ time = startOfDate( -+ this.year(), -+ this.month() - (this.month() % 3), -+ 1 -+ ); -+ break; -+ case 'month': -+ time = startOfDate(this.year(), this.month(), 1); -+ break; -+ case 'week': -+ time = startOfDate( -+ this.year(), -+ this.month(), -+ this.date() - this.weekday() -+ ); -+ break; -+ case 'isoWeek': -+ time = startOfDate( -+ this.year(), -+ this.month(), -+ this.date() - (this.isoWeekday() - 1) -+ ); -+ break; -+ case 'day': -+ case 'date': -+ time = startOfDate(this.year(), this.month(), this.date()); -+ break; -+ case 'hour': -+ time = this._d.valueOf(); -+ time -= mod$1( -+ time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), -+ MS_PER_HOUR -+ ); -+ break; -+ case 'minute': -+ time = this._d.valueOf(); -+ time -= mod$1(time, MS_PER_MINUTE); -+ break; -+ case 'second': -+ time = this._d.valueOf(); -+ time -= mod$1(time, MS_PER_SECOND); -+ break; -+ } -+ -+ this._d.setTime(time); -+ hooks.updateOffset(this, true); -+ return this; -+ } -+ -+ function endOf(units) { -+ var time, startOfDate; -+ units = normalizeUnits(units); -+ if (units === undefined || units === 'millisecond' || !this.isValid()) { -+ return this; -+ } -+ -+ startOfDate = this._isUTC ? utcStartOfDate : localStartOfDate; -+ -+ switch (units) { -+ case 'year': -+ time = startOfDate(this.year() + 1, 0, 1) - 1; -+ break; -+ case 'quarter': -+ time = -+ startOfDate( -+ this.year(), -+ this.month() - (this.month() % 3) + 3, -+ 1 -+ ) - 1; -+ break; -+ case 'month': -+ time = startOfDate(this.year(), this.month() + 1, 1) - 1; -+ break; -+ case 'week': -+ time = -+ startOfDate( -+ this.year(), -+ this.month(), -+ this.date() - this.weekday() + 7 -+ ) - 1; -+ break; -+ case 'isoWeek': -+ time = -+ startOfDate( -+ this.year(), -+ this.month(), -+ this.date() - (this.isoWeekday() - 1) + 7 -+ ) - 1; -+ break; -+ case 'day': -+ case 'date': -+ time = startOfDate(this.year(), this.month(), this.date() + 1) - 1; -+ break; -+ case 'hour': -+ time = this._d.valueOf(); -+ time += -+ MS_PER_HOUR - -+ mod$1( -+ time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), -+ MS_PER_HOUR -+ ) - -+ 1; -+ break; -+ case 'minute': -+ time = this._d.valueOf(); -+ time += MS_PER_MINUTE - mod$1(time, MS_PER_MINUTE) - 1; -+ break; -+ case 'second': -+ time = this._d.valueOf(); -+ time += MS_PER_SECOND - mod$1(time, MS_PER_SECOND) - 1; -+ break; -+ } -+ -+ this._d.setTime(time); -+ hooks.updateOffset(this, true); -+ return this; -+ } -+ -+ function valueOf() { -+ return this._d.valueOf() - (this._offset || 0) * 60000; -+ } -+ -+ function unix() { -+ return Math.floor(this.valueOf() / 1000); -+ } -+ -+ function toDate() { -+ return new Date(this.valueOf()); -+ } -+ -+ function toArray() { -+ var m = this; -+ return [ -+ m.year(), -+ m.month(), -+ m.date(), -+ m.hour(), -+ m.minute(), -+ m.second(), -+ m.millisecond(), -+ ]; -+ } -+ -+ function toObject() { -+ var m = this; -+ return { -+ years: m.year(), -+ months: m.month(), -+ date: m.date(), -+ hours: m.hours(), -+ minutes: m.minutes(), -+ seconds: m.seconds(), -+ milliseconds: m.milliseconds(), -+ }; -+ } -+ -+ function toJSON() { -+ // new Date(NaN).toJSON() === null -+ return this.isValid() ? this.toISOString() : null; -+ } -+ -+ function isValid$2() { -+ return isValid(this); -+ } -+ -+ function parsingFlags() { -+ return extend({}, getParsingFlags(this)); -+ } -+ -+ function invalidAt() { -+ return getParsingFlags(this).overflow; -+ } -+ -+ function creationData() { -+ return { -+ input: this._i, -+ format: this._f, -+ locale: this._locale, -+ isUTC: this._isUTC, -+ strict: this._strict, -+ }; -+ } -+ -+ addFormatToken('N', 0, 0, 'eraAbbr'); -+ addFormatToken('NN', 0, 0, 'eraAbbr'); -+ addFormatToken('NNN', 0, 0, 'eraAbbr'); -+ addFormatToken('NNNN', 0, 0, 'eraName'); -+ addFormatToken('NNNNN', 0, 0, 'eraNarrow'); -+ -+ addFormatToken('y', ['y', 1], 'yo', 'eraYear'); -+ addFormatToken('y', ['yy', 2], 0, 'eraYear'); -+ addFormatToken('y', ['yyy', 3], 0, 'eraYear'); -+ addFormatToken('y', ['yyyy', 4], 0, 'eraYear'); -+ -+ addRegexToken('N', matchEraAbbr); -+ addRegexToken('NN', matchEraAbbr); -+ addRegexToken('NNN', matchEraAbbr); -+ addRegexToken('NNNN', matchEraName); -+ addRegexToken('NNNNN', matchEraNarrow); -+ -+ addParseToken(['N', 'NN', 'NNN', 'NNNN', 'NNNNN'], function ( -+ input, -+ array, -+ config, -+ token -+ ) { -+ var era = config._locale.erasParse(input, token, config._strict); -+ if (era) { -+ getParsingFlags(config).era = era; -+ } else { -+ getParsingFlags(config).invalidEra = input; -+ } -+ }); -+ -+ addRegexToken('y', matchUnsigned); -+ addRegexToken('yy', matchUnsigned); -+ addRegexToken('yyy', matchUnsigned); -+ addRegexToken('yyyy', matchUnsigned); -+ addRegexToken('yo', matchEraYearOrdinal); -+ -+ addParseToken(['y', 'yy', 'yyy', 'yyyy'], YEAR); -+ addParseToken(['yo'], function (input, array, config, token) { -+ var match; -+ if (config._locale._eraYearOrdinalRegex) { -+ match = input.match(config._locale._eraYearOrdinalRegex); -+ } -+ -+ if (config._locale.eraYearOrdinalParse) { -+ array[YEAR] = config._locale.eraYearOrdinalParse(input, match); -+ } else { -+ array[YEAR] = parseInt(input, 10); -+ } -+ }); -+ -+ function localeEras(m, format) { -+ var i, -+ l, -+ date, -+ eras = this._eras || getLocale('en')._eras; -+ for (i = 0, l = eras.length; i < l; ++i) { -+ switch (typeof eras[i].since) { -+ case 'string': -+ // truncate time -+ date = hooks(eras[i].since).startOf('day'); -+ eras[i].since = date.valueOf(); -+ break; -+ } -+ -+ switch (typeof eras[i].until) { -+ case 'undefined': -+ eras[i].until = +Infinity; -+ break; -+ case 'string': -+ // truncate time -+ date = hooks(eras[i].until).startOf('day').valueOf(); -+ eras[i].until = date.valueOf(); -+ break; -+ } -+ } -+ return eras; -+ } -+ -+ function localeErasParse(eraName, format, strict) { -+ var i, -+ l, -+ eras = this.eras(), -+ name, -+ abbr, -+ narrow; -+ eraName = eraName.toUpperCase(); -+ -+ for (i = 0, l = eras.length; i < l; ++i) { -+ name = eras[i].name.toUpperCase(); -+ abbr = eras[i].abbr.toUpperCase(); -+ narrow = eras[i].narrow.toUpperCase(); -+ -+ if (strict) { -+ switch (format) { -+ case 'N': -+ case 'NN': -+ case 'NNN': -+ if (abbr === eraName) { -+ return eras[i]; -+ } -+ break; -+ -+ case 'NNNN': -+ if (name === eraName) { -+ return eras[i]; -+ } -+ break; -+ -+ case 'NNNNN': -+ if (narrow === eraName) { -+ return eras[i]; -+ } -+ break; -+ } -+ } else if ([name, abbr, narrow].indexOf(eraName) >= 0) { -+ return eras[i]; -+ } -+ } -+ } -+ -+ function localeErasConvertYear(era, year) { -+ var dir = era.since <= era.until ? +1 : -1; -+ if (year === undefined) { -+ return hooks(era.since).year(); -+ } else { -+ return hooks(era.since).year() + (year - era.offset) * dir; -+ } -+ } -+ -+ function getEraName() { -+ var i, -+ l, -+ val, -+ eras = this.localeData().eras(); -+ for (i = 0, l = eras.length; i < l; ++i) { -+ // truncate time -+ val = this.clone().startOf('day').valueOf(); -+ -+ if (eras[i].since <= val && val <= eras[i].until) { -+ return eras[i].name; -+ } -+ if (eras[i].until <= val && val <= eras[i].since) { -+ return eras[i].name; -+ } -+ } -+ -+ return ''; -+ } -+ -+ function getEraNarrow() { -+ var i, -+ l, -+ val, -+ eras = this.localeData().eras(); -+ for (i = 0, l = eras.length; i < l; ++i) { -+ // truncate time -+ val = this.clone().startOf('day').valueOf(); -+ -+ if (eras[i].since <= val && val <= eras[i].until) { -+ return eras[i].narrow; -+ } -+ if (eras[i].until <= val && val <= eras[i].since) { -+ return eras[i].narrow; -+ } -+ } -+ -+ return ''; -+ } -+ -+ function getEraAbbr() { -+ var i, -+ l, -+ val, -+ eras = this.localeData().eras(); -+ for (i = 0, l = eras.length; i < l; ++i) { -+ // truncate time -+ val = this.clone().startOf('day').valueOf(); -+ -+ if (eras[i].since <= val && val <= eras[i].until) { -+ return eras[i].abbr; -+ } -+ if (eras[i].until <= val && val <= eras[i].since) { -+ return eras[i].abbr; -+ } -+ } -+ -+ return ''; -+ } -+ -+ function getEraYear() { -+ var i, -+ l, -+ dir, -+ val, -+ eras = this.localeData().eras(); -+ for (i = 0, l = eras.length; i < l; ++i) { -+ dir = eras[i].since <= eras[i].until ? +1 : -1; -+ -+ // truncate time -+ val = this.clone().startOf('day').valueOf(); -+ -+ if ( -+ (eras[i].since <= val && val <= eras[i].until) || -+ (eras[i].until <= val && val <= eras[i].since) -+ ) { -+ return ( -+ (this.year() - hooks(eras[i].since).year()) * dir + -+ eras[i].offset -+ ); -+ } -+ } -+ -+ return this.year(); -+ } -+ -+ function erasNameRegex(isStrict) { -+ if (!hasOwnProp(this, '_erasNameRegex')) { -+ computeErasParse.call(this); -+ } -+ return isStrict ? this._erasNameRegex : this._erasRegex; -+ } -+ -+ function erasAbbrRegex(isStrict) { -+ if (!hasOwnProp(this, '_erasAbbrRegex')) { -+ computeErasParse.call(this); -+ } -+ return isStrict ? this._erasAbbrRegex : this._erasRegex; -+ } -+ -+ function erasNarrowRegex(isStrict) { -+ if (!hasOwnProp(this, '_erasNarrowRegex')) { -+ computeErasParse.call(this); -+ } -+ return isStrict ? this._erasNarrowRegex : this._erasRegex; -+ } -+ -+ function matchEraAbbr(isStrict, locale) { -+ return locale.erasAbbrRegex(isStrict); -+ } -+ -+ function matchEraName(isStrict, locale) { -+ return locale.erasNameRegex(isStrict); -+ } -+ -+ function matchEraNarrow(isStrict, locale) { -+ return locale.erasNarrowRegex(isStrict); -+ } -+ -+ function matchEraYearOrdinal(isStrict, locale) { -+ return locale._eraYearOrdinalRegex || matchUnsigned; -+ } -+ -+ function computeErasParse() { -+ var abbrPieces = [], -+ namePieces = [], -+ narrowPieces = [], -+ mixedPieces = [], -+ i, -+ l, -+ eras = this.eras(); -+ -+ for (i = 0, l = eras.length; i < l; ++i) { -+ namePieces.push(regexEscape(eras[i].name)); -+ abbrPieces.push(regexEscape(eras[i].abbr)); -+ narrowPieces.push(regexEscape(eras[i].narrow)); -+ -+ mixedPieces.push(regexEscape(eras[i].name)); -+ mixedPieces.push(regexEscape(eras[i].abbr)); -+ mixedPieces.push(regexEscape(eras[i].narrow)); -+ } -+ -+ this._erasRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); -+ this._erasNameRegex = new RegExp('^(' + namePieces.join('|') + ')', 'i'); -+ this._erasAbbrRegex = new RegExp('^(' + abbrPieces.join('|') + ')', 'i'); -+ this._erasNarrowRegex = new RegExp( -+ '^(' + narrowPieces.join('|') + ')', -+ 'i' -+ ); -+ } -+ -+ // FORMATTING -+ -+ addFormatToken(0, ['gg', 2], 0, function () { -+ return this.weekYear() % 100; -+ }); -+ -+ addFormatToken(0, ['GG', 2], 0, function () { -+ return this.isoWeekYear() % 100; -+ }); -+ -+ function addWeekYearFormatToken(token, getter) { -+ addFormatToken(0, [token, token.length], 0, getter); -+ } -+ -+ addWeekYearFormatToken('gggg', 'weekYear'); -+ addWeekYearFormatToken('ggggg', 'weekYear'); -+ addWeekYearFormatToken('GGGG', 'isoWeekYear'); -+ addWeekYearFormatToken('GGGGG', 'isoWeekYear'); -+ -+ // ALIASES -+ -+ addUnitAlias('weekYear', 'gg'); -+ addUnitAlias('isoWeekYear', 'GG'); -+ -+ // PRIORITY -+ -+ addUnitPriority('weekYear', 1); -+ addUnitPriority('isoWeekYear', 1); -+ -+ // PARSING -+ -+ addRegexToken('G', matchSigned); -+ addRegexToken('g', matchSigned); -+ addRegexToken('GG', match1to2, match2); -+ addRegexToken('gg', match1to2, match2); -+ addRegexToken('GGGG', match1to4, match4); -+ addRegexToken('gggg', match1to4, match4); -+ addRegexToken('GGGGG', match1to6, match6); -+ addRegexToken('ggggg', match1to6, match6); -+ -+ addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function ( -+ input, -+ week, -+ config, -+ token -+ ) { -+ week[token.substr(0, 2)] = toInt(input); -+ }); -+ -+ addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { -+ week[token] = hooks.parseTwoDigitYear(input); -+ }); -+ -+ // MOMENTS -+ -+ function getSetWeekYear(input) { -+ return getSetWeekYearHelper.call( -+ this, -+ input, -+ this.week(), -+ this.weekday(), -+ this.localeData()._week.dow, -+ this.localeData()._week.doy -+ ); -+ } -+ -+ function getSetISOWeekYear(input) { -+ return getSetWeekYearHelper.call( -+ this, -+ input, -+ this.isoWeek(), -+ this.isoWeekday(), -+ 1, -+ 4 -+ ); -+ } -+ -+ function getISOWeeksInYear() { -+ return weeksInYear(this.year(), 1, 4); -+ } -+ -+ function getISOWeeksInISOWeekYear() { -+ return weeksInYear(this.isoWeekYear(), 1, 4); -+ } -+ -+ function getWeeksInYear() { -+ var weekInfo = this.localeData()._week; -+ return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); -+ } -+ -+ function getWeeksInWeekYear() { -+ var weekInfo = this.localeData()._week; -+ return weeksInYear(this.weekYear(), weekInfo.dow, weekInfo.doy); -+ } -+ -+ function getSetWeekYearHelper(input, week, weekday, dow, doy) { -+ var weeksTarget; -+ if (input == null) { -+ return weekOfYear(this, dow, doy).year; -+ } else { -+ weeksTarget = weeksInYear(input, dow, doy); -+ if (week > weeksTarget) { -+ week = weeksTarget; -+ } -+ return setWeekAll.call(this, input, week, weekday, dow, doy); -+ } -+ } -+ -+ function setWeekAll(weekYear, week, weekday, dow, doy) { -+ var dayOfYearData = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy), -+ date = createUTCDate(dayOfYearData.year, 0, dayOfYearData.dayOfYear); -+ -+ this.year(date.getUTCFullYear()); -+ this.month(date.getUTCMonth()); -+ this.date(date.getUTCDate()); -+ return this; -+ } -+ -+ // FORMATTING -+ -+ addFormatToken('Q', 0, 'Qo', 'quarter'); -+ -+ // ALIASES -+ -+ addUnitAlias('quarter', 'Q'); -+ -+ // PRIORITY -+ -+ addUnitPriority('quarter', 7); -+ -+ // PARSING -+ -+ addRegexToken('Q', match1); -+ addParseToken('Q', function (input, array) { -+ array[MONTH] = (toInt(input) - 1) * 3; -+ }); -+ -+ // MOMENTS -+ -+ function getSetQuarter(input) { -+ return input == null -+ ? Math.ceil((this.month() + 1) / 3) -+ : this.month((input - 1) * 3 + (this.month() % 3)); -+ } -+ -+ // FORMATTING -+ -+ addFormatToken('D', ['DD', 2], 'Do', 'date'); -+ -+ // ALIASES -+ -+ addUnitAlias('date', 'D'); -+ -+ // PRIORITY -+ addUnitPriority('date', 9); -+ -+ // PARSING -+ -+ addRegexToken('D', match1to2); -+ addRegexToken('DD', match1to2, match2); -+ addRegexToken('Do', function (isStrict, locale) { -+ // TODO: Remove "ordinalParse" fallback in next major release. -+ return isStrict -+ ? locale._dayOfMonthOrdinalParse || locale._ordinalParse -+ : locale._dayOfMonthOrdinalParseLenient; -+ }); -+ -+ addParseToken(['D', 'DD'], DATE); -+ addParseToken('Do', function (input, array) { -+ array[DATE] = toInt(input.match(match1to2)[0]); -+ }); -+ -+ // MOMENTS -+ -+ var getSetDayOfMonth = makeGetSet('Date', true); -+ -+ // FORMATTING -+ -+ addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); -+ -+ // ALIASES -+ -+ addUnitAlias('dayOfYear', 'DDD'); -+ -+ // PRIORITY -+ addUnitPriority('dayOfYear', 4); -+ -+ // PARSING -+ -+ addRegexToken('DDD', match1to3); -+ addRegexToken('DDDD', match3); -+ addParseToken(['DDD', 'DDDD'], function (input, array, config) { -+ config._dayOfYear = toInt(input); -+ }); -+ -+ // HELPERS -+ -+ // MOMENTS -+ -+ function getSetDayOfYear(input) { -+ var dayOfYear = -+ Math.round( -+ (this.clone().startOf('day') - this.clone().startOf('year')) / 864e5 -+ ) + 1; -+ return input == null ? dayOfYear : this.add(input - dayOfYear, 'd'); -+ } -+ -+ // FORMATTING -+ -+ addFormatToken('m', ['mm', 2], 0, 'minute'); -+ -+ // ALIASES -+ -+ addUnitAlias('minute', 'm'); -+ -+ // PRIORITY -+ -+ addUnitPriority('minute', 14); -+ -+ // PARSING -+ -+ addRegexToken('m', match1to2); -+ addRegexToken('mm', match1to2, match2); -+ addParseToken(['m', 'mm'], MINUTE); -+ -+ // MOMENTS -+ -+ var getSetMinute = makeGetSet('Minutes', false); -+ -+ // FORMATTING -+ -+ addFormatToken('s', ['ss', 2], 0, 'second'); -+ -+ // ALIASES -+ -+ addUnitAlias('second', 's'); -+ -+ // PRIORITY -+ -+ addUnitPriority('second', 15); -+ -+ // PARSING -+ -+ addRegexToken('s', match1to2); -+ addRegexToken('ss', match1to2, match2); -+ addParseToken(['s', 'ss'], SECOND); -+ -+ // MOMENTS -+ -+ var getSetSecond = makeGetSet('Seconds', false); -+ -+ // FORMATTING -+ -+ addFormatToken('S', 0, 0, function () { -+ return ~~(this.millisecond() / 100); -+ }); -+ -+ addFormatToken(0, ['SS', 2], 0, function () { -+ return ~~(this.millisecond() / 10); -+ }); -+ -+ addFormatToken(0, ['SSS', 3], 0, 'millisecond'); -+ addFormatToken(0, ['SSSS', 4], 0, function () { -+ return this.millisecond() * 10; -+ }); -+ addFormatToken(0, ['SSSSS', 5], 0, function () { -+ return this.millisecond() * 100; -+ }); -+ addFormatToken(0, ['SSSSSS', 6], 0, function () { -+ return this.millisecond() * 1000; -+ }); -+ addFormatToken(0, ['SSSSSSS', 7], 0, function () { -+ return this.millisecond() * 10000; -+ }); -+ addFormatToken(0, ['SSSSSSSS', 8], 0, function () { -+ return this.millisecond() * 100000; -+ }); -+ addFormatToken(0, ['SSSSSSSSS', 9], 0, function () { -+ return this.millisecond() * 1000000; -+ }); -+ -+ // ALIASES -+ -+ addUnitAlias('millisecond', 'ms'); -+ -+ // PRIORITY -+ -+ addUnitPriority('millisecond', 16); -+ -+ // PARSING -+ -+ addRegexToken('S', match1to3, match1); -+ addRegexToken('SS', match1to3, match2); -+ addRegexToken('SSS', match1to3, match3); -+ -+ var token, getSetMillisecond; -+ for (token = 'SSSS'; token.length <= 9; token += 'S') { -+ addRegexToken(token, matchUnsigned); -+ } -+ -+ function parseMs(input, array) { -+ array[MILLISECOND] = toInt(('0.' + input) * 1000); -+ } -+ -+ for (token = 'S'; token.length <= 9; token += 'S') { -+ addParseToken(token, parseMs); -+ } -+ -+ getSetMillisecond = makeGetSet('Milliseconds', false); -+ -+ // FORMATTING -+ -+ addFormatToken('z', 0, 0, 'zoneAbbr'); -+ addFormatToken('zz', 0, 0, 'zoneName'); -+ -+ // MOMENTS -+ -+ function getZoneAbbr() { -+ return this._isUTC ? 'UTC' : ''; -+ } -+ -+ function getZoneName() { -+ return this._isUTC ? 'Coordinated Universal Time' : ''; -+ } -+ -+ var proto = Moment.prototype; -+ -+ proto.add = add; -+ proto.calendar = calendar$1; -+ proto.clone = clone; -+ proto.diff = diff; -+ proto.endOf = endOf; -+ proto.format = format; -+ proto.from = from; -+ proto.fromNow = fromNow; -+ proto.to = to; -+ proto.toNow = toNow; -+ proto.get = stringGet; -+ proto.invalidAt = invalidAt; -+ proto.isAfter = isAfter; -+ proto.isBefore = isBefore; -+ proto.isBetween = isBetween; -+ proto.isSame = isSame; -+ proto.isSameOrAfter = isSameOrAfter; -+ proto.isSameOrBefore = isSameOrBefore; -+ proto.isValid = isValid$2; -+ proto.lang = lang; -+ proto.locale = locale; -+ proto.localeData = localeData; -+ proto.max = prototypeMax; -+ proto.min = prototypeMin; -+ proto.parsingFlags = parsingFlags; -+ proto.set = stringSet; -+ proto.startOf = startOf; -+ proto.subtract = subtract; -+ proto.toArray = toArray; -+ proto.toObject = toObject; -+ proto.toDate = toDate; -+ proto.toISOString = toISOString; -+ proto.inspect = inspect; -+ if (typeof Symbol !== 'undefined' && Symbol.for != null) { -+ proto[Symbol.for('nodejs.util.inspect.custom')] = function () { -+ return 'Moment<' + this.format() + '>'; -+ }; -+ } -+ proto.toJSON = toJSON; -+ proto.toString = toString; -+ proto.unix = unix; -+ proto.valueOf = valueOf; -+ proto.creationData = creationData; -+ proto.eraName = getEraName; -+ proto.eraNarrow = getEraNarrow; -+ proto.eraAbbr = getEraAbbr; -+ proto.eraYear = getEraYear; -+ proto.year = getSetYear; -+ proto.isLeapYear = getIsLeapYear; -+ proto.weekYear = getSetWeekYear; -+ proto.isoWeekYear = getSetISOWeekYear; -+ proto.quarter = proto.quarters = getSetQuarter; -+ proto.month = getSetMonth; -+ proto.daysInMonth = getDaysInMonth; -+ proto.week = proto.weeks = getSetWeek; -+ proto.isoWeek = proto.isoWeeks = getSetISOWeek; -+ proto.weeksInYear = getWeeksInYear; -+ proto.weeksInWeekYear = getWeeksInWeekYear; -+ proto.isoWeeksInYear = getISOWeeksInYear; -+ proto.isoWeeksInISOWeekYear = getISOWeeksInISOWeekYear; -+ proto.date = getSetDayOfMonth; -+ proto.day = proto.days = getSetDayOfWeek; -+ proto.weekday = getSetLocaleDayOfWeek; -+ proto.isoWeekday = getSetISODayOfWeek; -+ proto.dayOfYear = getSetDayOfYear; -+ proto.hour = proto.hours = getSetHour; -+ proto.minute = proto.minutes = getSetMinute; -+ proto.second = proto.seconds = getSetSecond; -+ proto.millisecond = proto.milliseconds = getSetMillisecond; -+ proto.utcOffset = getSetOffset; -+ proto.utc = setOffsetToUTC; -+ proto.local = setOffsetToLocal; -+ proto.parseZone = setOffsetToParsedOffset; -+ proto.hasAlignedHourOffset = hasAlignedHourOffset; -+ proto.isDST = isDaylightSavingTime; -+ proto.isLocal = isLocal; -+ proto.isUtcOffset = isUtcOffset; -+ proto.isUtc = isUtc; -+ proto.isUTC = isUtc; -+ proto.zoneAbbr = getZoneAbbr; -+ proto.zoneName = getZoneName; -+ proto.dates = deprecate( -+ 'dates accessor is deprecated. Use date instead.', -+ getSetDayOfMonth -+ ); -+ proto.months = deprecate( -+ 'months accessor is deprecated. Use month instead', -+ getSetMonth -+ ); -+ proto.years = deprecate( -+ 'years accessor is deprecated. Use year instead', -+ getSetYear -+ ); -+ proto.zone = deprecate( -+ 'moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/', -+ getSetZone -+ ); -+ proto.isDSTShifted = deprecate( -+ 'isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information', -+ isDaylightSavingTimeShifted -+ ); -+ -+ function createUnix(input) { -+ return createLocal(input * 1000); -+ } -+ -+ function createInZone() { -+ return createLocal.apply(null, arguments).parseZone(); -+ } -+ -+ function preParsePostFormat(string) { -+ return string; -+ } -+ -+ var proto$1 = Locale.prototype; -+ -+ proto$1.calendar = calendar; -+ proto$1.longDateFormat = longDateFormat; -+ proto$1.invalidDate = invalidDate; -+ proto$1.ordinal = ordinal; -+ proto$1.preparse = preParsePostFormat; -+ proto$1.postformat = preParsePostFormat; -+ proto$1.relativeTime = relativeTime; -+ proto$1.pastFuture = pastFuture; -+ proto$1.set = set; -+ proto$1.eras = localeEras; -+ proto$1.erasParse = localeErasParse; -+ proto$1.erasConvertYear = localeErasConvertYear; -+ proto$1.erasAbbrRegex = erasAbbrRegex; -+ proto$1.erasNameRegex = erasNameRegex; -+ proto$1.erasNarrowRegex = erasNarrowRegex; -+ -+ proto$1.months = localeMonths; -+ proto$1.monthsShort = localeMonthsShort; -+ proto$1.monthsParse = localeMonthsParse; -+ proto$1.monthsRegex = monthsRegex; -+ proto$1.monthsShortRegex = monthsShortRegex; -+ proto$1.week = localeWeek; -+ proto$1.firstDayOfYear = localeFirstDayOfYear; -+ proto$1.firstDayOfWeek = localeFirstDayOfWeek; -+ -+ proto$1.weekdays = localeWeekdays; -+ proto$1.weekdaysMin = localeWeekdaysMin; -+ proto$1.weekdaysShort = localeWeekdaysShort; -+ proto$1.weekdaysParse = localeWeekdaysParse; -+ -+ proto$1.weekdaysRegex = weekdaysRegex; -+ proto$1.weekdaysShortRegex = weekdaysShortRegex; -+ proto$1.weekdaysMinRegex = weekdaysMinRegex; -+ -+ proto$1.isPM = localeIsPM; -+ proto$1.meridiem = localeMeridiem; -+ -+ function get$1(format, index, field, setter) { -+ var locale = getLocale(), -+ utc = createUTC().set(setter, index); -+ return locale[field](utc, format); -+ } -+ -+ function listMonthsImpl(format, index, field) { -+ if (isNumber(format)) { -+ index = format; -+ format = undefined; -+ } -+ -+ format = format || ''; -+ -+ if (index != null) { -+ return get$1(format, index, field, 'month'); -+ } -+ -+ var i, -+ out = []; -+ for (i = 0; i < 12; i++) { -+ out[i] = get$1(format, i, field, 'month'); -+ } -+ return out; -+ } -+ -+ // () -+ // (5) -+ // (fmt, 5) -+ // (fmt) -+ // (true) -+ // (true, 5) -+ // (true, fmt, 5) -+ // (true, fmt) -+ function listWeekdaysImpl(localeSorted, format, index, field) { -+ if (typeof localeSorted === 'boolean') { -+ if (isNumber(format)) { -+ index = format; -+ format = undefined; -+ } -+ -+ format = format || ''; -+ } else { -+ format = localeSorted; -+ index = format; -+ localeSorted = false; -+ -+ if (isNumber(format)) { -+ index = format; -+ format = undefined; -+ } -+ -+ format = format || ''; -+ } -+ -+ var locale = getLocale(), -+ shift = localeSorted ? locale._week.dow : 0, -+ i, -+ out = []; -+ -+ if (index != null) { -+ return get$1(format, (index + shift) % 7, field, 'day'); -+ } -+ -+ for (i = 0; i < 7; i++) { -+ out[i] = get$1(format, (i + shift) % 7, field, 'day'); -+ } -+ return out; -+ } -+ -+ function listMonths(format, index) { -+ return listMonthsImpl(format, index, 'months'); -+ } -+ -+ function listMonthsShort(format, index) { -+ return listMonthsImpl(format, index, 'monthsShort'); -+ } -+ -+ function listWeekdays(localeSorted, format, index) { -+ return listWeekdaysImpl(localeSorted, format, index, 'weekdays'); -+ } -+ -+ function listWeekdaysShort(localeSorted, format, index) { -+ return listWeekdaysImpl(localeSorted, format, index, 'weekdaysShort'); -+ } -+ -+ function listWeekdaysMin(localeSorted, format, index) { -+ return listWeekdaysImpl(localeSorted, format, index, 'weekdaysMin'); -+ } -+ -+ getSetGlobalLocale('en', { -+ eras: [ -+ { -+ since: '0001-01-01', -+ until: +Infinity, -+ offset: 1, -+ name: 'Anno Domini', -+ narrow: 'AD', -+ abbr: 'AD', -+ }, -+ { -+ since: '0000-12-31', -+ until: -Infinity, -+ offset: 1, -+ name: 'Before Christ', -+ narrow: 'BC', -+ abbr: 'BC', -+ }, -+ ], -+ dayOfMonthOrdinalParse: /\d{1,2}(th|st|nd|rd)/, -+ ordinal: function (number) { -+ var b = number % 10, -+ output = -+ toInt((number % 100) / 10) === 1 -+ ? 'th' -+ : b === 1 -+ ? 'st' -+ : b === 2 -+ ? 'nd' -+ : b === 3 -+ ? 'rd' -+ : 'th'; -+ return number + output; -+ }, -+ }); -+ -+ // Side effect imports -+ -+ hooks.lang = deprecate( -+ 'moment.lang is deprecated. Use moment.locale instead.', -+ getSetGlobalLocale -+ ); -+ hooks.langData = deprecate( -+ 'moment.langData is deprecated. Use moment.localeData instead.', -+ getLocale -+ ); -+ -+ var mathAbs = Math.abs; -+ -+ function abs() { -+ var data = this._data; -+ -+ this._milliseconds = mathAbs(this._milliseconds); -+ this._days = mathAbs(this._days); -+ this._months = mathAbs(this._months); -+ -+ data.milliseconds = mathAbs(data.milliseconds); -+ data.seconds = mathAbs(data.seconds); -+ data.minutes = mathAbs(data.minutes); -+ data.hours = mathAbs(data.hours); -+ data.months = mathAbs(data.months); -+ data.years = mathAbs(data.years); -+ -+ return this; -+ } -+ -+ function addSubtract$1(duration, input, value, direction) { -+ var other = createDuration(input, value); -+ -+ duration._milliseconds += direction * other._milliseconds; -+ duration._days += direction * other._days; -+ duration._months += direction * other._months; -+ -+ return duration._bubble(); -+ } -+ -+ // supports only 2.0-style add(1, 's') or add(duration) -+ function add$1(input, value) { -+ return addSubtract$1(this, input, value, 1); -+ } -+ -+ // supports only 2.0-style subtract(1, 's') or subtract(duration) -+ function subtract$1(input, value) { -+ return addSubtract$1(this, input, value, -1); -+ } -+ -+ function absCeil(number) { -+ if (number < 0) { -+ return Math.floor(number); -+ } else { -+ return Math.ceil(number); -+ } -+ } -+ -+ function bubble() { -+ var milliseconds = this._milliseconds, -+ days = this._days, -+ months = this._months, -+ data = this._data, -+ seconds, -+ minutes, -+ hours, -+ years, -+ monthsFromDays; -+ -+ // if we have a mix of positive and negative values, bubble down first -+ // check: https://github.com/moment/moment/issues/2166 -+ if ( -+ !( -+ (milliseconds >= 0 && days >= 0 && months >= 0) || -+ (milliseconds <= 0 && days <= 0 && months <= 0) -+ ) -+ ) { -+ milliseconds += absCeil(monthsToDays(months) + days) * 864e5; -+ days = 0; -+ months = 0; -+ } -+ -+ // The following code bubbles up values, see the tests for -+ // examples of what that means. -+ data.milliseconds = milliseconds % 1000; -+ -+ seconds = absFloor(milliseconds / 1000); -+ data.seconds = seconds % 60; -+ -+ minutes = absFloor(seconds / 60); -+ data.minutes = minutes % 60; -+ -+ hours = absFloor(minutes / 60); -+ data.hours = hours % 24; -+ -+ days += absFloor(hours / 24); -+ -+ // convert days to months -+ monthsFromDays = absFloor(daysToMonths(days)); -+ months += monthsFromDays; -+ days -= absCeil(monthsToDays(monthsFromDays)); -+ -+ // 12 months -> 1 year -+ years = absFloor(months / 12); -+ months %= 12; -+ -+ data.days = days; -+ data.months = months; -+ data.years = years; -+ -+ return this; -+ } -+ -+ function daysToMonths(days) { -+ // 400 years have 146097 days (taking into account leap year rules) -+ // 400 years have 12 months === 4800 -+ return (days * 4800) / 146097; -+ } -+ -+ function monthsToDays(months) { -+ // the reverse of daysToMonths -+ return (months * 146097) / 4800; -+ } -+ -+ function as(units) { -+ if (!this.isValid()) { -+ return NaN; -+ } -+ var days, -+ months, -+ milliseconds = this._milliseconds; -+ -+ units = normalizeUnits(units); -+ -+ if (units === 'month' || units === 'quarter' || units === 'year') { -+ days = this._days + milliseconds / 864e5; -+ months = this._months + daysToMonths(days); -+ switch (units) { -+ case 'month': -+ return months; -+ case 'quarter': -+ return months / 3; -+ case 'year': -+ return months / 12; -+ } -+ } else { -+ // handle milliseconds separately because of floating point math errors (issue #1867) -+ days = this._days + Math.round(monthsToDays(this._months)); -+ switch (units) { -+ case 'week': -+ return days / 7 + milliseconds / 6048e5; -+ case 'day': -+ return days + milliseconds / 864e5; -+ case 'hour': -+ return days * 24 + milliseconds / 36e5; -+ case 'minute': -+ return days * 1440 + milliseconds / 6e4; -+ case 'second': -+ return days * 86400 + milliseconds / 1000; -+ // Math.floor prevents floating point math errors here -+ case 'millisecond': -+ return Math.floor(days * 864e5) + milliseconds; -+ default: -+ throw new Error('Unknown unit ' + units); -+ } -+ } -+ } -+ -+ // TODO: Use this.as('ms')? -+ function valueOf$1() { -+ if (!this.isValid()) { -+ return NaN; -+ } -+ return ( -+ this._milliseconds + -+ this._days * 864e5 + -+ (this._months % 12) * 2592e6 + -+ toInt(this._months / 12) * 31536e6 -+ ); -+ } -+ -+ function makeAs(alias) { -+ return function () { -+ return this.as(alias); -+ }; -+ } -+ -+ var asMilliseconds = makeAs('ms'), -+ asSeconds = makeAs('s'), -+ asMinutes = makeAs('m'), -+ asHours = makeAs('h'), -+ asDays = makeAs('d'), -+ asWeeks = makeAs('w'), -+ asMonths = makeAs('M'), -+ asQuarters = makeAs('Q'), -+ asYears = makeAs('y'); -+ -+ function clone$1() { -+ return createDuration(this); -+ } -+ -+ function get$2(units) { -+ units = normalizeUnits(units); -+ return this.isValid() ? this[units + 's']() : NaN; -+ } -+ -+ function makeGetter(name) { -+ return function () { -+ return this.isValid() ? this._data[name] : NaN; -+ }; -+ } -+ -+ var milliseconds = makeGetter('milliseconds'), -+ seconds = makeGetter('seconds'), -+ minutes = makeGetter('minutes'), -+ hours = makeGetter('hours'), -+ days = makeGetter('days'), -+ months = makeGetter('months'), -+ years = makeGetter('years'); -+ -+ function weeks() { -+ return absFloor(this.days() / 7); -+ } -+ -+ var round = Math.round, -+ thresholds = { -+ ss: 44, // a few seconds to seconds -+ s: 45, // seconds to minute -+ m: 45, // minutes to hour -+ h: 22, // hours to day -+ d: 26, // days to month/week -+ w: null, // weeks to month -+ M: 11, // months to year -+ }; -+ -+ // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize -+ function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { -+ return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); -+ } -+ -+ function relativeTime$1(posNegDuration, withoutSuffix, thresholds, locale) { -+ var duration = createDuration(posNegDuration).abs(), -+ seconds = round(duration.as('s')), -+ minutes = round(duration.as('m')), -+ hours = round(duration.as('h')), -+ days = round(duration.as('d')), -+ months = round(duration.as('M')), -+ weeks = round(duration.as('w')), -+ years = round(duration.as('y')), -+ a = -+ (seconds <= thresholds.ss && ['s', seconds]) || -+ (seconds < thresholds.s && ['ss', seconds]) || -+ (minutes <= 1 && ['m']) || -+ (minutes < thresholds.m && ['mm', minutes]) || -+ (hours <= 1 && ['h']) || -+ (hours < thresholds.h && ['hh', hours]) || -+ (days <= 1 && ['d']) || -+ (days < thresholds.d && ['dd', days]); -+ -+ if (thresholds.w != null) { -+ a = -+ a || -+ (weeks <= 1 && ['w']) || -+ (weeks < thresholds.w && ['ww', weeks]); -+ } -+ a = a || -+ (months <= 1 && ['M']) || -+ (months < thresholds.M && ['MM', months]) || -+ (years <= 1 && ['y']) || ['yy', years]; -+ -+ a[2] = withoutSuffix; -+ a[3] = +posNegDuration > 0; -+ a[4] = locale; -+ return substituteTimeAgo.apply(null, a); -+ } -+ -+ // This function allows you to set the rounding function for relative time strings -+ function getSetRelativeTimeRounding(roundingFunction) { -+ if (roundingFunction === undefined) { -+ return round; -+ } -+ if (typeof roundingFunction === 'function') { -+ round = roundingFunction; -+ return true; -+ } -+ return false; -+ } -+ -+ // This function allows you to set a threshold for relative time strings -+ function getSetRelativeTimeThreshold(threshold, limit) { -+ if (thresholds[threshold] === undefined) { -+ return false; -+ } -+ if (limit === undefined) { -+ return thresholds[threshold]; -+ } -+ thresholds[threshold] = limit; -+ if (threshold === 's') { -+ thresholds.ss = limit - 1; -+ } -+ return true; -+ } -+ -+ function humanize(argWithSuffix, argThresholds) { -+ if (!this.isValid()) { -+ return this.localeData().invalidDate(); -+ } -+ -+ var withSuffix = false, -+ th = thresholds, -+ locale, -+ output; -+ -+ if (typeof argWithSuffix === 'object') { -+ argThresholds = argWithSuffix; -+ argWithSuffix = false; -+ } -+ if (typeof argWithSuffix === 'boolean') { -+ withSuffix = argWithSuffix; -+ } -+ if (typeof argThresholds === 'object') { -+ th = Object.assign({}, thresholds, argThresholds); -+ if (argThresholds.s != null && argThresholds.ss == null) { -+ th.ss = argThresholds.s - 1; -+ } -+ } -+ -+ locale = this.localeData(); -+ output = relativeTime$1(this, !withSuffix, th, locale); -+ -+ if (withSuffix) { -+ output = locale.pastFuture(+this, output); -+ } -+ -+ return locale.postformat(output); -+ } -+ -+ var abs$1 = Math.abs; -+ -+ function sign(x) { -+ return (x > 0) - (x < 0) || +x; -+ } -+ -+ function toISOString$1() { -+ // for ISO strings we do not use the normal bubbling rules: -+ // * milliseconds bubble up until they become hours -+ // * days do not bubble at all -+ // * months bubble up until they become years -+ // This is because there is no context-free conversion between hours and days -+ // (think of clock changes) -+ // and also not between days and months (28-31 days per month) -+ if (!this.isValid()) { -+ return this.localeData().invalidDate(); -+ } -+ -+ var seconds = abs$1(this._milliseconds) / 1000, -+ days = abs$1(this._days), -+ months = abs$1(this._months), -+ minutes, -+ hours, -+ years, -+ s, -+ total = this.asSeconds(), -+ totalSign, -+ ymSign, -+ daysSign, -+ hmsSign; -+ -+ if (!total) { -+ // this is the same as C#'s (Noda) and python (isodate)... -+ // but not other JS (goog.date) -+ return 'P0D'; -+ } -+ -+ // 3600 seconds -> 60 minutes -> 1 hour -+ minutes = absFloor(seconds / 60); -+ hours = absFloor(minutes / 60); -+ seconds %= 60; -+ minutes %= 60; -+ -+ // 12 months -> 1 year -+ years = absFloor(months / 12); -+ months %= 12; -+ -+ // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js -+ s = seconds ? seconds.toFixed(3).replace(/\.?0+$/, '') : ''; -+ -+ totalSign = total < 0 ? '-' : ''; -+ ymSign = sign(this._months) !== sign(total) ? '-' : ''; -+ daysSign = sign(this._days) !== sign(total) ? '-' : ''; -+ hmsSign = sign(this._milliseconds) !== sign(total) ? '-' : ''; -+ -+ return ( -+ totalSign + -+ 'P' + -+ (years ? ymSign + years + 'Y' : '') + -+ (months ? ymSign + months + 'M' : '') + -+ (days ? daysSign + days + 'D' : '') + -+ (hours || minutes || seconds ? 'T' : '') + -+ (hours ? hmsSign + hours + 'H' : '') + -+ (minutes ? hmsSign + minutes + 'M' : '') + -+ (seconds ? hmsSign + s + 'S' : '') -+ ); -+ } -+ -+ var proto$2 = Duration.prototype; -+ -+ proto$2.isValid = isValid$1; -+ proto$2.abs = abs; -+ proto$2.add = add$1; -+ proto$2.subtract = subtract$1; -+ proto$2.as = as; -+ proto$2.asMilliseconds = asMilliseconds; -+ proto$2.asSeconds = asSeconds; -+ proto$2.asMinutes = asMinutes; -+ proto$2.asHours = asHours; -+ proto$2.asDays = asDays; -+ proto$2.asWeeks = asWeeks; -+ proto$2.asMonths = asMonths; -+ proto$2.asQuarters = asQuarters; -+ proto$2.asYears = asYears; -+ proto$2.valueOf = valueOf$1; -+ proto$2._bubble = bubble; -+ proto$2.clone = clone$1; -+ proto$2.get = get$2; -+ proto$2.milliseconds = milliseconds; -+ proto$2.seconds = seconds; -+ proto$2.minutes = minutes; -+ proto$2.hours = hours; -+ proto$2.days = days; -+ proto$2.weeks = weeks; -+ proto$2.months = months; -+ proto$2.years = years; -+ proto$2.humanize = humanize; -+ proto$2.toISOString = toISOString$1; -+ proto$2.toString = toISOString$1; -+ proto$2.toJSON = toISOString$1; -+ proto$2.locale = locale; -+ proto$2.localeData = localeData; -+ -+ proto$2.toIsoString = deprecate( -+ 'toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', -+ toISOString$1 -+ ); -+ proto$2.lang = lang; -+ -+ // FORMATTING -+ -+ addFormatToken('X', 0, 0, 'unix'); -+ addFormatToken('x', 0, 0, 'valueOf'); -+ -+ // PARSING -+ -+ addRegexToken('x', matchSigned); -+ addRegexToken('X', matchTimestamp); -+ addParseToken('X', function (input, array, config) { -+ config._d = new Date(parseFloat(input) * 1000); -+ }); -+ addParseToken('x', function (input, array, config) { -+ config._d = new Date(toInt(input)); -+ }); -+ -+ //! moment.js -+ -+ hooks.version = '2.29.1'; -+ -+ setHookCallback(createLocal); -+ -+ hooks.fn = proto; -+ hooks.min = min; -+ hooks.max = max; -+ hooks.now = now; -+ hooks.utc = createUTC; -+ hooks.unix = createUnix; -+ hooks.months = listMonths; -+ hooks.isDate = isDate; -+ hooks.locale = getSetGlobalLocale; -+ hooks.invalid = createInvalid; -+ hooks.duration = createDuration; -+ hooks.isMoment = isMoment; -+ hooks.weekdays = listWeekdays; -+ hooks.parseZone = createInZone; -+ hooks.localeData = getLocale; -+ hooks.isDuration = isDuration; -+ hooks.monthsShort = listMonthsShort; -+ hooks.weekdaysMin = listWeekdaysMin; -+ hooks.defineLocale = defineLocale; -+ hooks.updateLocale = updateLocale; -+ hooks.locales = listLocales; -+ hooks.weekdaysShort = listWeekdaysShort; -+ hooks.normalizeUnits = normalizeUnits; -+ hooks.relativeTimeRounding = getSetRelativeTimeRounding; -+ hooks.relativeTimeThreshold = getSetRelativeTimeThreshold; -+ hooks.calendarFormat = getCalendarFormat; -+ hooks.prototype = proto; -+ -+ // currently HTML5 input type only supports 24-hour formats -+ hooks.HTML5_FMT = { -+ DATETIME_LOCAL: 'YYYY-MM-DDTHH:mm', // -+ DATETIME_LOCAL_SECONDS: 'YYYY-MM-DDTHH:mm:ss', // -+ DATETIME_LOCAL_MS: 'YYYY-MM-DDTHH:mm:ss.SSS', // -+ DATE: 'YYYY-MM-DD', // -+ TIME: 'HH:mm', // -+ TIME_SECONDS: 'HH:mm:ss', // -+ TIME_MS: 'HH:mm:ss.SSS', // -+ WEEK: 'GGGG-[W]WW', // -+ MONTH: 'YYYY-MM', // -+ }; -+ -+ return hooks; -+ -+}))); -diff --git a/pkg/http-server/assets/webhook-scan-logs.css b/pkg/http-server/assets/webhook-scan-logs.css -new file mode 100644 -index 0000000..5e701fb ---- /dev/null -+++ b/pkg/http-server/assets/webhook-scan-logs.css -@@ -0,0 +1,33 @@ -+ul { -+ list-style: none; -+ margin-left: 16px; -+ padding: 0; -+} -+ -+.jsontree_child-nodes { -+ margin-left: 16px; -+ padding-left: 16px; -+} -+ -+* { -+ box-sizing: border-box; -+} -+ -+.table-sm td, .table-sm th { -+ padding: 10px; -+} -+ -+.review-status.warn { -+ color: #e28d43; -+ font-weight: bold; -+} -+ -+.review-status.rejected { -+ color: #e24343; -+ font-weight: bold; -+} -+ -+.review-status.allowed { -+ color: #43e268; -+ font-weight: bold; -+} -diff --git a/pkg/http-server/assets/webhook-scan-logs.js b/pkg/http-server/assets/webhook-scan-logs.js -new file mode 100644 -index 0000000..8655332 ---- /dev/null -+++ b/pkg/http-server/assets/webhook-scan-logs.js -@@ -0,0 +1,44 @@ -+// Replace all json-objects elements to be a JSON tree -+let jsonElements = document.getElementsByClassName("json-object") -+for (var i = 0; i < jsonElements.length; i++) { -+ let element = jsonElements[i] -+ if (element.innerText.length < 1) { -+ continue -+ } -+ -+ let data = JSON.parse(element.innerText); -+ element.innerText = "" -+ -+ jsonTree.create(data, element); -+} -+ -+// Replace all time-object elements to be in the 'DD/MM/YYYY hh:mm:ss A' format of moment.js -+let timeElements = document.getElementsByClassName("time-object") -+for (var i = 0; i < timeElements.length; i++) { -+ let element = timeElements[i] -+ let elapsedTimeUntilNow = Date.now() - new Date(element.innerText) -+ if (elapsedTimeUntilNow / 1000 < 120) { -+ // In case elapsed less than 2 minutes, show "A few seconds ago" -+ element.innerText = moment(element.innerText).fromNow() -+ } -+ else { -+ element.innerText = moment(element.innerText).format('DD/MM/YYYY hh:mm:ss A') -+ } -+} -+ -+// Change the colors of the review status -+let statusElements = document.getElementsByClassName("review-status") -+for (var i = 0; i < statusElements.length; i++) { -+ let element = statusElements[i] -+ switch (element.innerText) { -+ case "Allowed": -+ element.classList.add("allowed") -+ break -+ case "Rejected": -+ element.classList.add("rejected") -+ break -+ default: -+ element.classList.add("warn") -+ break -+ } -+} -diff --git a/pkg/http-server/constants.go b/pkg/http-server/constants.go -index 2226cf4..cb7e4ea 100644 ---- a/pkg/http-server/constants.go -+++ b/pkg/http-server/constants.go -@@ -20,6 +20,9 @@ const ( - // GatewayDefaultPort - default port at which the http server listens - GatewayDefaultPort = "9010" - -+ // GatewayDefaultPort - default port at which the https server listens -+ TLSGatewayDefaultPort = "9443" -+ - // APIVersion - default api version for REST endpoints - APIVersion = "v1" - ) -diff --git a/pkg/http-server/handler.go b/pkg/http-server/handler.go -index eb86e5a..e165d25 100644 ---- a/pkg/http-server/handler.go -+++ b/pkg/http-server/handler.go -@@ -19,9 +19,12 @@ package httpserver - // APIHandler struct for http api server - type APIHandler struct { - test bool -+ configFile string - } - - // NewAPIHandler returns a new APIHandler{} --func NewAPIHandler() *APIHandler { -- return &APIHandler{} -+func NewAPIHandler(configFile string) *APIHandler { -+ return &APIHandler{ -+ configFile: configFile, -+ } - } -diff --git a/pkg/http-server/handler_test.go b/pkg/http-server/handler_test.go -index 15c926e..d4d88c1 100644 ---- a/pkg/http-server/handler_test.go -+++ b/pkg/http-server/handler_test.go -@@ -8,8 +8,10 @@ import ( - func TestNewAPIHandler(t *testing.T) { - t.Run("new API gateway", func(t *testing.T) { - var ( -- want = APIHandler{} -- got = NewAPIHandler() -+ want = APIHandler{ -+ configFile: "", -+ } -+ got = NewAPIHandler("") - ) - if !reflect.DeepEqual(*got, want) { - t.Errorf("got: '%v', want: '%v'", *got, want) -diff --git a/pkg/http-server/health_test.go b/pkg/http-server/health_test.go -index 08b92f8..2a447ea 100644 ---- a/pkg/http-server/health_test.go -+++ b/pkg/http-server/health_test.go -@@ -8,7 +8,7 @@ import ( - - func TestHealth(t *testing.T) { - -- handler := NewAPIHandler() -+ handler := NewAPIHandler("") - - t.Run("test health api", func(t *testing.T) { - var ( -diff --git a/pkg/http-server/k8s_testdata/config-deny-category.toml b/pkg/http-server/k8s_testdata/config-deny-category.toml -new file mode 100644 -index 0000000..b3d4204 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/config-deny-category.toml -@@ -0,0 +1,5 @@ -+[k8s-deny-rules] -+ denied-categories = [ -+ "Identity and Access Management", -+ "Network Security", -+ ] -diff --git a/pkg/http-server/k8s_testdata/config-deny-high.toml b/pkg/http-server/k8s_testdata/config-deny-high.toml -new file mode 100644 -index 0000000..5046654 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/config-deny-high.toml -@@ -0,0 +1,5 @@ -+[severity] -+level = "medium" -+ -+[k8s-deny-rules] -+ denied-severity = "high" -diff --git a/pkg/http-server/k8s_testdata/config-deny-non-existing-category.toml b/pkg/http-server/k8s_testdata/config-deny-non-existing-category.toml -new file mode 100644 -index 0000000..d38bc48 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/config-deny-non-existing-category.toml -@@ -0,0 +1,8 @@ -+[severity] -+level = "medium" -+ -+[k8s-deny-rules] -+ denied-categories = [ -+ "Hola", -+ "Invalid", -+ ] -diff --git a/pkg/http-server/k8s_testdata/config-medium-severity.toml b/pkg/http-server/k8s_testdata/config-medium-severity.toml -new file mode 100644 -index 0000000..17b1aed ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/config-medium-severity.toml -@@ -0,0 +1,2 @@ -+[severity] -+level = "medium" -diff --git a/pkg/http-server/k8s_testdata/config-specific-rule.toml b/pkg/http-server/k8s_testdata/config-specific-rule.toml -new file mode 100644 -index 0000000..ad35bd7 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/config-specific-rule.toml -@@ -0,0 +1,5 @@ -+[rules] -+ scan-rules = [ -+ "AWS.S3Bucket.DS.High.1043", -+ "accurics.kubernetes.IAM.107" -+ ] -diff --git a/pkg/http-server/k8s_testdata/empty.json b/pkg/http-server/k8s_testdata/empty.json -new file mode 100644 -index 0000000..e69de29 -diff --git a/pkg/http-server/k8s_testdata/empty_object.json b/pkg/http-server/k8s_testdata/empty_object.json -new file mode 100644 -index 0000000..fb4f3ba ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/empty_object.json -@@ -0,0 +1,7 @@ -+{ -+ "apiVersion": "admission.k8s.io/v1", -+ "kind": "AdmissionReview", -+ "request": { -+ "uid": "705ab4f5-6393-11e8-b7cc-42010a800002" -+ } -+} -diff --git a/pkg/http-server/k8s_testdata/invalid.json b/pkg/http-server/k8s_testdata/invalid.json -new file mode 100644 -index 0000000..f9ff3aa ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/invalid.json -@@ -0,0 +1 @@ -+some invalid tf file -diff --git a/pkg/http-server/k8s_testdata/risky_testconfig.json b/pkg/http-server/k8s_testdata/risky_testconfig.json -new file mode 100644 -index 0000000..3b1f42d ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/risky_testconfig.json -@@ -0,0 +1,27 @@ -+{ -+ "apiVersion":"admission.k8s.io/v1", -+ "kind":"AdmissionReview", -+ "request":{ -+ "uid":"705ab4f5-6393-11e8-b7cc-42010a800002", -+ "object": -+ { -+ "apiVersion": "v1", -+ "kind": "Pod", -+ "metadata": { -+ "name": "root-run-unset" -+ }, -+ "spec": { -+ "containers": [ -+ { -+ "name": "busybox", -+ "image": "busybox", -+ "securityContext": { -+ "allowPrivilegeEscalation": false, -+ "readOnlyRootFilesystem": true -+ } -+ } -+ ] -+ } -+ } -+ } -+} -diff --git a/pkg/http-server/k8s_testdata/testconfig.json b/pkg/http-server/k8s_testdata/testconfig.json -new file mode 100644 -index 0000000..6e3bc1a ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testconfig.json -@@ -0,0 +1,27 @@ -+{ -+ "apiVersion": "admission.k8s.io/v1", -+ "kind": "AdmissionReview", -+ "request": { -+ "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", -+ "object": { -+ "apiVersion": "v1", -+ "kind": "Service", -+ "metadata": { -+ "creationTimestamp": "2021-02-16T19:16:01Z", -+ "labels": { -+ "run": "nginx" -+ }, -+ "name": "nginx", -+ "namespace": "default", -+ "resourceVersion": "17561", -+ "selfLink": "/api/v1/namespaces/default/pods/nginx", -+ "uid": "7a269efe-d951-49b6-a3af-e1a265cb9efe" -+ }, -+ "spec": { -+ "containers": [ -+ ] -+ } -+ }, -+ "operation": "CREATE" -+ } -+} -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json -new file mode 100755 -index 0000000..24409fb ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json -@@ -0,0 +1,21 @@ -+{ -+ "name": "privilegeEscalationCheck", -+ "file": "securityContextCheck.rego", -+ "template_args": { -+ "allowed": "false", -+ "arg1": "cpu", -+ "arg2": "limits", -+ "name": "privilegeEscalationCheck", -+ "not_allowed": "true", -+ "param": "allowPrivilegeEscalation", -+ "param1": "securityContext", -+ "prefix": "", -+ "suffix": "", -+ "value": "true" -+ }, -+ "severity": "HIGH", -+ "description": "Containers Should Not Run with AllowPrivilegeEscalation", -+ "reference_id": "AC-K8-CA-PO-H-0165", -+ "category": "Cloud Assets Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json -new file mode 100755 -index 0000000..d0bff54 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "kubeDashboardEnabled", -+ "file": "kubeDashboardEnabled.rego", -+ "template_args": { -+ "name": "kubeDashboardEnabled", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Ensure Kubernetes Dashboard Is Not Deployed", -+ "reference_id": "AC-K8-DS-PO-M-0176", -+ "category": "Data Security", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json -new file mode 100755 -index 0000000..d8a40cc ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "tillerDeployed", -+ "file": "tillerDeployed.rego", -+ "template_args": { -+ "name": "tillerDeployed", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Ensure That Tiller (Helm V2) Is Not Deployed", -+ "reference_id": "AC-K8-DS-PO-M-0177", -+ "category": "Data Security", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json -new file mode 100755 -index 0000000..d07858d ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "priviledgedContainersEnabled", -+ "file": "priviledgedContainersEnabled.rego", -+ "template_args": { -+ "name": "priviledgedContainersEnabled", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Minimize the admission of privileged containers", -+ "reference_id": "AC-K8-IA-PO-H-0106", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json -new file mode 100755 -index 0000000..71f74c3 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "disallowedSysCalls", -+ "file": "disallowedSysCalls.rego", -+ "template_args": { -+ "name": "disallowedSysCalls", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Allowing the pod to make system level calls provide access to host/node sensitive information", -+ "reference_id": "AC-K8-IA-PO-H-0137", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json -new file mode 100755 -index 0000000..16cfd6d ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "allowedHostPath", -+ "file": "allowedHostPath.rego", -+ "template_args": { -+ "name": "allowedHostPath", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Allowing hostPaths to mount to Pod arise the probability of getting access to the node's filesystem", -+ "reference_id": "AC-K8-IA-PO-H-0138", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json -new file mode 100755 -index 0000000..f7c9d54 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json -@@ -0,0 +1,21 @@ -+{ -+ "name": "runAsNonRootCheck", -+ "file": "securityContextCheck.rego", -+ "template_args": { -+ "allowed": "false", -+ "arg1": "cpu", -+ "arg2": "limits", -+ "name": "runAsNonRootCheck", -+ "not_allowed": "true", -+ "param": "runAsNonRoot", -+ "param1": "securityContext", -+ "prefix": "", -+ "suffix": "", -+ "value": "false" -+ }, -+ "severity": "HIGH", -+ "description": "Minimize Admission of Root Containers", -+ "reference_id": "AC-K8-IA-PO-H-0168", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json -new file mode 100755 -index 0000000..6f9be71 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "autoMountTokenEnabled", -+ "file": "autoMountTokenEnabled.rego", -+ "template_args": { -+ "name": "autoMountTokenEnabled", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Ensure that Service Account Tokens are only mounted where necessary", -+ "reference_id": "AC-K8-IA-PO-M-0105", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json -new file mode 100755 -index 0000000..d7befdd ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "appArmorProfile", -+ "file": "appArmorProfile.rego", -+ "template_args": { -+ "name": "appArmorProfile", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "AppArmor profile not set to default or custom profile will make the container vulnerable to kernel level threats", -+ "reference_id": "AC-K8-IA-PO-M-0135", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json -new file mode 100755 -index 0000000..5a22d3f ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "allowedProcMount", -+ "file": "allowedProcMount.rego", -+ "template_args": { -+ "name": "allowedProcMount", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Unmasking the procMount will allow more information than is necessary to the program running in the containers spawned by k8s", -+ "reference_id": "AC-K8-IA-PO-M-0139", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json -new file mode 100755 -index 0000000..10fad68 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json -@@ -0,0 +1,21 @@ -+{ -+ "name": "readOnlyFileSystem", -+ "file": "securityContextCheck.rego", -+ "template_args": { -+ "allowed": "false", -+ "arg1": "limits", -+ "arg2": "cpu", -+ "name": "readOnlyFileSystem", -+ "not_allowed": "true", -+ "param": "readOnlyRootFilesystem", -+ "param1": "securityContext", -+ "prefix": "", -+ "suffix": "", -+ "value": "false" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container images with readOnlyRootFileSystem set as false mounts the container root file system with write permissions", -+ "reference_id": "AC-K8-IA-PO-M-0140", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json -new file mode 100755 -index 0000000..5293c73 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "secCompProfile", -+ "file": "secCompProfile.rego", -+ "template_args": { -+ "name": "secCompProfile", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Default seccomp profile not enabled will make the container to make non-essential system calls", -+ "reference_id": "AC-K8-IA-PO-M-0141", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json -new file mode 100755 -index 0000000..07843f8 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json -@@ -0,0 +1,22 @@ -+{ -+ "name": "allowedVolumes", -+ "file": "allowedVolumes.rego", -+ "template_args": { -+ "name": "allowedVolumes", -+ "prefix": "", -+ "secure_volumes": [ -+ "configMap", -+ "emptyDir", -+ "projected", -+ "secret", -+ "downwardAPI", -+ "persistentVolumeClaim" -+ ], -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Some volume types mount the host file system paths to the pod or container, thus increasing the chance of escaping the container to access the host", -+ "reference_id": "AC-K8-IA-PO-M-0143", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json -new file mode 100755 -index 0000000..a98195d ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "falseHostPID", -+ "file": "specBoolCheck.rego", -+ "template_args": { -+ "name": "falseHostPID", -+ "param": "hostPID", -+ "prefix": "", -+ "suffix": "", -+ "value": "true" -+ }, -+ "severity": "MEDIUM", -+ "description": "Containers Should Not Share Host Process ID Namespace", -+ "reference_id": "AC-K8-IA-PO-M-0162", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json -new file mode 100755 -index 0000000..11f59e9 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json -@@ -0,0 +1,15 @@ -+{ -+ "name": "netRawCapabilityUsed", -+ "file": "capabilityUsed.rego", -+ "template_args": { -+ "attribute": "requiredDropCapabilities", -+ "name": "netRawCapabilityUsed", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Minimize the admission of containers with the NET_RAW capability", -+ "reference_id": "AC-K8-IA-PS-M-0112", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json -new file mode 100755 -index 0000000..23c8d90 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "secretsAsEnvVariables", -+ "file": "secretsAsEnvVariables.rego", -+ "template_args": { -+ "name": "secretsAsEnvVariables", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Prefer using secrets as files over secrets as environment variables", -+ "reference_id": "AC-K8-NS-PO-H-0117", -+ "category": "Network Security", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json -new file mode 100755 -index 0000000..b211361 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "capSysAdminUsed", -+ "file": "capSysAdminUsed.rego", -+ "template_args": { -+ "name": "capSysAdminUsed", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Do Not Use CAP_SYS_ADMIN Linux Capability", -+ "reference_id": "AC-K8-NS-PO-H-0170", -+ "category": "Network Security", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json -new file mode 100755 -index 0000000..43ba243 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "securityContextUsed", -+ "file": "securityContextUsed.rego", -+ "template_args": { -+ "name": "securityContextUsed", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Apply Security Context to Your Pods and Containers", -+ "reference_id": "AC-K8-NS-PO-M-0122", -+ "category": "Network Security", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json -new file mode 100755 -index 0000000..804a12e ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "imageWithoutDigest", -+ "file": "imageWithoutDigest.rego", -+ "template_args": { -+ "name": "imageWithoutDigest", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Image without digest affects the integrity principle of image security", -+ "reference_id": "AC-K8-NS-PO-M-0133", -+ "category": "Network Security", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json -new file mode 100755 -index 0000000..e96b364 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "falseHostIPC", -+ "file": "specBoolCheck.rego", -+ "template_args": { -+ "name": "falseHostIPC", -+ "param": "hostIPC", -+ "prefix": "", -+ "suffix": "", -+ "value": "true" -+ }, -+ "severity": "MEDIUM", -+ "description": "Containers Should Not Share Host IPC Namespace", -+ "reference_id": "AC-K8-NS-PO-M-0163", -+ "category": "Network Security", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json -new file mode 100755 -index 0000000..5c893ce ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "falseHostNetwork", -+ "file": "specBoolCheck.rego", -+ "template_args": { -+ "name": "falseHostNetwork", -+ "param": "hostNetwork", -+ "prefix": "", -+ "suffix": "", -+ "value": "true" -+ }, -+ "severity": "MEDIUM", -+ "description": "Containers Should Not Share the Host Network Namespace", -+ "reference_id": "AC-K8-NS-PO-M-0164", -+ "category": "Network Security", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json -new file mode 100755 -index 0000000..df493d8 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json -@@ -0,0 +1,17 @@ -+{ -+ "name": "dontConnectDockerSock", -+ "file": "dockerSockCheck.rego", -+ "template_args": { -+ "attrib": "spec.volumes[_].hostPath", -+ "name": "dontConnectDockerSock", -+ "param": "path", -+ "prefix": "", -+ "suffix": "", -+ "value": "/var/run/docker" -+ }, -+ "severity": "MEDIUM", -+ "description": "Restrict Mounting Docker Socket in a Container", -+ "reference_id": "AC-K8-NS-PO-M-0171", -+ "category": "Network Security", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json -new file mode 100755 -index 0000000..2243106 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "containersAsHighUID", -+ "file": "containersAsHighUID.rego", -+ "template_args": { -+ "name": "containersAsHighUID", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Containers Should Run as a High UID to Avoid Host Conflict", -+ "reference_id": "AC-K8-NS-PO-M-0182", -+ "category": "Network Security", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json -new file mode 100755 -index 0000000..6340d31 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json -@@ -0,0 +1,19 @@ -+{ -+ "name": "alwaysPullImages", -+ "file": "commandCheck.rego", -+ "template_args": { -+ "argument": "--enable-admission-plugins", -+ "name": "alwaysPullImages", -+ "negation": "", -+ "optional": "", -+ "param": "AlwaysPullImages", -+ "prefix": "", -+ "presence": "not", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "AlwaysPullImages plugin is not set", -+ "reference_id": "AC-K8-OE-PK-M-0034", -+ "category": "Operational Efficiency", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json -new file mode 100755 -index 0000000..aebef86 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json -@@ -0,0 +1,21 @@ -+{ -+ "name": "CpuRequestsCheck", -+ "file": "securityContextCheck.rego", -+ "template_args": { -+ "allowed": "true", -+ "arg1": "requests", -+ "arg2": "cpu", -+ "name": "CpuRequestsCheck", -+ "not_allowed": "false", -+ "param": "resources", -+ "param1": "resources", -+ "prefix": "", -+ "suffix": "", -+ "value": "false" -+ }, -+ "severity": "Medium", -+ "description": "CPU Request Not Set in config file.", -+ "reference_id": "AC-K8-OE-PK-M-0155", -+ "category": "Operational Efficiency", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json -new file mode 100755 -index 0000000..c74835c ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json -@@ -0,0 +1,21 @@ -+{ -+ "name": "CpulimitsCheck", -+ "file": "securityContextCheck.rego", -+ "template_args": { -+ "allowed": "true", -+ "arg1": "limits", -+ "arg2": "cpu", -+ "name": "CpulimitsCheck", -+ "not_allowed": "false", -+ "param": "limits", -+ "param1": "resources", -+ "prefix": "", -+ "suffix": "", -+ "value": "false" -+ }, -+ "severity": "Medium", -+ "description": "CPU Limits Not Set in config file.", -+ "reference_id": "AC-K8-OE-PK-M-0156", -+ "category": "Operational Efficiency", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json -new file mode 100755 -index 0000000..691b588 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json -@@ -0,0 +1,21 @@ -+{ -+ "name": "MemoryRequestsCheck", -+ "file": "securityContextCheck.rego", -+ "template_args": { -+ "allowed": "true", -+ "arg1": "requests", -+ "arg2": "memory", -+ "name": "MemoryRequestsCheck", -+ "not_allowed": "false", -+ "param": "resources", -+ "param1": "resources", -+ "prefix": "", -+ "suffix": "", -+ "value": "false" -+ }, -+ "severity": "Medium", -+ "description": "Memory Request Not Set in config file.", -+ "reference_id": "AC-K8-OE-PK-M-0157", -+ "category": "Operational Efficiency", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json -new file mode 100755 -index 0000000..7ab678c ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json -@@ -0,0 +1,21 @@ -+{ -+ "name": "MemorylimitsCheck", -+ "file": "securityContextCheck.rego", -+ "template_args": { -+ "allowed": "true", -+ "arg1": "limits", -+ "arg2": "memory", -+ "name": "MemorylimitsCheck", -+ "not_allowed": "false", -+ "param": "limits", -+ "param1": "resources", -+ "prefix": "", -+ "suffix": "", -+ "value": "false" -+ }, -+ "severity": "Medium", -+ "description": "Memory Limits Not Set in config file.", -+ "reference_id": "AC-K8-OE-PK-M-0158", -+ "category": "Operational Efficiency", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json -new file mode 100755 -index 0000000..9ce0938 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "nolivenessProbe", -+ "file": "probeCheck.rego", -+ "template_args": { -+ "argument": "livenessProbe", -+ "argumentTF": "liveness_probe", -+ "name": "nolivenessProbe", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "LOW", -+ "description": "No liveness probe will ensure there is no recovery in case of unexpected errors", -+ "reference_id": "AC-K8-OE-PO-L-0129", -+ "category": "Operational Efficiency", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json -new file mode 100755 -index 0000000..a0e4058 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "noReadinessProbe", -+ "file": "probeCheck.rego", -+ "template_args": { -+ "argument": "readinessProbe", -+ "argumentTF": "readiness_probe", -+ "name": "noReadinessProbe", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "LOW", -+ "description": "No readiness probe will affect automatic recovery in case of unexpected errors", -+ "reference_id": "AC-K8-OE-PO-L-0130", -+ "category": "Operational Efficiency", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json -new file mode 100755 -index 0000000..83eec4e ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "imageWithLatestTag", -+ "file": "imageWithLatestTag.rego", -+ "template_args": { -+ "name": "imageWithLatestTag", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "LOW", -+ "description": "No tag or container image with :Latest tag makes difficult to rollback and track", -+ "reference_id": "AC-K8-OE-PO-L-0134", -+ "category": "Operational Efficiency", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json -new file mode 100755 -index 0000000..6e0c8fd ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "otherNamespace", -+ "file": "otherNamespace.rego", -+ "template_args": { -+ "name": "otherNamespace", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Default Namespace Should Not be Used", -+ "reference_id": "AC-K8-OE-PO-M-0166", -+ "category": "Operational Efficiency", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego -new file mode 100755 -index 0000000..e7d8463 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego -@@ -0,0 +1,107 @@ -+### this policy depends on the parameters specified by the user/client. Here we are considering that no hostPath are allowed### -+package accurics -+ -+#rule for pod -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ vols := pod.config.spec.volumes[_] -+ parameters := {} -+ has_field(vols, "hostPath") -+ allowedPaths := get_allowed_paths(parameters) -+ input_hostpath_violation(allowedPaths, vols) -+} -+ -+#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ vols := kind.config.spec.template.spec.volumes[_] -+ #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } -+ parameters := {} -+ has_field(vols, "hostPath") -+ allowedPaths := get_allowed_paths(parameters) -+ input_hostpath_violation(allowedPaths, vols) -+} -+ -+#rule for cron_job -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ vols := cron_job.config.spec.jobTemplate.spec.template.spec.volumes[_] -+ #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } -+ parameters := {} -+ has_field(vols, "hostPath") -+ allowedPaths := get_allowed_paths(parameters) -+ input_hostpath_violation(allowedPaths, vols) -+} -+ -+#function for all KINDs -+has_field(object, field) = true { -+ object[field] -+} -+ -+#now allowed paths are null, this function will run## -+get_allowed_paths(params) = out { -+ not params.allowedHostPath == "undefined" -+ out = [] -+} -+ -+input_hostpath_violation(allowedPaths, volume) { -+ allowedPaths == [] -+} -+ -+### below functions are for violation when user has specified the hostPath, for testing uncomment the parameter array of objects at top#### -+ -+get_allowed_paths(params) = out { -+ out = params.allowedHostPath -+} -+ -+input_hostpath_violation(allowedPaths, volume) { -+ not input_hostpath_allowed(allowedPaths, volume) -+} -+ -+input_hostpath_allowed(allowedPaths, volume) { -+ allowedHostPath := allowedPaths[_] -+ path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) -+ not allowedHostPath.readOnly == true -+} -+ -+input_hostpath_allowed(allowedPaths, volume) { -+ allowedHostPath := allowedPaths[_] -+ path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) -+ allowedHostPath.readOnly -+ not writeable_input_volume_mounts(volume.name) -+} -+ -+writeable_input_volume_mounts(volume_name) { -+ containers := input.kubernetes_pod[_].config.spec.containers[_] -+ mount := containers.volumeMounts[_] -+ mount.name == volume_name -+ not mount.readOnly -+} -+ -+path_matches(prefix, path) { -+ a := split(trim(prefix, "/"), "/") -+ b := split(trim(path, "/"), "/") -+ prefix_matches(a, b) -+} -+ -+prefix_matches(a, b) { -+ count(a) <= count(b) -+ not any_not_equal_upto(a, b, count(a)) -+} -+ -+any_not_equal_upto(a, b, n) { -+ a[i] != b[i] -+ i < n -+} -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego -new file mode 100755 -index 0000000..d8d6e62 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego -@@ -0,0 +1,126 @@ -+package accurics -+ -+#rule for pod_security_policy -+{{.prefix}}{{.name}}{{.suffix}}[psp.id] { -+ psp := input.kubernetes_pod_security_policy[_] -+ psp.config.spec.allowProcMountTypes != "Default" -+} -+ -+#rule for pod_security_policy terraform -+{{.prefix}}{{.name}}{{.suffix}}[psp.id] { -+ psp := input.kubernetes_pod_security_policy[_] -+ psp.config.spec.allow_proc_mount_types != "Default" -+} -+ -+#rule for pod -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ parameters := {} -+ container := pod.config.spec.containers[_] -+ container.securityContext.procMount -+ allowedProcMount := get_allowed_proc_mount(parameters) -+ not input_proc_mount_type_allowed(allowedProcMount, container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ parameters := {} -+ container := pod.config.spec.initContainers[_] -+ container.securityContext.procMount -+ allowedProcMount := get_allowed_proc_mount(parameters) -+ not input_proc_mount_type_allowed(allowedProcMount, container) -+} -+ -+#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.containers[_] -+ #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } -+ parameters := {} -+ container.securityContext.procMount -+ allowedProcMount := get_allowed_proc_mount(parameters) -+ not input_proc_mount_type_allowed(allowedProcMount, container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.initContainers[_] -+ #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } -+ parameters := {} -+ container.securityContext.procMount -+ allowedProcMount := get_allowed_proc_mount(parameters) -+ not input_proc_mount_type_allowed(allowedProcMount, container) -+} -+ -+#rule for cron_job -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] -+ #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } -+ parameters := {} -+ container.securityContext.procMount -+ allowedProcMount := get_allowed_proc_mount(parameters) -+ not input_proc_mount_type_allowed(allowedProcMount, container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] -+ #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } -+ parameters := {} -+ container.securityContext.procMount -+ allowedProcMount := get_allowed_proc_mount(parameters) -+ not input_proc_mount_type_allowed(allowedProcMount, container) -+} -+ -+###this will get satisfied as no parameters are provided, thus checking with the baseline configuration which is checking that the procmount is default#### -+get_allowed_proc_mount(params) = out { -+ not params.procMount -+ out = "default" -+} -+ -+get_allowed_proc_mount(params) = out { -+ not valid_proc_mount(params.procMount) -+ out = "default" -+} -+ -+get_allowed_proc_mount(params) = out { -+ out = lower(params.procMount) -+} -+ -+valid_proc_mount(str) { -+ lower(str) == "default" -+} -+ -+valid_proc_mount(str) { -+ lower(str) == "unmasked" -+} -+ -+input_proc_mount_type_allowed(allowedProcMount, c) { -+ allowedProcMount == "default" -+ lower(c.securityContext.procMount) == "default" -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego -new file mode 100755 -index 0000000..cef0f21 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego -@@ -0,0 +1,58 @@ -+package accurics -+ -+####fixed the minimum set of allowed volumes, this may change as per the user#### -+ -+#rule for pod_security_policy -+{{.prefix}}{{.name}}{{.suffix}}[psp.id] { -+ psp := input.kubernetes_pod_security_policy[_] -+ secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] -+ volume_field := psp.config.spec.volumes[_] -+ not input_volume_type_allowed(volume_field, secure_volumes) -+} -+ -+#rule for pod -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] -+ volume_fields := {x | pod.config.spec.volumes[_][x]; x != "name"} -+ field := volume_fields[_] -+ not input_volume_type_allowed(field, secure_volumes) -+} -+ -+#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] -+ volume_fields := {x | kind.config.spec.template.spec.volumes[_][x]; x != "name"} -+ field := volume_fields[_] -+ not input_volume_type_allowed(field, secure_volumes) -+} -+ -+#rule for cron_job -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] -+ volume_fields := {x | cron_job.config.spec.jobTemplate.spec.template.spec.volumes[_][x]; x != "name"} -+ field := volume_fields[_] -+ not input_volume_type_allowed(field, secure_volumes) -+} -+ -+input_volume_type_allowed(field, secure_volumes) { -+ secure_volumes[_] == "*" -+} -+ -+input_volume_type_allowed(field, secure_volumes) { -+ field == secure_volumes[_] -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json -new file mode 100755 -index 0000000..3a5ba1b ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_cron_job", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.73", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json -new file mode 100755 -index 0000000..860d3a3 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_daemonset", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.74", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json -new file mode 100755 -index 0000000..a873dc5 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_deployment", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.75", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json -new file mode 100755 -index 0000000..497e12d ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_job", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.76", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json -new file mode 100755 -index 0000000..72f9f69 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_pod", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.77", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json -new file mode 100755 -index 0000000..2ec1d9e ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_replicaset", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.78", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json -new file mode 100755 -index 0000000..0becff4 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_replication_controller", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.79", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json -new file mode 100755 -index 0000000..6c54f99 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_stateful_set", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.80", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json -new file mode 100755 -index 0000000..8e8aebd ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_cron_job", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.81", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json -new file mode 100755 -index 0000000..9d06ffb ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_daemonset", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.82", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json -new file mode 100755 -index 0000000..0259e02 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_deployment", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.83", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json -new file mode 100755 -index 0000000..646842e ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_job", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.84", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json -new file mode 100755 -index 0000000..59f98d1 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_pod", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.85", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json -new file mode 100755 -index 0000000..32ae92f ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_replicaset", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.86", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json -new file mode 100755 -index 0000000..b82fb11 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_replication_controller", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.87", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json -new file mode 100755 -index 0000000..33f7d06 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerHasAllowedCapabilities", -+ "file": "containerHasAllowedCapabilities.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerHasAllowedCapabilities", -+ "prefix": "", -+ "resource_type": "kubernetes_stateful_set", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Pod has extra capabilities allowed", -+ "reference_id": "accurics.kubernetes.IAM.88", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego -new file mode 100644 -index 0000000..bfe42d7 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego -@@ -0,0 +1,119 @@ -+package accurics -+ -+# Checks if any extra capabilities are added -+{{.prefix}}{{.name}}{{.suffix}}[api.id] { -+ {{- template "spec" . }} -+ count(spec.allowedCapabilities) > 0 -+} -+ -+# Note, no TF-equivalent -+ -+{{- if eq .is_init true}} -+ -+{{.prefix}}{{.name}}{{.suffix}}[api.id] { -+ {{- template "initContainersSecurityContext" .}} -+ count(initContainersSecurityContext.capabilities.add) > 0 -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[api.id] { -+ {{- template "initContainersSecurityContextTF" .}} -+ count(initContainersSecurityContextTF.capabilities.add) > 0 -+} -+ -+{{- else }} -+ -+{{.prefix}}{{.name}}{{.suffix}}[api.id] { -+ {{- template "containersSecurityContext" .}} -+ count(containersSecurityContext.capabilities.add) > 0 -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[api.id] { -+ {{- template "containersSecurityContextTF" .}} -+ count(containersSecurityContextTF.capabilities.add) > 0 -+} -+ -+{{- end }} -+ -+ -+################################## -+### Template definitions below ### -+################################## -+{{- define "api" }} -+ api = input.{{.resource_type}}[_] -+{{- end}} -+ -+# resolves path to the spec key -+{{- define "spec" }} -+ {{- template "api" . }} -+ {{- if eq .resource_type "kubernetes_pod" }} -+ spec = api.config.spec -+ {{- else if eq .resource_type "kubernetes_pod_security_policy" }} -+ spec = api.config.spec -+ {{- else if eq .resource_type "kubernetes_cron_job" }} -+ spec = api.config.spec.jobTemplate.spec.template.spec -+ {{- else }} -+ spec = api.config.spec.template.spec -+ {{- end }} -+{{- end }} -+ -+# resolves path to the spec key for terraform-defined k8s resources -+{{- define "specTF" }} -+ {{- template "api" . }} -+ {{- if eq .resource_type "kubernetes_pod" }} -+ specTF = api.config.spec -+ {{- else if eq .resource_type "kubernetes_pod_security_policy" }} -+ specTF = api.config.spec -+ {{- else if eq .resource_type "kubernetes_cron_job" }} -+ specTF = api.config.spec.job_template.spec.template.spec -+ {{- else }} -+ specTF = api.config.spec.template.spec -+ {{- end }} -+{{- end }} -+ -+# resolves path to the containers list -+{{- define "containers" }} -+ {{- template "spec" . }} -+ containers = spec.containers[_] -+{{- end }} -+ -+# resolves path to the containers' security context -+{{- define "containersSecurityContext" }} -+ {{- template "containers" . }} -+ containersSecurityContext = containers.securityContext -+{{- end }} -+ -+# resolves path to the containers list for terraform-defined k8s resources -+{{- define "containersTF" }} -+ {{- template "specTF" . }} -+ containersTF = specTF.containers[_] -+{{- end }} -+ -+# resolves path to the containers' security context for terraform-defined k8s resources -+{{- define "containersSecurityContextTF" }} -+ {{- template "containersTF" . }} -+ containersSecurityContextTF = containersTF.security_context -+{{- end }} -+ -+# resolves path to the initContainers list -+{{- define "initContainers" }} -+ {{- template "spec" . }} -+ initContainers = spec.initContainers[_] -+{{- end }} -+ -+# resolves path to the initContainers' security context -+{{- define "initContainersSecurityContext" }} -+ {{- template "initContainers" . }} -+ initContainersSecurityContext = initContainers.securityContext -+{{- end }} -+ -+# resolves path to the initContainers list for terraform-defined k8s resources -+{{- define "initContainersTF" }} -+ {{- template "specTF" . }} -+ initContainersTF = specTF.init_containers[_] -+{{- end }} -+ -+# resolves path to the initContainers' security context for terraform-defined k8s resources -+{{- define "initContainersSecurityContextTF" }} -+ {{- template "initContainersTF" . }} -+ initContainersSecurityContextTF = initContainersTF.security_context -+{{- end }} -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego -new file mode 100755 -index 0000000..7be8687 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego -@@ -0,0 +1,108 @@ -+package accurics -+ -+#rule for pod security policy, will be valid for terraform pod_security_policy -+{{.prefix}}{{.name}}{{.suffix}}[psp.id] { -+ psp := input.kubernetes_pod_security_policy[_] -+ psp.config.metadata.annotations["apparmor.security.beta.kubernetes.io/defaultProfileName"] != "runtime/default" -+} -+ -+#rule for pod, covers containers -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.containers[_] -+ not input_apparmor_allowed(container.name, pod.config.metadata) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.initContainers[_] -+ not input_apparmor_allowed(container.name, pod.config.metadata) -+} -+ -+#terraform init_containers -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.init_containers[_] -+ not input_apparmor_allowed(container.name, pod.config.metadata) -+} -+ -+##rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.containers[_] -+ not input_apparmor_allowed(container.name, kind.config.spec.template.metadata) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.initContainers[_] -+ not input_apparmor_allowed(container.name, kind.config.spec.template.metadata) -+} -+ -+#terraform init_containers -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.init_containers[_] -+ not input_apparmor_allowed(container.name, kind.config.spec.template.metadata) -+} -+ -+#rule for cron_job, covers containers -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] -+ not input_apparmor_allowed(container.name, cron_job.config.spec.jobTemplate.spec.template.metadata) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] -+ not input_apparmor_allowed(container.name, cron_job.config.spec.jobTemplate.spec.template.metadata) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] -+ not input_apparmor_allowed(container.name, cron_job.config.spec.jobTemplate.spec.template.metadata) -+} -+ -+#function for all Kinds -+input_apparmor_allowed(containerName, metadata) { -+ metadata.annotations[key] == "runtime/default" -+ key == sprintf("container.apparmor.security.beta.kubernetes.io/%v", [containerName]) -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego -new file mode 100755 -index 0000000..248878b ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego -@@ -0,0 +1,33 @@ -+package accurics -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ pod.config.spec.automountServiceAccountToken == true -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_service_account[_] -+ pod.config.automountServiceAccountToken == true -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ pod.config.spec.template.spec.automountServiceAccountToken == true -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ pod.config.spec.jobTemplate.spec.template.spec.automountServiceAccountToken == true -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego -new file mode 100755 -index 0000000..d8d1ae4 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego -@@ -0,0 +1,69 @@ -+package accurics -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.containers[_] -+ container.securityContext.capabilities.add == "-SYS_ADMIN" -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ initcontainer := pod.config.spec.initContainers[_] -+ initcontainer.securityContext.capabilities.add == "-SYS_ADMIN" -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] -+ container.securityContext.capabilities.add == "-SYS_ADMIN" -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] -+ initcontainer.securityContext.capabilities.add == "-SYS_ADMIN" -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ containerCheck(pod.config.spec.template.spec) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ initContainerCheck(pod.config.spec.template.spec) -+} -+ -+initContainerCheck(spec) { -+ container := spec.initContainers[_] -+ container.securityContext.capabilities.add == "-SYS_ADMIN" -+} -+ -+containerCheck(spec) { -+ container := spec.containers[_] -+ container.securityContext.capabilities.add == "-SYS_ADMIN" -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego -new file mode 100755 -index 0000000..9920b38 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego -@@ -0,0 +1,74 @@ -+package accurics -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod_security_policy[_] -+ pod.config.spec.{{.attribute}} != [] -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.containers[_] -+ container.{{.attribute}} != [] -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ initcontainer := pod.config.spec.initContainers[_] -+ initcontainer.{{.attribute}} != [] -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] -+ container.{{.attribute}} != [] -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] -+ initcontainer.{{.attribute}} != [] -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ containerCheck(pod.config.spec.template.spec) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ initContainerCheck(pod.config.spec.template.spec) -+} -+ -+initContainerCheck(spec) { -+ container := spec.initContainers[_] -+ container.{{.attribute}} != [] -+} -+ -+containerCheck(spec) { -+ container := spec.containers[_] -+ container.{{.attribute}} != [] -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego -new file mode 100755 -index 0000000..07c315a ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego -@@ -0,0 +1,19 @@ -+package accurics -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod_kubeapi.id] { -+ pod_kubeapi := input.kubernetes_pod[_] -+ cmds := pod_kubeapi.config.spec.containers[_].command -+ {{.negation}} check(cmds) -+} -+ -+check(cmds) { -+ cmd := cmds[_] -+ startswith(cmd, "{{.argument}}") -+ {{.presence}} contains(cmd, "{{.param}}") -+ {{.optional}} -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ cmds := pod.config.spec.containers[_].imagePullPolicy != "Always" -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json -new file mode 100755 -index 0000000..56545ab ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_cron_job", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.105", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json -new file mode 100755 -index 0000000..4d97801 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_daemonset", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.106", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json -new file mode 100755 -index 0000000..ce27d4b ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_job", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.108", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json -new file mode 100755 -index 0000000..b740200 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_pod", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.109", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json -new file mode 100755 -index 0000000..19489a0 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_replicaset", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.110", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json -new file mode 100755 -index 0000000..d4a8f55 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_replication_controller", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.111", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json -new file mode 100755 -index 0000000..a18dc1c ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_stateful_set", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.112", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json -new file mode 100755 -index 0000000..d1cdd09 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_cron_job", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.113", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json -new file mode 100755 -index 0000000..64b9de3 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_daemonset", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.114", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json -new file mode 100755 -index 0000000..2f573f8 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_deployment", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.115", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json -new file mode 100755 -index 0000000..0f80098 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_job", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.116", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json -new file mode 100755 -index 0000000..9e420b8 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_pod", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.117", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json -new file mode 100755 -index 0000000..17d4116 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_replicaset", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.118", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json -new file mode 100755 -index 0000000..076a492 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_replication_controller", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.119", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json -new file mode 100755 -index 0000000..e5c922b ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerResourcesNotDefined", -+ "file": "containerResourcesNotDefined.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerResourcesNotDefined", -+ "prefix": "", -+ "resource_type": "kubernetes_stateful_set", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Container does not have resource limitations defined", -+ "reference_id": "accurics.kubernetes.IAM.120", -+ "category": "Identity and Access Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego -new file mode 100644 -index 0000000..b073034 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego -@@ -0,0 +1,111 @@ -+package accurics -+ -+{{- if eq .is_init true}} -+ -+{{.prefix}}{{.name}}{{.suffix}}[api.id] { -+ {{- template "initContainers" . }} -+ not initContainers.resources -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[api.id] { -+ {{- template "initContainersTF" . }} -+ not initContainersTF.resources -+} -+ -+{{- else }} -+ -+{{.prefix}}{{.name}}{{.suffix}}[api.id] { -+ {{- template "containers" . }} -+ not containers.resources -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[api.id] { -+ {{- template "containersTF" . }} -+ not containersTF.resources -+} -+ -+{{- end }} -+ -+ -+################################## -+### Template definitions below ### -+################################## -+{{- define "api" }} -+ api = input.{{.resource_type}}[_] -+{{- end}} -+ -+# resolves path to the spec key -+{{- define "spec" }} -+ {{- template "api" . }} -+ {{- if eq .resource_type "kubernetes_pod" }} -+ spec = api.config.spec -+ {{- else if eq .resource_type "kubernetes_pod_security_policy" }} -+ spec = api.config.spec -+ {{- else if eq .resource_type "kubernetes_cron_job" }} -+ spec = api.config.spec.jobTemplate.spec.template.spec -+ {{- else }} -+ spec = api.config.spec.template.spec -+ {{- end }} -+{{- end }} -+ -+# resolves path to the spec key for terraform-defined k8s resources -+{{- define "specTF" }} -+ {{- template "api" . }} -+ {{- if eq .resource_type "kubernetes_pod" }} -+ specTF = api.config.spec -+ {{- else if eq .resource_type "kubernetes_pod_security_policy" }} -+ specTF = api.config.spec -+ {{- else if eq .resource_type "kubernetes_cron_job" }} -+ specTF = api.config.spec.job_template.spec.template.spec -+ {{- else }} -+ specTF = api.config.spec.template.spec -+ {{- end }} -+{{- end }} -+ -+# resolves path to the containers list -+{{- define "containers" }} -+ {{- template "spec" . }} -+ containers = spec.containers[_] -+{{- end }} -+ -+# resolves path to the containers' security context -+{{- define "containersSecurityContext" }} -+ {{- template "containers" . }} -+ containersSecurityContext = containers.securityContext -+{{- end }} -+ -+# resolves path to the containers list for terraform-defined k8s resources -+{{- define "containersTF" }} -+ {{- template "specTF" . }} -+ containersTF = specTF.containers[_] -+{{- end }} -+ -+# resolves path to the containers' security context for terraform-defined k8s resources -+{{- define "containersSecurityContextTF" }} -+ {{- template "containersTF" . }} -+ containersSecurityContextTF = containersTF.security_context -+{{- end }} -+ -+# resolves path to the initContainers list -+{{- define "initContainers" }} -+ {{- template "spec" . }} -+ initContainers = spec.initContainers[_] -+{{- end }} -+ -+# resolves path to the initContainers' security context -+{{- define "initContainersSecurityContext" }} -+ {{- template "initContainers" . }} -+ initContainersSecurityContext = initContainers.securityContext -+{{- end }} -+ -+# resolves path to the initContainers list for terraform-defined k8s resources -+{{- define "initContainersTF" }} -+ {{- template "specTF" . }} -+ initContainersTF = specTF.init_containers[_] -+{{- end }} -+ -+# resolves path to the initContainers' security context for terraform-defined k8s resources -+{{- define "initContainersSecurityContextTF" }} -+ {{- template "initContainersTF" . }} -+ initContainersSecurityContextTF = initContainersTF.security_context -+{{- end }} -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json -new file mode 100755 -index 0000000..7962ef7 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_cron_job", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.57", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json -new file mode 100755 -index 0000000..f8f70ea ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_daemonset", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.58", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json -new file mode 100755 -index 0000000..739c621 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_deployment", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.59", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json -new file mode 100755 -index 0000000..f8a42a8 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_job", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.60", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json -new file mode 100755 -index 0000000..fc7091e ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_pod", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.61", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json -new file mode 100755 -index 0000000..3fc461f ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_replicaset", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.62", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json -new file mode 100755 -index 0000000..59662b3 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_replication_controller", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.63", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json -new file mode 100755 -index 0000000..13cb5dc ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": false, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_stateful_set", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.64", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json -new file mode 100755 -index 0000000..00ec55c ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_cron_job", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.65", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json -new file mode 100755 -index 0000000..0f4a3d5 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_daemonset", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.66", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json -new file mode 100755 -index 0000000..42cd868 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_deployment", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.67", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json -new file mode 100755 -index 0000000..d023923 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_job", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.68", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json -new file mode 100755 -index 0000000..c522de8 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_pod", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.69", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json -new file mode 100755 -index 0000000..45c9cb9 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_replicaset", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.70", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json -new file mode 100755 -index 0000000..4bbbdd2 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_replication_controller", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.71", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json -new file mode 100755 -index 0000000..88fc00e ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json -@@ -0,0 +1,16 @@ -+{ -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "file": "containerUsesSecretsInEnvironmentVar.rego", -+ "template_args": { -+ "is_init": true, -+ "name": "containerUsesSecretsInEnvironmentVar", -+ "prefix": "", -+ "resource_type": "kubernetes_stateful_set", -+ "suffix": "" -+ }, -+ "severity": "HIGH", -+ "description": "Container uses secrets in environment variables", -+ "reference_id": "accurics.kubernetes.EKM.72", -+ "category": "Encryption and Key Management", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego -new file mode 100644 -index 0000000..86c10e3 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego -@@ -0,0 +1,115 @@ -+package accurics -+ -+{{- if eq .is_init true}} -+ -+{{.prefix}}{{.name}}{{.suffix}}[api.id] { -+ {{- template "initContainers" .}} -+ envVars := initContainers.env[_] -+ envVars.valueFrom.secretKeyRef -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[api.id] { -+ {{- template "initContainersTF" .}} -+ envVars := initContainersTF.env[_] -+ envVars.valueFrom.secretKeyRef -+} -+ -+{{- else }} -+ -+{{.prefix}}{{.name}}{{.suffix}}[api.id] { -+ {{- template "containers" .}} -+ envVars := containers.env[_] -+ envVars.valueFrom.secretKeyRef -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[api.id] { -+ {{- template "containersTF" .}} -+ envVars := containersTF.env[_] -+ envVars.valueFrom.secretKeyRef -+} -+ -+{{- end }} -+ -+ -+################################## -+### Template definitions below ### -+################################## -+{{- define "api" }} -+ api = input.{{.resource_type}}[_] -+{{- end}} -+ -+# resolves path to the spec key -+{{- define "spec" }} -+ {{- template "api" . }} -+ {{- if eq .resource_type "kubernetes_pod" }} -+ spec = api.config.spec -+ {{- else if eq .resource_type "kubernetes_pod_security_policy" }} -+ spec = api.config.spec -+ {{- else if eq .resource_type "kubernetes_cron_job" }} -+ spec = api.config.spec.jobTemplate.spec.template.spec -+ {{- else }} -+ spec = api.config.spec.template.spec -+ {{- end }} -+{{- end }} -+ -+# resolves path to the spec key for terraform-defined k8s resources -+{{- define "specTF" }} -+ {{- template "api" . }} -+ {{- if eq .resource_type "kubernetes_pod" }} -+ specTF = api.config.spec -+ {{- else if eq .resource_type "kubernetes_pod_security_policy" }} -+ specTF = api.config.spec -+ {{- else if eq .resource_type "kubernetes_cron_job" }} -+ specTF = api.config.spec.job_template.spec.template.spec -+ {{- else }} -+ specTF = api.config.spec.template.spec -+ {{- end }} -+{{- end }} -+ -+# resolves path to the containers list -+{{- define "containers" }} -+ {{- template "spec" . }} -+ containers = spec.containers[_] -+{{- end }} -+ -+# resolves path to the containers' security context -+{{- define "containersSecurityContext" }} -+ {{- template "containers" . }} -+ containersSecurityContext = containers.securityContext -+{{- end }} -+ -+# resolves path to the containers list for terraform-defined k8s resources -+{{- define "containersTF" }} -+ {{- template "specTF" . }} -+ containersTF = specTF.containers[_] -+{{- end }} -+ -+# resolves path to the containers' security context for terraform-defined k8s resources -+{{- define "containersSecurityContextTF" }} -+ {{- template "containersTF" . }} -+ containersSecurityContextTF = containersTF.security_context -+{{- end }} -+ -+# resolves path to the initContainers list -+{{- define "initContainers" }} -+ {{- template "spec" . }} -+ initContainers = spec.initContainers[_] -+{{- end }} -+ -+# resolves path to the initContainers' security context -+{{- define "initContainersSecurityContext" }} -+ {{- template "initContainers" . }} -+ initContainersSecurityContext = initContainers.securityContext -+{{- end }} -+ -+# resolves path to the initContainers list for terraform-defined k8s resources -+{{- define "initContainersTF" }} -+ {{- template "specTF" . }} -+ initContainersTF = specTF.init_containers[_] -+{{- end }} -+ -+# resolves path to the initContainers' security context for terraform-defined k8s resources -+{{- define "initContainersSecurityContextTF" }} -+ {{- template "initContainersTF" . }} -+ initContainersSecurityContextTF = initContainersTF.security_context -+{{- end }} -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego -new file mode 100755 -index 0000000..6d6e8e6 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego -@@ -0,0 +1,102 @@ -+package accurics -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ pod.config.spec.securityContext.runAsUser < 1000 -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.containers[_] -+ container.securityContext.runAsUser < 1000 -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ initcontainer := pod.config.spec.initContainers[_] -+ initcontainer.securityContext.runAsUser < 1000 -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ pod.config.spec.jobTemplate.spec.template.spec.securityContext.runAsUser < 1000 -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] -+ container.securityContext.runAsUser < 1000 -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] -+ initcontainer.securityContext.runAsUser < 1000 -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod_security_policy[_] -+ ranges := pod.config.spec.runAsUser.ranges[_] -+ ranges.min < 1000 -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ pod.config.spec.template.spec.securityContext.runAsUser < 1000 -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ checkContainer(pod.config.spec.template.spec) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ checkInitContainer(pod.config.spec.template.spec) -+} -+ -+checkInitContainer(spec) { -+ containers := spec.initContainers[_] -+ containers.securityContext.runAsUser < 1000 -+} -+ -+checkContainer(spec) { -+ containers := spec.containers[_] -+ containers.securityContext.runAsUser < 1000 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego -new file mode 100755 -index 0000000..f918cc2 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego -@@ -0,0 +1,51 @@ -+### this pollicy depends on the parameters specified by the user/client. Here we are considering that no kernel level syscalls are allowed### -+package accurics -+ -+#rule for pod -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ forbiddenSysctls = ["kernel.*"] -+ sysctl := pod.config.spec.securityContext.sysctls[_].name -+ forbidden_sysctl(sysctl, forbiddenSysctls) -+} -+ -+##rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ forbiddenSysctls = ["kernel.*"] -+ sysctl := kind.config.spec.template.spec.securityContext.sysctls[_].name -+ forbidden_sysctl(sysctl, forbiddenSysctls) -+} -+ -+#rule for cron_job -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ forbiddenSysctls = ["kernel.*"] -+ sysctl := cron_job.config.spec.jobTemplate.spec.template.spec.securityContext.sysctls[_].name -+ forbidden_sysctl(sysctl, forbiddenSysctls) -+} -+ -+# if all syscalls are forbidden -+forbidden_sysctl(sysctl, arg) { -+ arg[_] == "*" -+} -+ -+forbidden_sysctl(sysctl, arg) { -+ arg[_] == sysctl -+} -+ -+forbidden_sysctl(sysctl, arg) { -+ startswith(sysctl, trim(arg[_], "*")) -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json -new file mode 100644 -index 0000000..10000a6 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json -@@ -0,0 +1,14 @@ -+{ -+ "name": "disAllowedVolumes", -+ "file": "disAllowedVolumes.rego", -+ "template_args": { -+ "name": "disAllowedVolumes", -+ "prefix": "", -+ "suffix": "" -+ }, -+ "severity": "MEDIUM", -+ "description": "Vulnerable to CVE-2020-8555 (affected version of kube-controller-manager: v1.18.0, v1.17.0 - v1.17.4, v1.16.0 - v1.16.8,< v1.15.11", -+ "reference_id": "AC-K8-DS-PO-M-0143", -+ "category": "Data Security", -+ "version": 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego -new file mode 100644 -index 0000000..706bbaf ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego -@@ -0,0 +1,52 @@ -+package accurics -+ -+#rule for pod_security_policy -+{{.prefix}}{{.name}}{{.suffix}}[psp.id] { -+ psp := input.kubernetes_pod_security_policy[_] -+ affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] -+ volume_type := psp.config.spec.volumes[_] -+ volNotAllowed(volume_type, affected_volumes) -+} -+ -+#rule for pod -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] -+ volume_types := {x | pod.config.spec.volumes[_][x]; x != "name"} -+ vol:= volume_types[_] -+ volNotAllowed(vol, affected_volumes) -+} -+ -+#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] -+ volume_types := {x | kind.config.spec.template.spec.volumes[_][x]; x != "name"} -+ vol:= volume_types[_] -+ volNotAllowed(vol, affected_volumes) -+} -+ -+#rule for cron_job -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] -+ volume_types := {x | cron_job.config.spec.jobTemplate.spec.template.spec.volumes[_][x]; x != "name"} -+ vol:= volume_types[_] -+ volNotAllowed(vol, affected_volumes) -+} -+ -+volNotAllowed(field, affected_volumes) { -+ field == affected_volumes[_] -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego -new file mode 100755 -index 0000000..890439e ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego -@@ -0,0 +1,35 @@ -+package accurics -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ vol := pod.config.spec.jobTemplate.spec.template.spec.volumes[_] -+ socketPathCheck(vol.hostPath.path) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ vol := pod.config.spec.volumes[_] -+ socketPathCheck(vol.hostPath.path) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ vol := pod.config.spec.template.spec.volumes[_] -+ socketPathCheck(vol.hostPath.path) -+} -+ -+socketPathCheck(attrib) { -+ contains(attrib, "/var/run/docker") -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego -new file mode 100755 -index 0000000..de75e61 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego -@@ -0,0 +1,196 @@ -+package accurics -+ -+#rule for pod, covers containers, initContainers, terraform, init_containers -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.containers[_] -+ checkForPodLatest(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.containers[_] -+ checkForPodNoTag(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.initContainers[_] -+ checkForPodLatest(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.initContainers[_] -+ checkForPodNoTag(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.init_containers[_] -+ checkForPodLatest(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.init_containers[_] -+ checkForPodNoTag(container) -+} -+ -+#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers, initContainers, terraform init_containers -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.containers[_] -+ checkForPodLatest(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.containers[_] -+ checkForPodNoTag(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.initContainers[_] -+ checkForPodLatest(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.init_containers[_] -+ checkForPodLatest(container) -+ } -+ -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.initContainers[_] -+ checkForPodNoTag(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.init_containers[_] -+ checkForPodNoTag(container) -+} -+ -+#rule for cron_job, covers containers, initContainers, terraform init_containers -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] -+ checkForPodLatest(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] -+ checkForPodNoTag(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] -+ checkForPodLatest(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] -+ checkForPodLatest(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] -+ checkForPodNoTag(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] -+ checkForPodNoTag(container) -+} -+ -+#check function for All KINDs -+checkForPodLatest(arg) { -+ img_split := split(arg.image, ":") -+ tag := img_split[count(img_split) - 1] -+ tag == "latest" -+} -+ -+checkForPodNoTag(argument) { -+ img_split := split(argument.image, ":") -+ count(img_split) == 1 -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego -new file mode 100755 -index 0000000..6cf92e6 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego -@@ -0,0 +1,105 @@ -+package accurics -+ -+#rule for pod, same will satisfy terraform pod, covers containers, initContainers, and terraform init_containers -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.containers[_] -+ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] -+ not all(satisfied) -+} -+ -+#rule for init containers -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.initContainers[_] -+ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] -+ not all(satisfied) -+} -+ -+#rule for terraform init_containers -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.init_containers[_] -+ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] -+ not all(satisfied) -+} -+ -+#rule for deployment, daemonset, job, replica_set, replication_controller, stateful_set covers containers, initContainers, terraform init_containers -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.containers[_] -+ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] -+ not all(satisfied) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.initContainers[_] -+ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] -+ not all(satisfied) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.init_containers[_] -+ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] -+ not all(satisfied) -+} -+ -+#rule for cron_job, covers containers, initContainers, terraform init_containers -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] -+ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] -+ not all(satisfied) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] -+ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] -+ not all(satisfied) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] -+ satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] -+ not all(satisfied) -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego -new file mode 100755 -index 0000000..d9746d1 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego -@@ -0,0 +1,6 @@ -+package accurics -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ pod.config.metadata.labels.app == "kubernetes-dashboard" -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego -new file mode 100755 -index 0000000..7ce3427 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego -@@ -0,0 +1,20 @@ -+package accurics -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_cron_job", "undefined"), -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_pod", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ pod.config.metadata.namespace == "default" -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego -new file mode 100755 -index 0000000..148651e ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego -@@ -0,0 +1,11 @@ -+package accurics -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ pod.config.spec.privileged == true -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod_security_policy[_] -+ pod.config.spec.privileged == true -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego -new file mode 100755 -index 0000000..19eaf1a ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego -@@ -0,0 +1,68 @@ -+#liveenessprobe and readinessprobe are not applicable for init containers. -+package accurics -+ -+#rule for pod -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.containers[_] -+ not container["{{.argument}}"] -+} -+ -+#rule for pod terraform -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.containers[_] -+ not container["{{.argumentTF}}"] -+} -+ -+#rule for deployment, daemonset, job, replica_Set, replication_controller, stateful_set -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.containers[_] -+ not container["{{.argument}}"] -+} -+ -+#rule for terraform deployment, daemonset, job, replica_Set, replication_controller, stateful_set -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.containers[_] -+ not container["{{.argumentTF}}"] -+} -+ -+#rule for cronjob -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] -+ not container["{{.argument}}"] -+} -+ -+#rule for terraform cronjob -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] -+ not container["{{.argumentTF}}"] -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego -new file mode 100755 -index 0000000..f4bd21a ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego -@@ -0,0 +1,153 @@ -+package accurics -+ -+#rule for pod, pod_security_policy covers containers -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_pod", "undefined"), -+ object.get(input, "kubernetes_pod_security_policy", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ not input_container_allowed(kind.config.metadata) -+} -+ -+#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ not input_container_allowed(kind.config.spec.template.metadata) -+} -+ -+#rule for cron_job -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ not input_container_allowed(cron_job.config.spec.jobTemplate.spec.template.metadata) -+} -+ -+input_container_allowed(metadata) { -+ metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"] == "runtime/default" -+} -+ -+input_container_allowed(metadata) { -+ metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"] == "docker/default" -+} -+ -+ ####Kubernetes v1.19 or later######## -+ -+#rule for pod covers containers and checks field seccompProfile at container security context which is found at spec.containers. -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.containers[_] -+ not check_seccomp(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.initContainers[_] -+ not check_seccomp(container) -+} -+ -+#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.containers[_] -+ not check_seccomp(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ container := kind.config.spec.template.spec.initContainers[_] -+ not check_seccomp(container) -+} -+ -+#rule for cron_job -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] -+ not check_seccomp(container) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] -+ not check_seccomp(container) -+} -+ -+##rule to check seccompProfile at PodSecurityContext which is found at PodSpec## -+ -+#rule for pod -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ not check_seccomp(pod.config.spec) -+} -+ -+#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers -+{{.prefix}}{{.name}}{{.suffix}}[kind.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ kind := item[_] -+ not check_seccomp(kind.config.spec.template.spec) -+} -+ -+#rule for cron_job -+{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { -+ cron_job := input.kubernetes_cron_job[_] -+ not check_seccomp(cron_job.config.spec.jobTemplate.spec.template.spec) -+} -+ -+#function for all Kinds and scenarios -+check_seccomp(container) { -+ container.securityContext.seccompProfile.type == "RuntimeDefault" -+} -+ -+check_seccomp(container) { -+ container.securityContext.seccompProfile.type == "DockerDefault" -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego -new file mode 100755 -index 0000000..780bb45 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego -@@ -0,0 +1,75 @@ -+package accurics -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ containers := pod.config.spec.containers[_] -+ env := containers.env[_] -+ env.valueFrom != [] -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ initcontainer := pod.config.spec.initContainers[_] -+ env := initcontainer.env[_] -+ env.valueFrom != [] -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ containers := pod.config.spec.jobTemplate.spec.template.spec.containers[_] -+ env := containers.env[_] -+ env.valueFrom != [] -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] -+ env := initcontainer.env[_] -+ env.valueFrom != [] -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ checkContainer(pod.config.spec.template.spec) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ checkInitContainer(pod.config.spec.template.spec) -+} -+ -+checkInitContainer(spec) { -+ containers := spec.initContainers[_] -+ env := containers.env[_] -+ env.valueFrom != [] -+} -+ -+checkContainer(spec) { -+ containers := spec.containers[_] -+ env := containers.env[_] -+ env.valueFrom != [] -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego -new file mode 100755 -index 0000000..f76c539 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego -@@ -0,0 +1,76 @@ -+package accurics -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ checkCorrectAttribute(pod.config.spec.jobTemplate.spec.template.spec) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ checkCorrectAttribute(pod.config.spec.template.spec) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ checkCorrectAttribute(pod.config.spec) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod_security_policy[_] -+ podSecurityCheck(pod.config.spec) -+} -+ -+checkCorrectAttribute(spec) { -+ container := spec.containers[_] -+ containerSecurityCheck(container) -+} -+ -+checkCorrectAttribute(spec) { -+ container := spec.initContainers[_] -+ containerSecurityCheck(container) -+} -+ -+checkCorrectAttribute(spec) { -+ secContext := spec.securityContext -+ podSecurityCheck(secContext) -+} -+ -+containerSecurityCheck(container) { -+ {{.not_allowed}} -+ container.{{.param1}}.{{.param}} == {{.value}} -+} -+ -+containerSecurityCheck(container) { -+ object.get(container, "{{.param1}}", "undefined") == "undefined" -+} -+ -+containerSecurityCheck(container) { -+ not container.{{.param1}}.{{.param}} -+} -+ -+containerSecurityCheck(container) { -+ {{.allowed}} -+ not container.{{.param1}}.{{.arg1}}.{{.arg2}} -+} -+ -+podSecurityCheck(secContext) { -+ {{.not_allowed}} -+ secContext.{{.param}} == {{.value}} -+} -+ -+podSecurityCheck(secContext) { -+ {{.not_allowed}} -+ object.get(secContext, "{{.param}}", "undefined") == "undefined" -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego -new file mode 100755 -index 0000000..5d24387 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego -@@ -0,0 +1,103 @@ -+package accurics -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ container := pod.config.spec.containers[_] -+ not container.securityContext -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ initcontainer := pod.config.spec.initContainers[_] -+ not initcontainer.securityContext -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ not pod.config.spec.securityContext -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] -+ not container.securityContext -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] -+ not initcontainer.securityContext -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ not pod.config.spec.jobTemplate.spec.template.spec.securityContext -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined"), -+ object.get(input, "kubernetes_cron_job", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ checkPod(pod) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined"), -+ object.get(input, "kubernetes_cron_job", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ checkInitContainer(pod.config.spec.template.spec) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined"), -+ object.get(input, "kubernetes_cron_job", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ checkContainer(pod.config.spec.template.spec) -+} -+ -+checkContainer(spec) { -+ containers := spec.containers[_] -+ not containers.securityContext -+} -+ -+checkInitContainer(spec) { -+ containers := spec.initContainers[_] -+ not containers.securityContext -+} -+ -+checkPod(pod) { -+ not pod.config.spec.template.spec.securityContext -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego -new file mode 100755 -index 0000000..5794487 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego -@@ -0,0 +1,36 @@ -+package accurics -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ attribute := pod.config.spec.jobTemplate.spec.template.spec -+ boolCheck(attribute) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ attribute := pod.config.spec -+ boolCheck(attribute) -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ attribute := pod.config.spec.template.spec -+ -+ boolCheck(attribute) -+} -+ -+boolCheck(attribute) { -+ attribute.{{.param}} == {{.value}} -+} -\ No newline at end of file -diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego -new file mode 100755 -index 0000000..f4b90f2 ---- /dev/null -+++ b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego -@@ -0,0 +1,35 @@ -+package accurics -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_pod[_] -+ pod.config.metadata.labels.app == "helm" -+ pod.config.metadata.labels.name == "tiller" -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ pod := input.kubernetes_cron_job[_] -+ pod.config.spec.jobTemplate.spec.template.metadata.labels.app == "helm" -+ pod.config.spec.jobTemplate.spec.template.metadata.labels.name == "tiller" -+} -+ -+{{.prefix}}{{.name}}{{.suffix}}[pod.id] { -+ item_list := [ -+ object.get(input, "kubernetes_daemonset", "undefined"), -+ object.get(input, "kubernetes_deployment", "undefined"), -+ object.get(input, "kubernetes_job", "undefined"), -+ object.get(input, "kubernetes_replica_set", "undefined"), -+ object.get(input, "kubernetes_replication_controller", "undefined"), -+ object.get(input, "kubernetes_stateful_set", "undefined") -+ ] -+ -+ item = item_list[_] -+ item != "undefined" -+ -+ pod := item[_] -+ checkPod(pod) -+} -+ -+checkPod(pod) { -+ pod.config.spec.template.metadata.labels.app == "helm" -+ pod.config.spec.template.metadata.labels.name == "tiller" -+} -\ No newline at end of file -diff --git a/pkg/http-server/routes.go b/pkg/http-server/routes.go -index 400e990..c7e86ae 100644 ---- a/pkg/http-server/routes.go -+++ b/pkg/http-server/routes.go -@@ -29,13 +29,19 @@ type Route struct { - - // Routes returns a slice of routes of API endpoints to be registered with - // http server --func (g *APIServer) Routes() []*Route { -- h := NewAPIHandler() -+func (g *APIServer) Routes(configFile string) []*Route { -+ h := NewAPIHandler(configFile) - routes := []*Route{ - {verb: "GET", path: "/health", fn: h.Health}, - {verb: "POST", path: versionedPath("/{iac}/{iacVersion}/{cloud}/local/file/scan"), fn: h.scanFile}, - {verb: "POST", path: versionedPath("/{iac}/{iacVersion}/{cloud}/remote/dir/scan"), fn: h.scanRemoteRepo}, -+ -+ // K8s Webhook Routes -+ {verb: "GET", path: "/k8s/webhooks/{apiKey}/logs", fn: h.getLogs}, -+ {verb: "GET", path: "/k8s/webhooks/{apiKey}/logs/{uid}", fn: h.getLogByUID}, -+ {verb: "POST", path: versionedPath("/k8s/webhooks/{apiKey}/scan"), fn: h.scanIncomingWebhook}, - } -+ - return routes - } - -diff --git a/pkg/http-server/routes_test.go b/pkg/http-server/routes_test.go -index 06d3dad..3e20588 100644 ---- a/pkg/http-server/routes_test.go -+++ b/pkg/http-server/routes_test.go -@@ -8,7 +8,7 @@ func TestRoutes(t *testing.T) { - t.Run("health route check", func(t *testing.T) { - var ( - server = NewAPIServer() -- got = server.Routes() -+ got = server.Routes("") - passed = false - ) - -diff --git a/pkg/http-server/server.go b/pkg/http-server/server.go -index 68529d8..275bafc 100644 ---- a/pkg/http-server/server.go -+++ b/pkg/http-server/server.go -@@ -17,7 +17,8 @@ - package httpserver - - // APIServer struct for http api server --type APIServer struct{} -+type APIServer struct{ -+} - - // NewAPIServer returns a new APIServer{} - func NewAPIServer() *APIServer { -diff --git a/pkg/http-server/start.go b/pkg/http-server/start.go -index 8075389..aa4c6bb 100644 ---- a/pkg/http-server/start.go -+++ b/pkg/http-server/start.go -@@ -28,19 +28,19 @@ import ( - ) - - // Start initializes api routes and starts http server --func Start() { -+func Start(configFile string, certFile string, privateKeyFile string) { - // create a new API server - server := NewAPIServer() - - // get all routes -- routes := server.Routes() -+ routes := server.Routes(configFile) - - // register routes and start the http server -- server.start(routes) -+ server.start(routes, certFile, privateKeyFile) - } - - // start http server --func (g *APIServer) start(routes []*Route) { -+func (g *APIServer) start(routes []*Route, certFile string, privateKeyFile string) { - - var ( - err error -@@ -56,19 +56,36 @@ func (g *APIServer) start(routes []*Route) { - router.Methods(v.verb).Path(v.path).HandlerFunc(v.fn) - } - -+ // Add a route for all static templates / assets. Currently used for the Webhook logs views -+ // go/terrascan/asset is the path where the assets files are located inside the docker container -+ router.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(http.Dir("/go/terrascan/assets")))) -+ -+ var port = GatewayDefaultPort -+ -+ // In case a certificate file is specified, we run the server with a different port -+ if certFile != "" { -+ port = TLSGatewayDefaultPort -+ } -+ - // start http server - server := &http.Server{ -- Addr: ":" + GatewayDefaultPort, -+ Addr: ":" + port, - Handler: router, - } - - go func() { -- err = server.ListenAndServe() -+ var err error -+ if certFile != "" { -+ // In case a certificate file is specified, the server support TLS -+ err = server.ListenAndServeTLS(certFile, privateKeyFile) -+ } else { -+ err = server.ListenAndServe() -+ } - if err != nil && err != http.ErrServerClosed { - logger.Fatal(err) - } - }() -- logger.Infof("http server listening at port %v", GatewayDefaultPort) -+ logger.Infof("http server listening at port %v", port) - - // Wait for interrupt signal to gracefully shutdown the server - quit := make(chan os.Signal, 1) -diff --git a/pkg/http-server/templates/index.html b/pkg/http-server/templates/index.html -new file mode 100644 -index 0000000..43b0bd9 ---- /dev/null -+++ b/pkg/http-server/templates/index.html -@@ -0,0 +1,34 @@ -+ -+ -+ -+ K8s Admission Review Logs -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ {{range .}} -+ -+ -+ -+ -+ -+ -+ {{end}} -+ -+
          TimeStatusRequestReasoning
          {{.CreatedAt}}{{.Status}}{{.Request}}{{.Reasoning}}
          -+ -+ -+ -+ -+ -diff --git a/pkg/http-server/templates/show.html b/pkg/http-server/templates/show.html -new file mode 100644 -index 0000000..7b36330 ---- /dev/null -+++ b/pkg/http-server/templates/show.html -@@ -0,0 +1,40 @@ -+ -+ -+ -+ K8s Admission Review Logs -+ -+ -+ -+ -+ -+
          -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+
          UID{{.UID}}
          Status{{.Status}}
          Request{{.Request}}
          Violations Summary{{.Violations}}
          Deniable Violations{{.DeniableViolations}}
          -+
          -+ -+ -+ -+ -+ -diff --git a/pkg/http-server/webhook-deny-rule-matcher.go b/pkg/http-server/webhook-deny-rule-matcher.go -new file mode 100644 -index 0000000..4becf0b ---- /dev/null -+++ b/pkg/http-server/webhook-deny-rule-matcher.go -@@ -0,0 +1,35 @@ -+package httpserver -+ -+import ( -+ "github.com/accurics/terrascan/pkg/config" -+ "github.com/accurics/terrascan/pkg/results" -+ "github.com/accurics/terrascan/pkg/utils" -+) -+ -+type webhookDenyRuleMatcher struct { -+} -+ -+// This class should check if one of the violations found is relevant for the specified K8s deny rules -+func (g *webhookDenyRuleMatcher) match(violation results.Violation, denyRules config.K8sDenyRules) bool { -+ if &denyRules == nil { -+ return false -+ } -+ -+ // Currently we support: -+ // 1. A minimum severity level -+ // 2. A category list -+ // In case one of the conditions is met, we return true. (We perform an OR between the rules) -+ if len(denyRules.DeniedSeverity) > 0 && utils.CheckSeverity(violation.Severity, denyRules.DeniedSeverity) { -+ return true -+ } -+ -+ if denyRules.Categories != nil { -+ for _, category := range denyRules.Categories { -+ if category == violation.Category { -+ return true -+ } -+ } -+ } -+ -+ return false -+} -diff --git a/pkg/http-server/webhook-deny-rule-matcher_test.go b/pkg/http-server/webhook-deny-rule-matcher_test.go -new file mode 100644 -index 0000000..8f0e165 ---- /dev/null -+++ b/pkg/http-server/webhook-deny-rule-matcher_test.go -@@ -0,0 +1,98 @@ -+package httpserver -+ -+import ( -+ "github.com/accurics/terrascan/pkg/config" -+ "github.com/accurics/terrascan/pkg/results" -+ "testing" -+) -+ -+func TestDenyRuleMatcher(t *testing.T) { -+ testMediumSeverity := "MEDIUM" -+ testCategory := "Identity and Access Management" -+ testRuleName := "My Amazing Rule" -+ -+ table := []struct { -+ name string -+ ruleSeverity string -+ ruleCategory string -+ ruleName string -+ k8sDenyRules config.K8sDenyRules -+ expectedResult bool -+ }{ -+ { -+ name: "no deny rules", -+ ruleSeverity: testMediumSeverity, -+ ruleCategory: testCategory, -+ ruleName: testRuleName, -+ expectedResult: false, -+ }, -+ { -+ name: "matched severity", -+ ruleSeverity: testMediumSeverity, -+ ruleCategory: testCategory, -+ ruleName: testRuleName, -+ k8sDenyRules: config.K8sDenyRules{ DeniedSeverity: testMediumSeverity }, -+ expectedResult: true, -+ }, -+ -+ { -+ name: "lower severity", -+ ruleSeverity: testMediumSeverity, -+ ruleCategory: testCategory, -+ ruleName: testRuleName, -+ k8sDenyRules: config.K8sDenyRules{ DeniedSeverity: "LOW" }, -+ expectedResult: true, -+ }, -+ { -+ name: "higher severity", -+ ruleSeverity: testMediumSeverity, -+ ruleCategory: testCategory, -+ ruleName: testRuleName, -+ k8sDenyRules: config.K8sDenyRules{ DeniedSeverity: "High" }, -+ expectedResult: false, -+ }, -+ { -+ name: "not matching category", -+ ruleSeverity: testMediumSeverity, -+ ruleCategory: testCategory, -+ ruleName: testRuleName, -+ k8sDenyRules: config.K8sDenyRules{ Categories: []string { "WRONG!" } }, -+ expectedResult: false, -+ }, -+ -+ { -+ name: "matching category", -+ ruleSeverity: testMediumSeverity, -+ ruleCategory: testCategory, -+ ruleName: testRuleName, -+ k8sDenyRules: config.K8sDenyRules{ Categories: []string { "WRONG!", testCategory } }, -+ expectedResult: true, -+ }, -+ { -+ name: "incorrect severity by matching category", -+ ruleSeverity: testMediumSeverity, -+ ruleCategory: testCategory, -+ ruleName: testRuleName, -+ k8sDenyRules: config.K8sDenyRules{ Categories: []string { "WRONG!", testCategory }, DeniedSeverity: "HIGH" }, -+ expectedResult: true, -+ }, -+ } -+ -+ var denyRuleMatcher = webhookDenyRuleMatcher{} -+ -+ for _, tt := range table { -+ t.Run(tt.name, func(t *testing.T) { -+ -+ violation := results.Violation{ -+ RuleName: tt.ruleName, -+ Severity: tt.ruleSeverity, -+ Category: tt.ruleCategory, -+ } -+ -+ result := denyRuleMatcher.match(violation, tt.k8sDenyRules) -+ if result != tt.expectedResult { -+ t.Errorf("Expected: %v, Got: %v", tt.expectedResult, result) -+ } -+ }) -+ } -+} -diff --git a/pkg/http-server/webhook-scan-logger.go b/pkg/http-server/webhook-scan-logger.go -new file mode 100644 -index 0000000..cbec83b ---- /dev/null -+++ b/pkg/http-server/webhook-scan-logger.go -@@ -0,0 +1,197 @@ -+package httpserver -+ -+import ( -+ "database/sql" -+ "go.uber.org/zap" -+ "os" -+ "time" -+) -+ -+type WebhookScanLogger struct { -+ test bool -+} -+ -+type webhookScanLog struct { -+ UID string -+ Request string -+ Allowed bool -+ ViolationsSummary string -+ DeniableViolations string -+ CreatedAt time.Time -+} -+ -+// The file name where the DB is stored. Currently we use an SQLite DB -+var dbFileName = "k8s-admission-review-logs.db" -+ -+func (g *WebhookScanLogger) log(webhookScanLog webhookScanLog) error { -+ // Insert a new Log record to the DB -+ -+ db, err := g.getDbHandler() -+ if err != nil { -+ return err -+ } -+ defer db.Close() -+ -+ insertLogSQL := `INSERT INTO logs(uid, request, allowed, violations_summary, deniable_violations, created_at) -+ VALUES (?, ?, ?, ?, ?, ?)` -+ -+ statement, err := db.Prepare(insertLogSQL) -+ if err != nil { -+ zap.S().Errorf("failed preparing SQL statement. error: '%v'", err) -+ return err -+ } -+ _, err = statement.Exec(webhookScanLog.UID, -+ webhookScanLog.Request, -+ webhookScanLog.Allowed, -+ webhookScanLog.ViolationsSummary, -+ webhookScanLog.DeniableViolations, -+ webhookScanLog.CreatedAt) -+ if err != nil { -+ zap.S().Errorf("failed to insert a new log. error: '%v'", err) -+ return err -+ } -+ -+ return nil -+} -+ -+func (g *WebhookScanLogger) fetchLogs() ([]webhookScanLog, error) { -+ // Fetch the entire logs in the DB, ordered by created_at DESC (the most updated will be at the top) -+ -+ db, err := g.getDbHandler() -+ if err != nil { -+ return nil, err -+ } -+ defer db.Close() -+ -+ row, err := db.Query("SELECT * FROM logs ORDER BY created_at DESC") -+ if err != nil { -+ zap.S().Errorf("failed query logs table. error: '%v'", err) -+ return nil, err -+ } -+ -+ var result []webhookScanLog -+ defer row.Close() -+ for row.Next() { -+ var id int -+ var uid string -+ var request string -+ var allowed bool -+ var violationsSummary string -+ var deniableViolations string -+ var createdAt time.Time -+ row.Scan(&id, &uid, &request, &allowed, &violationsSummary, &deniableViolations, &createdAt) -+ -+ result = append(result, webhookScanLog { -+ UID: uid, -+ Request: request, -+ Allowed: allowed, -+ ViolationsSummary: violationsSummary, -+ DeniableViolations: deniableViolations, -+ CreatedAt: createdAt, -+ }) -+ } -+ -+ return result, nil -+} -+ -+func (g *WebhookScanLogger) fetchLogById(logUID string) (*webhookScanLog, error) { -+ // Fetch a specific log by its request UID -+ -+ db, err := g.getDbHandler() -+ if err != nil { -+ return nil, err -+ } -+ defer db.Close() -+ -+ row, err := db.Query("SELECT * FROM logs WHERE uid=?", logUID) -+ if err != nil { -+ zap.S().Errorf("failed query logs table. error: '%v'", err) -+ return nil, err -+ } -+ defer row.Close() -+ -+ for row.Next() { -+ var id int -+ var uid string -+ var request string -+ var allowed bool -+ var violationsSummary string -+ var deniableViolations string -+ var createdAt time.Time -+ row.Scan(&id, &uid, &request, &allowed, &violationsSummary, &deniableViolations, &createdAt) -+ -+ return &webhookScanLog { -+ UID: uid, -+ Request: request, -+ Allowed: allowed, -+ ViolationsSummary: violationsSummary, -+ DeniableViolations: deniableViolations, -+ CreatedAt: createdAt, -+ }, nil -+ } -+ -+ return &webhookScanLog{}, nil -+} -+ -+func (g *WebhookScanLogger) initDBIfNeeded() error { -+ // Check where the SQL file exists. If it does do nothing. Otherwise, create the DB file and the Logs table. -+ if _, err := os.Stat(g.dbFilePath()); os.IsNotExist(err) { -+ file, err := os.Create(g.dbFilePath()) -+ if err != nil { -+ zap.S().Errorf("failed create db file. error: '%v'", err) -+ return err -+ } -+ file.Close() -+ -+ db, err := sql.Open("sqlite3", g.dbFilePath()) -+ if err != nil { -+ zap.S().Errorf("failed to open sql file. error: '%v'", err) -+ return err -+ } -+ defer db.Close() -+ -+ createLogsTableSQL := `CREATE TABLE logs ( -+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, -+ "uid" TEXT UNIQUE, -+ "request" TEXT, -+ "allowed" INTEGER, -+ "violations_summary" TEXT, -+ "deniable_violations" TEXT, -+ "created_at" DATETIME -+ );` -+ statement, err := db.Prepare(createLogsTableSQL) -+ if err != nil { -+ zap.S().Errorf("failed to create logs table. error: '%v'", err) -+ return err -+ } -+ statement.Exec() -+ } -+ -+ return nil -+} -+ -+func (g*WebhookScanLogger) getDbHandler() (*sql.DB, error) { -+ g.initDBIfNeeded() -+ -+ db, err := sql.Open("sqlite3", g.dbFilePath()) -+ if err != nil { -+ zap.S().Errorf("failed to open sql file. error: '%v'", err) -+ } -+ -+ return db, err -+} -+ -+ -+func (g *WebhookScanLogger) dbFilePath() string { -+ if g.test { -+ return "./" + dbFileName -+ } else { -+ // This is where the DB file should be located in the container (It is going to be saved in the host machine volume) -+ return "/data/k8s-admission-review-logs.db" -+ } -+} -+ -+// Used for Tests only - clear the DB file after the tests are done -+func (g *WebhookScanLogger) clearDbFilePath() { -+ os.Remove(g.dbFilePath()) -+} -diff --git a/pkg/http-server/webhook-scan-logger_test.go b/pkg/http-server/webhook-scan-logger_test.go -new file mode 100644 -index 0000000..518c0ae ---- /dev/null -+++ b/pkg/http-server/webhook-scan-logger_test.go -@@ -0,0 +1,77 @@ -+package httpserver -+ -+import ( -+ "testing" -+ "time" -+) -+func TestLogs(t *testing.T) { -+ t.Run("Log a new webhook scan", func(t *testing.T) { -+ var logger = WebhookScanLogger{ -+ test: true, -+ } -+ defer logger.clearDbFilePath() -+ -+ fetchedLogs, err := logger.fetchLogs() -+ if len(fetchedLogs) > 0 { -+ t.Errorf("At the beginning no logs should exist. Got: '%v'", len(fetchedLogs)) -+ } -+ -+ if err != nil { -+ t.Errorf("Got error") -+ } -+ -+ var log = webhookScanLog{ -+ UID: "myUID", -+ Request: "MyRequest", -+ CreatedAt: time.Now(), -+ Allowed: true, -+ DeniableViolations: "MyViolations", -+ ViolationsSummary: "ViolationsSummary", -+ } -+ -+ logger.log(log) -+ -+ fetchedLogs, err = logger.fetchLogs() -+ if err != nil { -+ t.Errorf("Got error") -+ } -+ -+ if len(fetchedLogs) != 1 { -+ t.Errorf("A new log should be returned. Got: '%v' logs", len(fetchedLogs)) -+ } -+ -+ myFetchLog, err := logger.fetchLogById(log.UID) -+ if err != nil { -+ t.Errorf("Got error") -+ } -+ -+ if len(myFetchLog.UID) < 1 { -+ t.Errorf("Log with ID: '%v' is not returned by fetchLogById", log.UID) -+ } -+ -+ if myFetchLog.UID != log.UID { -+ t.Errorf("Wrong UID. Expected '%v', Got: '%v'", log.UID, myFetchLog.UID) -+ -+ } -+ -+ if myFetchLog.Allowed != log.Allowed { -+ t.Errorf("Wrong Allowed. Expected '%v', Got: '%v'", log.Allowed, myFetchLog.Allowed) -+ } -+ -+ if myFetchLog.ViolationsSummary != log.ViolationsSummary { -+ t.Errorf("Wrong ViolationsSummary. Expected '%v', Got: '%v'", log.ViolationsSummary, myFetchLog.ViolationsSummary) -+ } -+ -+ if myFetchLog.Request != log.Request { -+ t.Errorf("Wrong Request. Expected '%v', Got: '%v'", log.Request, myFetchLog.Request) -+ } -+ -+ if myFetchLog.DeniableViolations != log.DeniableViolations { -+ t.Errorf("Wrong DeniableViolations. Expected '%v', Got: '%v'", log.DeniableViolations, myFetchLog.DeniableViolations) -+ } -+ -+ if myFetchLog.CreatedAt.Unix() != log.CreatedAt.Unix() { -+ t.Errorf("Wrong CreatedAt. Expected '%v', Got: '%v'", log.CreatedAt, myFetchLog.CreatedAt) -+ } -+ }) -+} -diff --git a/pkg/http-server/webhook-scan-logs.go b/pkg/http-server/webhook-scan-logs.go -new file mode 100644 -index 0000000..f99e80c ---- /dev/null -+++ b/pkg/http-server/webhook-scan-logs.go -@@ -0,0 +1,242 @@ -+/* -+ Copyright (C) 2020 Accurics, Inc. -+ -+ Licensed under the Apache License, Version 2.0 (the "License"); -+ you may not use this file except in compliance with the License. -+ You may obtain a copy of the License at -+ -+ http://www.apache.org/licenses/LICENSE-2.0 -+ -+ Unless required by applicable law or agreed to in writing, software -+ distributed under the License is distributed on an "AS IS" BASIS, -+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+ See the License for the specific language governing permissions and -+ limitations under the License. -+*/ -+ -+package httpserver -+ -+import ( -+ "encoding/json" -+ "fmt" -+ "github.com/accurics/terrascan/pkg/results" -+ "github.com/gorilla/mux" -+ _ "github.com/mattn/go-sqlite3" -+ "go.uber.org/zap" -+ "html/template" -+ "net/http" -+ "time" -+) -+ -+type webhookDisplayedViolation struct { -+ RuleName string `json:"rule_name"` -+ Category string `json:"category"` -+ Description string `json:"description"` -+ Severity string `json:"severity"` -+} -+ -+type webhookDisplayedReview struct { -+ Request webhookDisplayedRequest `json:"request"` -+} -+ -+type webhookDisplayedRequest struct { -+ Operation string `json:"operation"` -+ Object map[string]interface{} `json:"object"` -+} -+ -+type webhookDisplayedIndexScanLog struct { -+ CreatedAt time.Time -+ LogUrl string -+ Status string -+ Request string -+ Reasoning string -+} -+ -+type webhookDisplayedShowLog struct { -+ CreatedAt time.Time -+ UID string -+ Status string -+ Request string -+ Violations string -+ DeniableViolations string -+} -+ -+func (g *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) { -+ // Return an HTML page including all the logs history -+ -+ params := mux.Vars(r) -+ -+ apiKey := params["apiKey"] -+ if !g.validateAuthorization(apiKey, w) { -+ return -+ } -+ -+ logger := WebhookScanLogger{ -+ test: g.test, -+ } -+ -+ // The templates are saved in the docker in this location -+ t, _ := template.ParseFiles("/go/terrascan/index.html") -+ -+ logs, err := logger.fetchLogs() -+ if err != nil { -+ errMsg := fmt.Sprintf("error reading logs from DB: '%v'", err) -+ zap.S().Error(errMsg) -+ apiErrorResponse(w, errMsg, http.StatusInternalServerError) -+ return -+ } -+ -+ var logsData []webhookDisplayedIndexScanLog -+ for _, log := range logs { -+ logsData = append(logsData, webhookDisplayedIndexScanLog{ -+ CreatedAt: log.CreatedAt, -+ Status: g.getLogStatus(log), -+ LogUrl: g.getLogPath(r.Host, apiKey, log.UID), -+ Reasoning: g.getLogReasoning(log), -+ Request: g.getLogRequest(log), -+ }) -+ } -+ -+ t.Execute(w, logsData) -+} -+ -+func (g *APIHandler) getLogByUID(w http.ResponseWriter, r *http.Request) { -+ // Return an HTML page including the selected log -+ -+ params := mux.Vars(r) -+ -+ if !g.validateAuthorization(params["apiKey"], w) { -+ return -+ } -+ -+ var uid = params["uid"] -+ if len(uid) < 1 { -+ apiErrorResponse(w, "Log UID is missing", http.StatusBadRequest) -+ return -+ } -+ -+ logger := WebhookScanLogger{ -+ test: g.test, -+ } -+ -+ log, err := logger.fetchLogById(uid) -+ if err != nil { -+ errMsg := fmt.Sprintf("error reading logs from DB: '%v'", err) -+ zap.S().Error(errMsg) -+ apiErrorResponse(w, errMsg, http.StatusInternalServerError) -+ return -+ } -+ -+ if len(log.UID) < 1 { -+ apiErrorResponse(w, "Log is not found", http.StatusNotFound) -+ return -+ } -+ -+ displayedScanLog := webhookDisplayedShowLog{ -+ UID: log.UID, -+ CreatedAt: log.CreatedAt, -+ Status: g.getLogStatus(*log), -+ Request: log.Request, -+ Violations: log.ViolationsSummary, -+ DeniableViolations: log.DeniableViolations, -+ } -+ -+ t, _ := template.ParseFiles("/go/terrascan/show.html") -+ -+ t.Execute(w, displayedScanLog) -+} -+ -+func (g *APIHandler) getLogPath(host string, apiKey string, logUID string) string { -+ // Use this as the link to show the a specific log -+ return fmt.Sprintf("https://%v/k8s/webhooks/%v/logs/%v", host, apiKey, logUID) -+} -+ -+func (g *APIHandler)getLogStatus(log webhookScanLog) string { -+ // Calculate a log status: -+ // 1. !Allowed -> Rejected -+ // 2. Allowed -> if there are violations -> Allowed with Warnings. Otherwise -> Allowed -+ if !log.Allowed { -+ return "Rejected" -+ } -+ -+ var violationStore results.ViolationStore -+ err := json.Unmarshal([]byte(log.ViolationsSummary), &violationStore) -+ if err != nil { -+ zap.S().Errorf("Failed to ..") -+ } -+ -+ if len(violationStore.Violations) > 0 { -+ return "Allowed with warnings" -+ } -+ -+ return "Allowed" -+} -+ -+func (g *APIHandler)getLogReasoning(log webhookScanLog) string { -+ // Reasoning: -+ // - In case the request is denied (rejected), show the violations that cause the denial. -+ // - Otherwise, if there are violations, show the full violations list was found -+ // - Otherwise, reasoning is empty -+ -+ var violations []*results.Violation -+ if !log.Allowed { -+ err := json.Unmarshal([]byte(log.DeniableViolations), &violations) -+ if err != nil { -+ zap.S().Errorf("Failed to deserialize deniable violations summary. Error: %v", err.Error()) -+ return "" -+ } -+ } else { -+ var violationStore results.ViolationStore -+ err := json.Unmarshal([]byte(log.ViolationsSummary), &violationStore) -+ if err != nil { -+ zap.S().Errorf("Failed to deserialize violations summary. Error: %v", err.Error()) -+ return "" -+ } -+ -+ violations = violationStore.Violations -+ } -+ -+ var result []webhookDisplayedViolation -+ -+ if len(violations) < 1 { -+ return "" -+ } else { -+ for _, v := range violations { -+ result = append(result, webhookDisplayedViolation{ -+ Category: v.Category, -+ Description: v.Description, -+ RuleName: v.RuleName, -+ Severity: v.Severity, -+ }) -+ } -+ -+ encoded, err := json.Marshal(result) -+ if err != nil { -+ zap.S().Errorf("failed to serialize violations: '%v'", err) -+ return "" -+ } -+ -+ return string(encoded) -+ } -+ -+ return "" -+} -+ -+func (g *APIHandler)getLogRequest(log webhookScanLog) string { -+ var review webhookDisplayedReview -+ -+ err := json.Unmarshal([]byte(log.Request), &review) -+ -+ if err != nil { -+ zap.S().Errorf("Failed to deserialize request. Error: %v", err.Error()) -+ return "{}" -+ } -+ -+ result, err := json.Marshal(review.Request) -+ if err != nil { -+ zap.S().Errorf("Failed to serialize request. Error: %v", err.Error()) -+ return "{}" -+ } -+ -+ return string(result) -+} -diff --git a/pkg/http-server/webhook-scan.go b/pkg/http-server/webhook-scan.go -new file mode 100644 -index 0000000..bca2cc9 ---- /dev/null -+++ b/pkg/http-server/webhook-scan.go -@@ -0,0 +1,297 @@ -+/* -+ Copyright (C) 2020 Accurics, Inc. -+ -+ Licensed under the Apache License, Version 2.0 (the "License"); -+ you may not use this file except in compliance with the License. -+ You may obtain a copy of the License at -+ -+ http://www.apache.org/licenses/LICENSE-2.0 -+ -+ Unless required by applicable law or agreed to in writing, software -+ distributed under the License is distributed on an "AS IS" BASIS, -+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+ See the License for the specific language governing permissions and -+ limitations under the License. -+*/ -+ -+package httpserver -+ -+import ( -+ "encoding/json" -+ "fmt" -+ "github.com/accurics/terrascan/pkg/config" -+ "github.com/accurics/terrascan/pkg/results" -+ "github.com/accurics/terrascan/pkg/runtime" -+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -+ runtimeK8s "k8s.io/apimachinery/pkg/runtime" -+ -+ "k8s.io/apimachinery/pkg/runtime/serializer" -+ -+ "github.com/gorilla/mux" -+ "go.uber.org/zap" -+ "io/ioutil" -+ v1 "k8s.io/api/admission/v1" -+ "net/http" -+ "os" -+ "time" -+) -+ -+func (g *APIHandler) scanIncomingWebhook(w http.ResponseWriter, r *http.Request) { -+ currentTime := time.Now() -+ -+ params := mux.Vars(r) -+ apiKey := params["apiKey"] -+ -+ // Validate if authorized (API key is specified and matched the server one (saved in an environment variable) -+ if !g.validateAuthorization(apiKey, w) { -+ return -+ } -+ -+ // Read the request into byte array -+ bytesRequestAdmissionReview, err := ioutil.ReadAll(r.Body) -+ if err != nil { -+ msg := fmt.Sprintf("Failed to read admission review: '%v'", err) -+ apiErrorResponse(w, msg, http.StatusBadRequest) -+ return -+ } -+ -+ zap.S().Debugf("scanning configuration webhook request: %+v", string(bytesRequestAdmissionReview)) -+ -+ // Unmarshal the byte array into a v1.AdmissionReview object -+ requestedAdmissionReview, err := g.deserializeAdmissionReviewRequest(bytesRequestAdmissionReview) -+ if err != nil { -+ apiErrorResponse(w, err.Error(), http.StatusBadRequest) -+ return -+ } -+ -+ // In case the object is nil => an operation of DELETE happened, just return 'allow' since there is nothing to check -+ if len(requestedAdmissionReview.Request.Object.Raw) < 1 { -+ g.sendResponseAdmissionReview(w, *requestedAdmissionReview, true, nil, "") -+ return -+ } -+ -+ // Save the object into a temp file for the policy engines -+ tempFile, err := g.writeObjectToTempFile(requestedAdmissionReview.Request.Object.Raw) -+ defer os.Remove(tempFile.Name()) -+ if err != nil { -+ apiErrorResponse(w, err.Error(), http.StatusInternalServerError) -+ return -+ } -+ -+ // Run the policy engines -+ output, err := g.executeEngines(*tempFile) -+ if err != nil { -+ apiErrorResponse(w, err.Error(), http.StatusInternalServerError) -+ return -+ } -+ -+ // Calculate if there are anydeny violations -+ denyViolations, err := g.getDenyViolations(*output) -+ allowed := len(denyViolations) < 1 -+ logPath := g.getLogPath(r.Host, apiKey, string(requestedAdmissionReview.Request.UID)) -+ -+ // Log the request in the DB -+ err = g.logWebhook(*output, string(requestedAdmissionReview.Request.UID), bytesRequestAdmissionReview, denyViolations,currentTime, allowed) -+ if err != nil { -+ apiErrorResponse(w, err.Error(), http.StatusInternalServerError) -+ return -+ } -+ -+ // Send the correct response according to the result -+ g.sendResponseAdmissionReview(w, *requestedAdmissionReview, allowed, output, logPath) -+} -+ -+func (g *APIHandler) validateAuthorization(apiKey string, w http.ResponseWriter) bool { -+ if len(apiKey) < 1 { -+ msg := "apiKey is missing" -+ zap.S().Error(msg) -+ apiErrorResponse(w, msg, http.StatusBadRequest) -+ return false -+ } -+ -+ savedApiKey := os.Getenv("K8S_WEBHOOK_API_KEY") -+ if len(savedApiKey) < 1 { -+ msg := "K8S_WEBHOOK_API_KEY environment variable MUST be declared" -+ zap.S().Error(msg) -+ apiErrorResponse(w, msg, http.StatusInternalServerError) -+ return false -+ } -+ -+ if apiKey != savedApiKey { -+ msg := "Invalid apiKey" -+ zap.S().Error(msg) -+ apiErrorResponse(w, msg, http.StatusUnauthorized) -+ return false -+ } -+ -+ return true -+} -+ -+func (g *APIHandler) getDeniedViolations(violations results.ViolationStore, denyRules config.K8sDenyRules) []*results.Violation { -+ // Check whether one of the violations matches the deny violations configuration -+ -+ var denyViolations []*results.Violation -+ -+ denyRuleMatcher := webhookDenyRuleMatcher{} -+ -+ for _, violation := range violations.Violations { -+ if denyRuleMatcher.match(*violation, denyRules) { -+ denyViolations = append(denyViolations, violation) -+ } -+ } -+ -+ return denyViolations -+} -+ -+func (g *APIHandler) writeObjectToTempFile(objectBytes []byte) (*os.File, error) { -+ tempFile, err := ioutil.TempFile("", "terrascan-*.json") -+ if err != nil { -+ zap.S().Errorf("failed to create temp file: '%v'", err) -+ return nil, err -+ } -+ -+ zap.S().Debugf("created temp config file at '%s'", tempFile.Name()) -+ -+ _, err = tempFile.Write(objectBytes) -+ if err != nil { -+ zap.S().Errorf("failed to write object to temp file: '%v'", err) -+ return nil, err -+ } -+ -+ return tempFile, nil -+} -+ -+func (g *APIHandler) executeEngines(tempFile os.File) (*runtime.Output, error) { -+ var executor *runtime.Executor -+ var err error -+ if g.test { -+ executor, err = runtime.NewExecutor("k8s", "v1", []string { "k8s" }, -+ tempFile.Name(), "", g.configFile, []string{"./k8s_testdata/testpolicies"}, []string{}, []string{}, "") -+ } else { -+ executor, err = runtime.NewExecutor("k8s", "v1", []string { "k8s" }, -+ tempFile.Name(), "", g.configFile, []string{}, []string{}, []string{}, "") -+ } -+ -+ if err != nil { -+ zap.S().Errorf("failed to create runtime executer: '%v'", err) -+ return nil, err -+ } -+ -+ result, err := executor.Execute() -+ if err != nil { -+ zap.S().Error("failed to scan resource object. error: '%v'", err) -+ return nil, err -+ } -+ -+ return &result, nil -+} -+ -+func (g *APIHandler) getDenyViolations(output runtime.Output) ([]*results.Violation, error) { -+ // Calcualte the deny violations according to the configuration specified in the config file -+ configReader, err := config.NewTerrascanConfigReader(g.configFile) -+ if err != nil { -+ zap.S().Errorf("error loading config file: '%v'", err) -+ return nil, err -+ } -+ -+ denyViolations := g.getDeniedViolations(*output.Violations.ViolationStore, configReader.GetK8sDenyRules()) -+ -+ return denyViolations, nil -+} -+ -+func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, -+ requestedAdmissionReview v1.AdmissionReview, -+ allowed bool, -+ output *runtime.Output, -+ logPath string,) { -+ responseAdmissionReview := &v1.AdmissionReview{} -+ responseAdmissionReview.SetGroupVersionKind(requestedAdmissionReview.GroupVersionKind()) -+ -+ responseAdmissionReview.Response = &v1.AdmissionResponse{ -+ UID: requestedAdmissionReview.Request.UID, -+ Allowed: allowed, -+ } -+ -+ if output != nil { -+ // Means we ran the engines and we have results -+ if allowed { -+ if len(output.Violations.ViolationStore.Violations) > 0 { -+ // In case there are no denial violations, just return the log URL as a warning -+ responseAdmissionReview.Response.Warnings = []string{logPath} -+ } -+ } else { -+ // In case the request was denied, return 403 and the log URL as an error message -+ responseAdmissionReview.Response.Result = &metav1.Status{Message: logPath, Code: 403} -+ } -+ } -+ -+ respBytes, err := json.Marshal(responseAdmissionReview) -+ if err != nil { -+ msg := fmt.Sprintf("failed to serialize admission review response: %v", err) -+ zap.S().Error(msg) -+ apiErrorResponse(w, msg, http.StatusInternalServerError) -+ } -+ -+ zap.S().Debugf("Response result: %+v", string(respBytes)) -+ -+ apiResponse(w, string(respBytes), http.StatusOK) -+} -+ -+func (g *APIHandler) logWebhook(output runtime.Output, -+ uid string, -+ bytesAdmissionReview []byte, -+ denyViolations []*results.Violation, -+ currentTime time.Time, -+ allowed bool) error { -+ var deniedViolationsEncoded string -+ -+ if len(denyViolations) < 1 { -+ deniedViolationsEncoded = "" -+ } else { -+ d, _ := json.Marshal(denyViolations) -+ deniedViolationsEncoded = string(d) -+ } -+ -+ encodedViolationsSummary, _ := json.Marshal(output.Violations.ViolationStore) -+ -+ logger := WebhookScanLogger{ -+ test: g.test, -+ } -+ -+ err := logger.log(webhookScanLog { -+ UID: uid, -+ Request: string(bytesAdmissionReview), -+ Allowed: allowed, -+ DeniableViolations: deniedViolationsEncoded, -+ ViolationsSummary: string(encodedViolationsSummary), -+ CreatedAt: currentTime, -+ }) -+ if err != nil { -+ zap.S().Error("error logging scan result: '%v'", err) -+ return err -+ } -+ -+ return nil -+} -+ -+func (g *APIHandler) deserializeAdmissionReviewRequest(bytesAdmissionReview []byte) (*v1.AdmissionReview, error) { -+ var scheme = runtimeK8s.NewScheme() -+ v1.AddToScheme(scheme) -+ -+ var codecs = serializer.NewCodecFactory(scheme) -+ deserializer := codecs.UniversalDeserializer() -+ -+ obj, _, err := deserializer.Decode(bytesAdmissionReview, nil, nil) -+ if err != nil { -+ zap.S().Errorf("Request could not be decoded: %v", err) -+ return nil , err -+ } -+ -+ requestedAdmissionReview, ok := obj.(*v1.AdmissionReview) -+ if !ok { -+ zap.S().Errorf("Failed to deserialize request body to v1.AdmissionReview. Obj: %v", obj) -+ return nil , err -+ } -+ -+ return requestedAdmissionReview, nil -+} -diff --git a/pkg/http-server/webhook-scan_test.go b/pkg/http-server/webhook-scan_test.go -new file mode 100644 -index 0000000..7431359 ---- /dev/null -+++ b/pkg/http-server/webhook-scan_test.go -@@ -0,0 +1,244 @@ -+package httpserver -+ -+import ( -+ "bytes" -+ "encoding/json" -+ "fmt" -+ "github.com/gorilla/mux" -+ "io/ioutil" -+ v1 "k8s.io/api/admission/v1" -+ "net/http" -+ "net/http/httptest" -+ "os" -+ "testing" -+) -+ -+func TestUWebhooks(t *testing.T) { -+ testFilePath := "./k8s_testdata/testconfig.json" -+ testApiKey := "Test-API-KEY" -+ testEnvApiKey := "Test-API-KEY" -+ testConfigFile := "" -+ -+ table := []struct { -+ name string -+ contentRequestPath string -+ apiKey string -+ envApiKey string -+ wantStatus int -+ configFile string -+ warnings bool -+ allowed bool -+ statusCode int32 -+ statusMessage bool -+ }{ -+ { -+ name: "missing api key", -+ contentRequestPath: testFilePath, -+ apiKey: "", -+ envApiKey: testEnvApiKey, -+ wantStatus: http.StatusBadRequest, -+ configFile: testConfigFile, -+ }, -+ { -+ name: "missing K8S_WEBHOOK_API_KEY", -+ contentRequestPath: testFilePath, -+ apiKey: testApiKey, -+ envApiKey: "", -+ wantStatus: http.StatusInternalServerError, -+ configFile: testConfigFile, -+ }, -+ { -+ name: "invalid api key", -+ contentRequestPath: testFilePath, -+ apiKey: testApiKey, -+ envApiKey: "Invalid API KEY", -+ wantStatus: http.StatusUnauthorized, -+ configFile: testConfigFile, -+ }, -+ { -+ name: "invalid api key", -+ contentRequestPath: testFilePath, -+ apiKey: testApiKey, -+ envApiKey: "Invalid API KEY", -+ wantStatus: http.StatusUnauthorized, -+ configFile: testConfigFile, -+ }, -+ { -+ name: "invalid request json content", -+ contentRequestPath: "./k8s_testdata/invalid.json", -+ apiKey: testApiKey, -+ envApiKey: testEnvApiKey, -+ wantStatus: http.StatusBadRequest, -+ configFile: testConfigFile, -+ }, -+ { -+ name: "empty request json content", -+ contentRequestPath: "./k8s_testdata/empty.json", -+ apiKey: testApiKey, -+ envApiKey: testEnvApiKey, -+ wantStatus: http.StatusBadRequest, -+ configFile: testConfigFile, -+ }, -+ { -+ name: "request with empty object", -+ contentRequestPath: "./k8s_testdata/empty_object.json", -+ apiKey: testApiKey, -+ envApiKey: testEnvApiKey, -+ wantStatus: http.StatusOK, -+ configFile: testConfigFile, -+ warnings: false, -+ allowed: true, -+ }, -+ { -+ name: "safe request object", -+ contentRequestPath: testFilePath, -+ apiKey: testApiKey, -+ envApiKey: testEnvApiKey, -+ wantStatus: http.StatusOK, -+ configFile: testConfigFile, -+ warnings: false, -+ allowed: true, -+ }, -+ { -+ name: "risky request object without config", -+ contentRequestPath: "./k8s_testdata/risky_testconfig.json", -+ apiKey: testApiKey, -+ envApiKey: testEnvApiKey, -+ configFile: testConfigFile, -+ warnings: true, -+ allowed: true, -+ wantStatus: http.StatusOK, -+ }, -+ { -+ name: "risky request object with config that make it safe", -+ contentRequestPath: "./k8s_testdata/risky_testconfig.json", -+ apiKey: testApiKey, -+ envApiKey: testEnvApiKey, -+ configFile: "./k8s_testdata/config-specific-rule.toml", -+ warnings: false, -+ allowed: true, -+ wantStatus: http.StatusOK, -+ }, -+ { -+ name: "risky request object with config that just removes some of the violations", -+ contentRequestPath: "./k8s_testdata/risky_testconfig.json", -+ apiKey: testApiKey, -+ envApiKey: testEnvApiKey, -+ configFile: "./k8s_testdata/config-medium-severity.toml", -+ warnings: true, -+ allowed: true, -+ wantStatus: http.StatusOK, -+ }, -+ { -+ name: "risky request object with denied severity", -+ contentRequestPath: "./k8s_testdata/risky_testconfig.json", -+ apiKey: testApiKey, -+ envApiKey: testEnvApiKey, -+ configFile: "./k8s_testdata/config-deny-high.toml", -+ warnings: false, -+ allowed: false, -+ statusCode: 403, -+ statusMessage: true, -+ wantStatus: http.StatusOK, -+ }, -+ { -+ name: "risky request object with denied categories", -+ contentRequestPath: "./k8s_testdata/risky_testconfig.json", -+ apiKey: testApiKey, -+ envApiKey: testEnvApiKey, -+ configFile: "./k8s_testdata/config-deny-category.toml", -+ warnings: false, -+ allowed: false, -+ statusCode: 403, -+ statusMessage: true, -+ wantStatus: http.StatusOK, -+ }, -+ { -+ name: "risky request object with denied categories that does not exist", -+ contentRequestPath: "./k8s_testdata/risky_testconfig.json", -+ apiKey: testApiKey, -+ envApiKey: testEnvApiKey, -+ configFile: "./k8s_testdata/config-deny-non-existing-category.toml", -+ warnings: true, -+ allowed: true, -+ wantStatus: http.StatusOK, -+ }, -+ } -+ -+ for _, tt := range table { -+ t.Run(tt.name, func(t *testing.T) { -+ os.Setenv("K8S_WEBHOOK_API_KEY", tt.envApiKey) -+ -+ // test file to upload -+ path := tt.contentRequestPath -+ jsonFile, err := os.Open(path) -+ if err != nil { -+ t.Error(err) -+ return -+ } -+ defer jsonFile.Close() -+ logger := WebhookScanLogger{ -+ test: true, -+ } -+ defer logger.clearDbFilePath() -+ -+ byteValue, _ := ioutil.ReadAll(jsonFile) -+ -+ var admissionRequest v1.AdmissionReview -+ json.Unmarshal(byteValue, &admissionRequest) -+ -+ var url string -+ if len(tt.apiKey) > 0 { -+ url = fmt.Sprintf("/v1/k8s/webhooks/%v/scan", tt.apiKey) -+ } else { -+ url = fmt.Sprintf("/v1/k8s/webhooks/scan") -+ } -+ -+ req := httptest.NewRequest("POST", url, bytes.NewReader(byteValue)) -+ req.Header.Set("Content-Type", "application/json") -+ req = mux.SetURLVars(req, map[string]string{ -+ "apiKey": tt.apiKey, -+ }) -+ res := httptest.NewRecorder() -+ // new api handler -+ h := &APIHandler{test: true, configFile: tt.configFile } -+ h.scanIncomingWebhook(res, req) -+ -+ if res.Code != tt.wantStatus { -+ t.Errorf("incorrect status code, got: '%v', want: '%v', error: '%v'", res.Code, tt.wantStatus, res.Body) -+ } -+ -+ var response v1.AdmissionReview -+ _ = json.Unmarshal(res.Body.Bytes(), &response) -+ -+ if res.Code == http.StatusOK { -+ if tt.warnings && response.Response.Warnings == nil { -+ t.Errorf("Expected warnings but received None") -+ } -+ -+ if tt.allowed != response.Response.Allowed { -+ t.Errorf("Mismach in allowed. Got: %v, expected: %v", response.Response.Allowed, tt.allowed) -+ } -+ -+ if tt.statusCode != 0 && tt.statusCode != response.Response.Result.Code { -+ t.Errorf("Mismach Statud code Got: %v, expected: %v", response.Response.Result.Code, tt.statusCode) -+ } -+ -+ if tt.warnings || tt.statusMessage { -+ var logPath string -+ if tt.warnings { -+ logPath = response.Response.Warnings[0] -+ } else if tt.statusMessage { -+ logPath = response.Response.Result.Message -+ } -+ -+ expectedLogPath := fmt.Sprintf("https://%v/k8s/webhooks/%v/logs/705ab4f5-6393-11e8-b7cc-42010a800002", req.Host, tt.envApiKey) -+ -+ if logPath != expectedLogPath { -+ t.Errorf("Mismach Log path. Got: %v, expected: %v", logPath, expectedLogPath) -+ } -+ } -+ } -+ }) -+ } -+} -diff --git a/pkg/initialize/run.go b/pkg/initialize/run.go -index cec6f36..30472e0 100644 ---- a/pkg/initialize/run.go -+++ b/pkg/initialize/run.go -@@ -51,7 +51,7 @@ func Run(isScanCmd bool) error { - return err - } - -- zap.S().Debug("intialized successfully") -+ zap.S().Debug("initialized successfully") - return nil - } - -diff --git a/pkg/runtime/executor_test.go b/pkg/runtime/executor_test.go -index 79cb7d5..8cfb576 100644 ---- a/pkg/runtime/executor_test.go -+++ b/pkg/runtime/executor_test.go -@@ -1,12 +1,9 @@ - /* - Copyright (C) 2020 Accurics, Inc. -- - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at -- - http://www.apache.org/licenses/LICENSE-2.0 -- - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -diff --git a/pkg/runtime/testdata/scan-skip-rules-low-severity.toml b/pkg/runtime/testdata/scan-skip-rules-low-severity.toml -index 3f4d71b..5dadbf9 100644 ---- a/pkg/runtime/testdata/scan-skip-rules-low-severity.toml -+++ b/pkg/runtime/testdata/scan-skip-rules-low-severity.toml -@@ -11,4 +11,4 @@ - ] - - [severity] --level = "low" -\ No newline at end of file -+level = "low" --- -2.24.3 (Apple Git-128) - diff --git a/pkg/http-server/webhook-scan.go b/pkg/http-server/webhook-scan.go index e81d9dbd4..1a82cce98 100644 --- a/pkg/http-server/webhook-scan.go +++ b/pkg/http-server/webhook-scan.go @@ -72,10 +72,18 @@ func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) apiErrorResponse(w, err.Error(), http.StatusBadRequest) return } + if requestedAdmissionReview.Request == nil { + apiErrorResponse(w, "empty validating admission review request", http.StatusBadRequest) + return + } // process the admission review request output, allowed, denyViolations, err := validatingWebhook.ProcessWebhook(requestedAdmissionReview) if err != nil { + if err == admissionWebhook.ErrEmptyAdmissionReview { + g.sendResponseAdmissionReview(w, requestedAdmissionReview, true, output, "") + return + } apiErrorResponse(w, err.Error(), http.StatusInternalServerError) return } @@ -106,15 +114,17 @@ func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, Allowed: allowed, } - // Means we ran the engines and we have results - if allowed { - if len(output.Violations.ViolationStore.Violations) > 0 { - // In case there are no denial violations, just return the log URL as a warning - responseAdmissionReview.Response.Warnings = []string{logPath} + if output.Violations.ViolationStore != nil { + // Means we ran the engines and we have results + if allowed { + if len(output.Violations.ViolationStore.Violations) > 0 { + // In case there are no denial violations, just return the log URL as a warning + responseAdmissionReview.Response.Warnings = []string{logPath} + } + } else { + // In case the request was denied, return 403 and the log URL as an error message + responseAdmissionReview.Response.Result = &metav1.Status{Message: logPath, Code: 403} } - } else { - // In case the request was denied, return 403 and the log URL as an error message - responseAdmissionReview.Response.Result = &metav1.Status{Message: logPath, Code: 403} } respBytes, err := json.Marshal(responseAdmissionReview) @@ -125,7 +135,6 @@ func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, } zap.S().Debugf("Response result: %+v", string(respBytes)) - apiResponse(w, string(respBytes), http.StatusOK) } diff --git a/pkg/http-server/webhook-scan_test.go b/pkg/http-server/webhook-scan_test.go index 6e1d7eddb..503e9d85d 100644 --- a/pkg/http-server/webhook-scan_test.go +++ b/pkg/http-server/webhook-scan_test.go @@ -233,7 +233,7 @@ func TestUWebhooks(t *testing.T) { logPath = response.Response.Result.Message } - expectedLogPath := fmt.Sprintf("https://%v/k8s/webhooks/%v/logs/705ab4f5-6393-11e8-b7cc-42010a800002", req.Host, tt.envApiKey) + expectedLogPath := fmt.Sprintf("https://%v/k8s/webhooks/logs/705ab4f5-6393-11e8-b7cc-42010a800002", req.Host) if logPath != expectedLogPath { t.Errorf("Mismach Log path. Got: %v, expected: %v", logPath, expectedLogPath) diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index 0a84a972c..8ff17cdcd 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -43,9 +43,10 @@ func NewValidatingWebhook(configFile string) AdmissionWebhook { } var ( - ErrAPIKeyMissing = fmt.Errorf("apiKey is missing in validating admission webhook url") - ErrAPIKeyEnvNotSet = fmt.Errorf("variable K8S_WEBHOOK_API_KEY not set in terrascan server environment") - ErrUnauthorized = fmt.Errorf("invalid API key in validating admission webhook url") + ErrAPIKeyMissing = fmt.Errorf("apiKey is missing in validating admission webhook url") + ErrAPIKeyEnvNotSet = fmt.Errorf("variable K8S_WEBHOOK_API_KEY not set in terrascan server environment") + ErrUnauthorized = fmt.Errorf("invalid API key in validating admission webhook url") + ErrEmptyAdmissionReview = fmt.Errorf("empty admission review request") ) // Authorize checks if the incoming webhooks have valid apiKey @@ -102,8 +103,8 @@ func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) (o // In case the object is nil => an operation of DELETE happened, just return 'allow' since there is nothing to check if len(review.Request.Object.Raw) < 1 { - zap.S().Info("recieved empty validating admission review request", zap.Any("admission review object", review)) - return output, true, denyViolations, nil + zap.S().Info(ErrEmptyAdmissionReview, zap.Any("admission review object", review)) + return output, true, denyViolations, ErrEmptyAdmissionReview } // Save the object into a temp file for the policy engines diff --git a/pkg/http-server/webhook-deny-rule-matcher_test.go b/pkg/k8s/admission-webhook/webhook-deny-rule-matcher_test.go similarity index 80% rename from pkg/http-server/webhook-deny-rule-matcher_test.go rename to pkg/k8s/admission-webhook/webhook-deny-rule-matcher_test.go index dff5cb598..3b613b6a0 100644 --- a/pkg/http-server/webhook-deny-rule-matcher_test.go +++ b/pkg/k8s/admission-webhook/webhook-deny-rule-matcher_test.go @@ -1,4 +1,20 @@ -package httpserver +/* + Copyright (C) 2020 Accurics, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package admissionwebhook import ( "testing" From 4292f1e2a7374045793b44d43e759ab34304e6e6 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Mon, 15 Mar 2021 19:03:00 +0530 Subject: [PATCH 13/38] fixing linter issues --- pkg/config/types.go | 2 +- pkg/http-server/webhook-scan-logger.go | 8 +-- pkg/http-server/webhook-scan-logger_test.go | 4 +- pkg/http-server/webhook-scan-logs.go | 38 ++++++------ pkg/http-server/webhook-scan_test.go | 62 +++++++++---------- .../admission-webhook/validating-webhook.go | 13 +++- 6 files changed, 67 insertions(+), 60 deletions(-) diff --git a/pkg/config/types.go b/pkg/config/types.go index a1bc0190e..bea34ac17 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -63,7 +63,7 @@ type Rules struct { SkipRules []string `toml:"skip-rules,omitempty"` } -// K8s deny rules in the terrascan config file +// K8sDenyRules deny rules in the terrascan config file type K8sDenyRules struct { DeniedSeverity string `toml:"denied-severity,omitempty"` Categories []string `toml:"denied-categories,omitempty"` diff --git a/pkg/http-server/webhook-scan-logger.go b/pkg/http-server/webhook-scan-logger.go index 32d5d6bd3..02b14444c 100644 --- a/pkg/http-server/webhook-scan-logger.go +++ b/pkg/http-server/webhook-scan-logger.go @@ -8,6 +8,7 @@ import ( "go.uber.org/zap" ) +// WebhookScanLogger handles the logic to push scan logs to db type WebhookScanLogger struct { test bool } @@ -95,7 +96,7 @@ func (g *WebhookScanLogger) fetchLogs() ([]webhookScanLog, error) { return result, nil } -func (g *WebhookScanLogger) fetchLogById(logUID string) (*webhookScanLog, error) { +func (g *WebhookScanLogger) fetchLogByID(logUID string) (*webhookScanLog, error) { // Fetch a specific log by its request UID db, err := g.getDbHandler() @@ -185,10 +186,9 @@ func (g *WebhookScanLogger) getDbHandler() (*sql.DB, error) { func (g *WebhookScanLogger) dbFilePath() string { if g.test { return "./" + dbFileName - } else { - // This is where the DB file should be located in the container (It is going to be saved in the host machine volume) - return "/data/k8s-admission-review-logs.db" } + // This is where the DB file should be located in the container (It is going to be saved in the host machine volume) + return "/data/k8s-admission-review-logs.db" } // Used for Tests only - clear the DB file after the tests are done diff --git a/pkg/http-server/webhook-scan-logger_test.go b/pkg/http-server/webhook-scan-logger_test.go index 570fc8d84..adba7f537 100644 --- a/pkg/http-server/webhook-scan-logger_test.go +++ b/pkg/http-server/webhook-scan-logger_test.go @@ -41,13 +41,13 @@ func TestLogs(t *testing.T) { t.Errorf("A new log should be returned. Got: '%v' logs", len(fetchedLogs)) } - myFetchLog, err := logger.fetchLogById(log.UID) + myFetchLog, err := logger.fetchLogByID(log.UID) if err != nil { t.Errorf("Got error") } if len(myFetchLog.UID) < 1 { - t.Errorf("Log with ID: '%v' is not returned by fetchLogById", log.UID) + t.Errorf("Log with ID: '%v' is not returned by fetchLogByID", log.UID) } if myFetchLog.UID != log.UID { diff --git a/pkg/http-server/webhook-scan-logs.go b/pkg/http-server/webhook-scan-logs.go index 07bfb3484..1fe27072b 100644 --- a/pkg/http-server/webhook-scan-logs.go +++ b/pkg/http-server/webhook-scan-logs.go @@ -25,6 +25,7 @@ import ( "github.com/accurics/terrascan/pkg/results" "github.com/gorilla/mux" + // importing sqlite driver _ "github.com/mattn/go-sqlite3" "go.uber.org/zap" ) @@ -47,7 +48,7 @@ type webhookDisplayedRequest struct { type webhookDisplayedIndexScanLog struct { CreatedAt time.Time - LogUrl string + LogURL string Status string Request string Reasoning string @@ -85,7 +86,7 @@ func (g *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) { logsData = append(logsData, webhookDisplayedIndexScanLog{ CreatedAt: log.CreatedAt, Status: g.getLogStatus(log), - LogUrl: g.getLogPath(r.Host, log.UID), + LogURL: g.getLogPath(r.Host, log.UID), Reasoning: g.getLogReasoning(log), Request: g.getLogRequest(log), }) @@ -109,7 +110,7 @@ func (g *APIHandler) getLogByUID(w http.ResponseWriter, r *http.Request) { test: g.test, } - log, err := logger.fetchLogById(uid) + log, err := logger.fetchLogByID(uid) if err != nil { errMsg := fmt.Sprintf("error reading logs from DB: '%v'", err) zap.S().Error(errMsg) @@ -190,24 +191,23 @@ func (g *APIHandler) getLogReasoning(log webhookScanLog) string { if len(violations) < 1 { return "" - } else { - for _, v := range violations { - result = append(result, webhookDisplayedViolation{ - Category: v.Category, - Description: v.Description, - RuleName: v.RuleName, - Severity: v.Severity, - }) - } - - encoded, err := json.Marshal(result) - if err != nil { - zap.S().Errorf("failed to serialize violations: '%v'", err) - return "" - } + } + for _, v := range violations { + result = append(result, webhookDisplayedViolation{ + Category: v.Category, + Description: v.Description, + RuleName: v.RuleName, + Severity: v.Severity, + }) + } - return string(encoded) + encoded, err := json.Marshal(result) + if err != nil { + zap.S().Errorf("failed to serialize violations: '%v'", err) + return "" } + + return string(encoded) } func (g *APIHandler) getLogRequest(log webhookScanLog) string { diff --git a/pkg/http-server/webhook-scan_test.go b/pkg/http-server/webhook-scan_test.go index 503e9d85d..9f8ce93bb 100644 --- a/pkg/http-server/webhook-scan_test.go +++ b/pkg/http-server/webhook-scan_test.go @@ -16,15 +16,15 @@ import ( func TestUWebhooks(t *testing.T) { testFilePath := "./k8s_testdata/testconfig.json" - testApiKey := "Test-API-KEY" - testEnvApiKey := "Test-API-KEY" + testAPIKey := "Test-API-KEY" + testEnvAPIKey := "Test-API-KEY" testConfigFile := "" table := []struct { name string contentRequestPath string apiKey string - envApiKey string + envAPIKey string wantStatus int configFile string warnings bool @@ -36,55 +36,55 @@ func TestUWebhooks(t *testing.T) { name: "missing api key", contentRequestPath: testFilePath, apiKey: "", - envApiKey: testEnvApiKey, + envAPIKey: testEnvAPIKey, wantStatus: http.StatusBadRequest, configFile: testConfigFile, }, { name: "missing K8S_WEBHOOK_API_KEY", contentRequestPath: testFilePath, - apiKey: testApiKey, - envApiKey: "", + apiKey: testAPIKey, + envAPIKey: "", wantStatus: http.StatusInternalServerError, configFile: testConfigFile, }, { name: "invalid api key", contentRequestPath: testFilePath, - apiKey: testApiKey, - envApiKey: "Invalid API KEY", + apiKey: testAPIKey, + envAPIKey: "Invalid API KEY", wantStatus: http.StatusUnauthorized, configFile: testConfigFile, }, { name: "invalid api key", contentRequestPath: testFilePath, - apiKey: testApiKey, - envApiKey: "Invalid API KEY", + apiKey: testAPIKey, + envAPIKey: "Invalid API KEY", wantStatus: http.StatusUnauthorized, configFile: testConfigFile, }, { name: "invalid request json content", contentRequestPath: "./k8s_testdata/invalid.json", - apiKey: testApiKey, - envApiKey: testEnvApiKey, + apiKey: testAPIKey, + envAPIKey: testEnvAPIKey, wantStatus: http.StatusBadRequest, configFile: testConfigFile, }, { name: "empty request json content", contentRequestPath: "./k8s_testdata/empty.json", - apiKey: testApiKey, - envApiKey: testEnvApiKey, + apiKey: testAPIKey, + envAPIKey: testEnvAPIKey, wantStatus: http.StatusBadRequest, configFile: testConfigFile, }, { name: "request with empty object", contentRequestPath: "./k8s_testdata/empty_object.json", - apiKey: testApiKey, - envApiKey: testEnvApiKey, + apiKey: testAPIKey, + envAPIKey: testEnvAPIKey, wantStatus: http.StatusOK, configFile: testConfigFile, warnings: false, @@ -93,8 +93,8 @@ func TestUWebhooks(t *testing.T) { { name: "safe request object", contentRequestPath: testFilePath, - apiKey: testApiKey, - envApiKey: testEnvApiKey, + apiKey: testAPIKey, + envAPIKey: testEnvAPIKey, wantStatus: http.StatusOK, configFile: testConfigFile, warnings: false, @@ -103,8 +103,8 @@ func TestUWebhooks(t *testing.T) { { name: "risky request object without config", contentRequestPath: "./k8s_testdata/risky_testconfig.json", - apiKey: testApiKey, - envApiKey: testEnvApiKey, + apiKey: testAPIKey, + envAPIKey: testEnvAPIKey, configFile: testConfigFile, warnings: true, allowed: true, @@ -113,8 +113,8 @@ func TestUWebhooks(t *testing.T) { { name: "risky request object with config that make it safe", contentRequestPath: "./k8s_testdata/risky_testconfig.json", - apiKey: testApiKey, - envApiKey: testEnvApiKey, + apiKey: testAPIKey, + envAPIKey: testEnvAPIKey, configFile: "./k8s_testdata/config-specific-rule.toml", warnings: false, allowed: true, @@ -123,8 +123,8 @@ func TestUWebhooks(t *testing.T) { { name: "risky request object with config that just removes some of the violations", contentRequestPath: "./k8s_testdata/risky_testconfig.json", - apiKey: testApiKey, - envApiKey: testEnvApiKey, + apiKey: testAPIKey, + envAPIKey: testEnvAPIKey, configFile: "./k8s_testdata/config-medium-severity.toml", warnings: true, allowed: true, @@ -133,8 +133,8 @@ func TestUWebhooks(t *testing.T) { { name: "risky request object with denied severity", contentRequestPath: "./k8s_testdata/risky_testconfig.json", - apiKey: testApiKey, - envApiKey: testEnvApiKey, + apiKey: testAPIKey, + envAPIKey: testEnvAPIKey, configFile: "./k8s_testdata/config-deny-high.toml", warnings: false, allowed: false, @@ -145,8 +145,8 @@ func TestUWebhooks(t *testing.T) { { name: "risky request object with denied categories", contentRequestPath: "./k8s_testdata/risky_testconfig.json", - apiKey: testApiKey, - envApiKey: testEnvApiKey, + apiKey: testAPIKey, + envAPIKey: testEnvAPIKey, configFile: "./k8s_testdata/config-deny-category.toml", warnings: false, allowed: false, @@ -157,8 +157,8 @@ func TestUWebhooks(t *testing.T) { { name: "risky request object with denied categories that does not exist", contentRequestPath: "./k8s_testdata/risky_testconfig.json", - apiKey: testApiKey, - envApiKey: testEnvApiKey, + apiKey: testAPIKey, + envAPIKey: testEnvAPIKey, configFile: "./k8s_testdata/config-deny-non-existing-category.toml", warnings: true, allowed: true, @@ -168,7 +168,7 @@ func TestUWebhooks(t *testing.T) { for _, tt := range table { t.Run(tt.name, func(t *testing.T) { - os.Setenv("K8S_WEBHOOK_API_KEY", tt.envApiKey) + os.Setenv("K8S_WEBHOOK_API_KEY", tt.envAPIKey) // test file to upload path := tt.contentRequestPath diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index 8ff17cdcd..0ca0b3456 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -43,9 +43,16 @@ func NewValidatingWebhook(configFile string) AdmissionWebhook { } var ( - ErrAPIKeyMissing = fmt.Errorf("apiKey is missing in validating admission webhook url") - ErrAPIKeyEnvNotSet = fmt.Errorf("variable K8S_WEBHOOK_API_KEY not set in terrascan server environment") - ErrUnauthorized = fmt.Errorf("invalid API key in validating admission webhook url") + // ErrAPIKeyMissing indicates that API key is missing in webhook request + ErrAPIKeyMissing = fmt.Errorf("apiKey is missing in validating admission webhook url") + + // ErrAPIKeyEnvNotSet indicates K8S_WEBHOOK_API_KEY is not set in terrascan server env + ErrAPIKeyEnvNotSet = fmt.Errorf("variable K8S_WEBHOOK_API_KEY not set in terrascan server environment") + + // ErrUnauthorized means user is not authorized to make this call + ErrUnauthorized = fmt.Errorf("invalid API key in validating admission webhook url") + + // ErrEmptyAdmissionReview empty admission review request ErrEmptyAdmissionReview = fmt.Errorf("empty admission review request") ) From a2f211aa705f50f3f59165f394b53eb5b72a988a Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Mon, 15 Mar 2021 19:05:18 +0530 Subject: [PATCH 14/38] fixing go import issues --- pkg/http-server/webhook-scan-logs.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/http-server/webhook-scan-logs.go b/pkg/http-server/webhook-scan-logs.go index 1fe27072b..f6c0246c7 100644 --- a/pkg/http-server/webhook-scan-logs.go +++ b/pkg/http-server/webhook-scan-logs.go @@ -25,9 +25,10 @@ import ( "github.com/accurics/terrascan/pkg/results" "github.com/gorilla/mux" + "go.uber.org/zap" + // importing sqlite driver _ "github.com/mattn/go-sqlite3" - "go.uber.org/zap" ) type webhookDisplayedViolation struct { From ebcba457307f3c7b790daa7531444be5c3040b56 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Mon, 15 Mar 2021 19:17:38 +0530 Subject: [PATCH 15/38] fix staticheck errors --- pkg/http-server/webhook-scan_test.go | 2 +- pkg/k8s/admission-webhook/validating-webhook.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/http-server/webhook-scan_test.go b/pkg/http-server/webhook-scan_test.go index 9f8ce93bb..2be8c015c 100644 --- a/pkg/http-server/webhook-scan_test.go +++ b/pkg/http-server/webhook-scan_test.go @@ -192,7 +192,7 @@ func TestUWebhooks(t *testing.T) { if len(tt.apiKey) > 0 { url = fmt.Sprintf("/v1/k8s/webhooks/%v/scan", tt.apiKey) } else { - url = fmt.Sprintf("/v1/k8s/webhooks/scan") + url = "/v1/k8s/webhooks/scan" } req := httptest.NewRequest("POST", url, bytes.NewReader(byteValue)) diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index 0ca0b3456..f00ea2058 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -198,7 +198,8 @@ type webhookDenyRuleMatcher struct { // This class should check if one of the violations found is relevant for the specified K8s deny rules func (g *webhookDenyRuleMatcher) match(violation results.Violation, denyRules config.K8sDenyRules) bool { - if &denyRules == nil { + + if denyRules.DeniedSeverity == "" && len(denyRules.Categories) == 0 { return false } From e4871021e5e7d28630125744fdcf110b24c8e3f8 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Mon, 15 Mar 2021 20:17:11 +0530 Subject: [PATCH 16/38] removing redundant repos --- .../kubernetes_pod/AC-K8-CA-PO-H-0165.json | 21 -- .../kubernetes_pod/AC-K8-DS-PO-M-0176.json | 14 -- .../kubernetes_pod/AC-K8-DS-PO-M-0177.json | 14 -- .../kubernetes_pod/AC-K8-IA-PO-H-0106.json | 14 -- .../kubernetes_pod/AC-K8-IA-PO-H-0137.json | 14 -- .../kubernetes_pod/AC-K8-IA-PO-H-0138.json | 14 -- .../kubernetes_pod/AC-K8-IA-PO-H-0168.json | 21 -- .../kubernetes_pod/AC-K8-IA-PO-M-0105.json | 14 -- .../kubernetes_pod/AC-K8-IA-PO-M-0135.json | 14 -- .../kubernetes_pod/AC-K8-IA-PO-M-0139.json | 14 -- .../kubernetes_pod/AC-K8-IA-PO-M-0140.json | 21 -- .../kubernetes_pod/AC-K8-IA-PO-M-0141.json | 14 -- .../kubernetes_pod/AC-K8-IA-PO-M-0143.json | 22 -- .../kubernetes_pod/AC-K8-IA-PO-M-0162.json | 16 -- .../kubernetes_pod/AC-K8-IA-PS-M-0112.json | 15 -- .../kubernetes_pod/AC-K8-NS-PO-H-0117.json | 14 -- .../kubernetes_pod/AC-K8-NS-PO-H-0170.json | 14 -- .../kubernetes_pod/AC-K8-NS-PO-M-0122.json | 14 -- .../kubernetes_pod/AC-K8-NS-PO-M-0133.json | 14 -- .../kubernetes_pod/AC-K8-NS-PO-M-0163.json | 16 -- .../kubernetes_pod/AC-K8-NS-PO-M-0164.json | 16 -- .../kubernetes_pod/AC-K8-NS-PO-M-0171.json | 17 -- .../kubernetes_pod/AC-K8-NS-PO-M-0182.json | 14 -- .../kubernetes_pod/AC-K8-OE-PK-M-0034.json | 19 -- .../kubernetes_pod/AC-K8-OE-PK-M-0155.json | 21 -- .../kubernetes_pod/AC-K8-OE-PK-M-0156.json | 21 -- .../kubernetes_pod/AC-K8-OE-PK-M-0157.json | 21 -- .../kubernetes_pod/AC-K8-OE-PK-M-0158.json | 21 -- .../kubernetes_pod/AC-K8-OE-PO-L-0129.json | 16 -- .../kubernetes_pod/AC-K8-OE-PO-L-0130.json | 16 -- .../kubernetes_pod/AC-K8-OE-PO-L-0134.json | 14 -- .../kubernetes_pod/AC-K8-OE-PO-M-0166.json | 14 -- .../kubernetes_pod/allowedHostPath.rego | 107 ---------- .../kubernetes_pod/allowedProcMount.rego | 126 ----------- .../kubernetes_pod/allowedVolumes.rego | 58 ------ .../accurics.kubernetes.IAM.73.json | 16 -- .../accurics.kubernetes.IAM.74.json | 16 -- .../accurics.kubernetes.IAM.75.json | 16 -- .../accurics.kubernetes.IAM.76.json | 16 -- .../accurics.kubernetes.IAM.77.json | 16 -- .../accurics.kubernetes.IAM.78.json | 16 -- .../accurics.kubernetes.IAM.79.json | 16 -- .../accurics.kubernetes.IAM.80.json | 16 -- .../accurics.kubernetes.IAM.81.json | 16 -- .../accurics.kubernetes.IAM.82.json | 16 -- .../accurics.kubernetes.IAM.83.json | 16 -- .../accurics.kubernetes.IAM.84.json | 16 -- .../accurics.kubernetes.IAM.85.json | 16 -- .../accurics.kubernetes.IAM.86.json | 16 -- .../accurics.kubernetes.IAM.87.json | 16 -- .../accurics.kubernetes.IAM.88.json | 16 -- .../containerHasAllowedCapabilities.rego | 119 ----------- .../kubernetes_pod/appArmorProfile.rego | 108 ---------- .../kubernetes_pod/autoMountTokenEnabled.rego | 33 --- .../kubernetes_pod/capSysAdminUsed.rego | 69 ------ .../kubernetes_pod/capabilityUsed.rego | 74 ------- .../kubernetes_pod/commandCheck.rego | 19 -- .../accurics.kubernetes.IAM.105.json | 16 -- .../accurics.kubernetes.IAM.106.json | 16 -- .../accurics.kubernetes.IAM.108.json | 16 -- .../accurics.kubernetes.IAM.109.json | 16 -- .../accurics.kubernetes.IAM.110.json | 16 -- .../accurics.kubernetes.IAM.111.json | 16 -- .../accurics.kubernetes.IAM.112.json | 16 -- .../accurics.kubernetes.IAM.113.json | 16 -- .../accurics.kubernetes.IAM.114.json | 16 -- .../accurics.kubernetes.IAM.115.json | 16 -- .../accurics.kubernetes.IAM.116.json | 16 -- .../accurics.kubernetes.IAM.117.json | 16 -- .../accurics.kubernetes.IAM.118.json | 16 -- .../accurics.kubernetes.IAM.119.json | 16 -- .../accurics.kubernetes.IAM.120.json | 16 -- .../containerResourcesNotDefined.rego | 111 ---------- .../accurics.kubernetes.EKM.57.json | 16 -- .../accurics.kubernetes.EKM.58.json | 16 -- .../accurics.kubernetes.EKM.59.json | 16 -- .../accurics.kubernetes.EKM.60.json | 16 -- .../accurics.kubernetes.EKM.61.json | 16 -- .../accurics.kubernetes.EKM.62.json | 16 -- .../accurics.kubernetes.EKM.63.json | 16 -- .../accurics.kubernetes.EKM.64.json | 16 -- .../accurics.kubernetes.EKM.65.json | 16 -- .../accurics.kubernetes.EKM.66.json | 16 -- .../accurics.kubernetes.EKM.67.json | 16 -- .../accurics.kubernetes.EKM.68.json | 16 -- .../accurics.kubernetes.EKM.69.json | 16 -- .../accurics.kubernetes.EKM.70.json | 16 -- .../accurics.kubernetes.EKM.71.json | 16 -- .../accurics.kubernetes.EKM.72.json | 16 -- .../containerUsesSecretsInEnvironmentVar.rego | 115 ---------- .../kubernetes_pod/containersAsHighUID.rego | 102 --------- .../kubernetes_pod/disallowedSysCalls.rego | 51 ----- .../AC-K8-DS-PO-M-0143.json | 14 -- .../disallowed_volumes/disAllowedVolumes.rego | 52 ----- .../kubernetes_pod/dockerSockCheck.rego | 35 ---- .../kubernetes_pod/imageWithLatestTag.rego | 196 ------------------ .../kubernetes_pod/imageWithoutDigest.rego | 105 ---------- .../kubernetes_pod/kubeDashboardEnabled.rego | 6 - .../kubernetes_pod/otherNamespace.rego | 20 -- .../priviledgedContainersEnabled.rego | 11 - .../kubernetes_pod/probeCheck.rego | 68 ------ .../kubernetes_pod/secCompProfile.rego | 153 -------------- .../kubernetes_pod/secretsAsEnvVariables.rego | 75 ------- .../kubernetes_pod/securityContextCheck.rego | 76 ------- .../kubernetes_pod/securityContextUsed.rego | 103 --------- .../kubernetes_pod/specBoolCheck.rego | 36 ---- .../kubernetes_pod/tillerDeployed.rego | 35 ---- 107 files changed, 3353 deletions(-) delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json delete mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json delete mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json delete mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego delete mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json delete mode 100644 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego delete mode 100755 pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json deleted file mode 100755 index 24409fb1c..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-CA-PO-H-0165.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "privilegeEscalationCheck", - "file": "securityContextCheck.rego", - "template_args": { - "allowed": "false", - "arg1": "cpu", - "arg2": "limits", - "name": "privilegeEscalationCheck", - "not_allowed": "true", - "param": "allowPrivilegeEscalation", - "param1": "securityContext", - "prefix": "", - "suffix": "", - "value": "true" - }, - "severity": "HIGH", - "description": "Containers Should Not Run with AllowPrivilegeEscalation", - "reference_id": "AC-K8-CA-PO-H-0165", - "category": "Cloud Assets Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json deleted file mode 100755 index d0bff541c..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0176.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "kubeDashboardEnabled", - "file": "kubeDashboardEnabled.rego", - "template_args": { - "name": "kubeDashboardEnabled", - "prefix": "", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Ensure Kubernetes Dashboard Is Not Deployed", - "reference_id": "AC-K8-DS-PO-M-0176", - "category": "Data Security", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json deleted file mode 100755 index d8a40cc5e..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-DS-PO-M-0177.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "tillerDeployed", - "file": "tillerDeployed.rego", - "template_args": { - "name": "tillerDeployed", - "prefix": "", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Ensure That Tiller (Helm V2) Is Not Deployed", - "reference_id": "AC-K8-DS-PO-M-0177", - "category": "Data Security", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json deleted file mode 100755 index d07858d49..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0106.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "priviledgedContainersEnabled", - "file": "priviledgedContainersEnabled.rego", - "template_args": { - "name": "priviledgedContainersEnabled", - "prefix": "", - "suffix": "" - }, - "severity": "HIGH", - "description": "Minimize the admission of privileged containers", - "reference_id": "AC-K8-IA-PO-H-0106", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json deleted file mode 100755 index 71f74c306..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0137.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "disallowedSysCalls", - "file": "disallowedSysCalls.rego", - "template_args": { - "name": "disallowedSysCalls", - "prefix": "", - "suffix": "" - }, - "severity": "HIGH", - "description": "Allowing the pod to make system level calls provide access to host/node sensitive information", - "reference_id": "AC-K8-IA-PO-H-0137", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json deleted file mode 100755 index 16cfd6d99..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0138.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "allowedHostPath", - "file": "allowedHostPath.rego", - "template_args": { - "name": "allowedHostPath", - "prefix": "", - "suffix": "" - }, - "severity": "HIGH", - "description": "Allowing hostPaths to mount to Pod arise the probability of getting access to the node's filesystem", - "reference_id": "AC-K8-IA-PO-H-0138", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json deleted file mode 100755 index f7c9d5424..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-H-0168.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "runAsNonRootCheck", - "file": "securityContextCheck.rego", - "template_args": { - "allowed": "false", - "arg1": "cpu", - "arg2": "limits", - "name": "runAsNonRootCheck", - "not_allowed": "true", - "param": "runAsNonRoot", - "param1": "securityContext", - "prefix": "", - "suffix": "", - "value": "false" - }, - "severity": "HIGH", - "description": "Minimize Admission of Root Containers", - "reference_id": "AC-K8-IA-PO-H-0168", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json deleted file mode 100755 index 6f9be71f1..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0105.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "autoMountTokenEnabled", - "file": "autoMountTokenEnabled.rego", - "template_args": { - "name": "autoMountTokenEnabled", - "prefix": "", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Ensure that Service Account Tokens are only mounted where necessary", - "reference_id": "AC-K8-IA-PO-M-0105", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json deleted file mode 100755 index d7befdd86..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0135.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "appArmorProfile", - "file": "appArmorProfile.rego", - "template_args": { - "name": "appArmorProfile", - "prefix": "", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "AppArmor profile not set to default or custom profile will make the container vulnerable to kernel level threats", - "reference_id": "AC-K8-IA-PO-M-0135", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json deleted file mode 100755 index 5a22d3f4e..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0139.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "allowedProcMount", - "file": "allowedProcMount.rego", - "template_args": { - "name": "allowedProcMount", - "prefix": "", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Unmasking the procMount will allow more information than is necessary to the program running in the containers spawned by k8s", - "reference_id": "AC-K8-IA-PO-M-0139", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json deleted file mode 100755 index 10fad68ae..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0140.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "readOnlyFileSystem", - "file": "securityContextCheck.rego", - "template_args": { - "allowed": "false", - "arg1": "limits", - "arg2": "cpu", - "name": "readOnlyFileSystem", - "not_allowed": "true", - "param": "readOnlyRootFilesystem", - "param1": "securityContext", - "prefix": "", - "suffix": "", - "value": "false" - }, - "severity": "MEDIUM", - "description": "Container images with readOnlyRootFileSystem set as false mounts the container root file system with write permissions", - "reference_id": "AC-K8-IA-PO-M-0140", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json deleted file mode 100755 index 5293c73bf..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0141.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "secCompProfile", - "file": "secCompProfile.rego", - "template_args": { - "name": "secCompProfile", - "prefix": "", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Default seccomp profile not enabled will make the container to make non-essential system calls", - "reference_id": "AC-K8-IA-PO-M-0141", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json deleted file mode 100755 index 07843f8bf..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0143.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "allowedVolumes", - "file": "allowedVolumes.rego", - "template_args": { - "name": "allowedVolumes", - "prefix": "", - "secure_volumes": [ - "configMap", - "emptyDir", - "projected", - "secret", - "downwardAPI", - "persistentVolumeClaim" - ], - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Some volume types mount the host file system paths to the pod or container, thus increasing the chance of escaping the container to access the host", - "reference_id": "AC-K8-IA-PO-M-0143", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json deleted file mode 100755 index a98195db9..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PO-M-0162.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "falseHostPID", - "file": "specBoolCheck.rego", - "template_args": { - "name": "falseHostPID", - "param": "hostPID", - "prefix": "", - "suffix": "", - "value": "true" - }, - "severity": "MEDIUM", - "description": "Containers Should Not Share Host Process ID Namespace", - "reference_id": "AC-K8-IA-PO-M-0162", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json deleted file mode 100755 index 11f59e9a7..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-IA-PS-M-0112.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "netRawCapabilityUsed", - "file": "capabilityUsed.rego", - "template_args": { - "attribute": "requiredDropCapabilities", - "name": "netRawCapabilityUsed", - "prefix": "", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Minimize the admission of containers with the NET_RAW capability", - "reference_id": "AC-K8-IA-PS-M-0112", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json deleted file mode 100755 index 23c8d904d..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0117.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "secretsAsEnvVariables", - "file": "secretsAsEnvVariables.rego", - "template_args": { - "name": "secretsAsEnvVariables", - "prefix": "", - "suffix": "" - }, - "severity": "HIGH", - "description": "Prefer using secrets as files over secrets as environment variables", - "reference_id": "AC-K8-NS-PO-H-0117", - "category": "Network Security", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json deleted file mode 100755 index b211361a6..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-H-0170.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "capSysAdminUsed", - "file": "capSysAdminUsed.rego", - "template_args": { - "name": "capSysAdminUsed", - "prefix": "", - "suffix": "" - }, - "severity": "HIGH", - "description": "Do Not Use CAP_SYS_ADMIN Linux Capability", - "reference_id": "AC-K8-NS-PO-H-0170", - "category": "Network Security", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json deleted file mode 100755 index 43ba2432f..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0122.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "securityContextUsed", - "file": "securityContextUsed.rego", - "template_args": { - "name": "securityContextUsed", - "prefix": "", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Apply Security Context to Your Pods and Containers", - "reference_id": "AC-K8-NS-PO-M-0122", - "category": "Network Security", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json deleted file mode 100755 index 804a12ed4..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0133.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "imageWithoutDigest", - "file": "imageWithoutDigest.rego", - "template_args": { - "name": "imageWithoutDigest", - "prefix": "", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Image without digest affects the integrity principle of image security", - "reference_id": "AC-K8-NS-PO-M-0133", - "category": "Network Security", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json deleted file mode 100755 index e96b364da..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0163.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "falseHostIPC", - "file": "specBoolCheck.rego", - "template_args": { - "name": "falseHostIPC", - "param": "hostIPC", - "prefix": "", - "suffix": "", - "value": "true" - }, - "severity": "MEDIUM", - "description": "Containers Should Not Share Host IPC Namespace", - "reference_id": "AC-K8-NS-PO-M-0163", - "category": "Network Security", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json deleted file mode 100755 index 5c893ce0b..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0164.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "falseHostNetwork", - "file": "specBoolCheck.rego", - "template_args": { - "name": "falseHostNetwork", - "param": "hostNetwork", - "prefix": "", - "suffix": "", - "value": "true" - }, - "severity": "MEDIUM", - "description": "Containers Should Not Share the Host Network Namespace", - "reference_id": "AC-K8-NS-PO-M-0164", - "category": "Network Security", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json deleted file mode 100755 index df493d82c..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0171.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "dontConnectDockerSock", - "file": "dockerSockCheck.rego", - "template_args": { - "attrib": "spec.volumes[_].hostPath", - "name": "dontConnectDockerSock", - "param": "path", - "prefix": "", - "suffix": "", - "value": "/var/run/docker" - }, - "severity": "MEDIUM", - "description": "Restrict Mounting Docker Socket in a Container", - "reference_id": "AC-K8-NS-PO-M-0171", - "category": "Network Security", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json deleted file mode 100755 index 224310674..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-NS-PO-M-0182.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "containersAsHighUID", - "file": "containersAsHighUID.rego", - "template_args": { - "name": "containersAsHighUID", - "prefix": "", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Containers Should Run as a High UID to Avoid Host Conflict", - "reference_id": "AC-K8-NS-PO-M-0182", - "category": "Network Security", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json deleted file mode 100755 index 6340d311e..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0034.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "alwaysPullImages", - "file": "commandCheck.rego", - "template_args": { - "argument": "--enable-admission-plugins", - "name": "alwaysPullImages", - "negation": "", - "optional": "", - "param": "AlwaysPullImages", - "prefix": "", - "presence": "not", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "AlwaysPullImages plugin is not set", - "reference_id": "AC-K8-OE-PK-M-0034", - "category": "Operational Efficiency", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json deleted file mode 100755 index aebef8612..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0155.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "CpuRequestsCheck", - "file": "securityContextCheck.rego", - "template_args": { - "allowed": "true", - "arg1": "requests", - "arg2": "cpu", - "name": "CpuRequestsCheck", - "not_allowed": "false", - "param": "resources", - "param1": "resources", - "prefix": "", - "suffix": "", - "value": "false" - }, - "severity": "Medium", - "description": "CPU Request Not Set in config file.", - "reference_id": "AC-K8-OE-PK-M-0155", - "category": "Operational Efficiency", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json deleted file mode 100755 index c74835c6e..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0156.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "CpulimitsCheck", - "file": "securityContextCheck.rego", - "template_args": { - "allowed": "true", - "arg1": "limits", - "arg2": "cpu", - "name": "CpulimitsCheck", - "not_allowed": "false", - "param": "limits", - "param1": "resources", - "prefix": "", - "suffix": "", - "value": "false" - }, - "severity": "Medium", - "description": "CPU Limits Not Set in config file.", - "reference_id": "AC-K8-OE-PK-M-0156", - "category": "Operational Efficiency", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json deleted file mode 100755 index 691b58895..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0157.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "MemoryRequestsCheck", - "file": "securityContextCheck.rego", - "template_args": { - "allowed": "true", - "arg1": "requests", - "arg2": "memory", - "name": "MemoryRequestsCheck", - "not_allowed": "false", - "param": "resources", - "param1": "resources", - "prefix": "", - "suffix": "", - "value": "false" - }, - "severity": "Medium", - "description": "Memory Request Not Set in config file.", - "reference_id": "AC-K8-OE-PK-M-0157", - "category": "Operational Efficiency", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json deleted file mode 100755 index 7ab678c76..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PK-M-0158.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "MemorylimitsCheck", - "file": "securityContextCheck.rego", - "template_args": { - "allowed": "true", - "arg1": "limits", - "arg2": "memory", - "name": "MemorylimitsCheck", - "not_allowed": "false", - "param": "limits", - "param1": "resources", - "prefix": "", - "suffix": "", - "value": "false" - }, - "severity": "Medium", - "description": "Memory Limits Not Set in config file.", - "reference_id": "AC-K8-OE-PK-M-0158", - "category": "Operational Efficiency", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json deleted file mode 100755 index 9ce09380f..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0129.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "nolivenessProbe", - "file": "probeCheck.rego", - "template_args": { - "argument": "livenessProbe", - "argumentTF": "liveness_probe", - "name": "nolivenessProbe", - "prefix": "", - "suffix": "" - }, - "severity": "LOW", - "description": "No liveness probe will ensure there is no recovery in case of unexpected errors", - "reference_id": "AC-K8-OE-PO-L-0129", - "category": "Operational Efficiency", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json deleted file mode 100755 index a0e4058fd..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0130.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "noReadinessProbe", - "file": "probeCheck.rego", - "template_args": { - "argument": "readinessProbe", - "argumentTF": "readiness_probe", - "name": "noReadinessProbe", - "prefix": "", - "suffix": "" - }, - "severity": "LOW", - "description": "No readiness probe will affect automatic recovery in case of unexpected errors", - "reference_id": "AC-K8-OE-PO-L-0130", - "category": "Operational Efficiency", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json deleted file mode 100755 index 83eec4e4d..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-L-0134.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "imageWithLatestTag", - "file": "imageWithLatestTag.rego", - "template_args": { - "name": "imageWithLatestTag", - "prefix": "", - "suffix": "" - }, - "severity": "LOW", - "description": "No tag or container image with :Latest tag makes difficult to rollback and track", - "reference_id": "AC-K8-OE-PO-L-0134", - "category": "Operational Efficiency", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json deleted file mode 100755 index 6e0c8fd97..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/AC-K8-OE-PO-M-0166.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "otherNamespace", - "file": "otherNamespace.rego", - "template_args": { - "name": "otherNamespace", - "prefix": "", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Default Namespace Should Not be Used", - "reference_id": "AC-K8-OE-PO-M-0166", - "category": "Operational Efficiency", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego deleted file mode 100755 index e7d8463e1..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedHostPath.rego +++ /dev/null @@ -1,107 +0,0 @@ -### this policy depends on the parameters specified by the user/client. Here we are considering that no hostPath are allowed### -package accurics - -#rule for pod -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - vols := pod.config.spec.volumes[_] - parameters := {} - has_field(vols, "hostPath") - allowedPaths := get_allowed_paths(parameters) - input_hostpath_violation(allowedPaths, vols) -} - -#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - vols := kind.config.spec.template.spec.volumes[_] - #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } - parameters := {} - has_field(vols, "hostPath") - allowedPaths := get_allowed_paths(parameters) - input_hostpath_violation(allowedPaths, vols) -} - -#rule for cron_job -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - vols := cron_job.config.spec.jobTemplate.spec.template.spec.volumes[_] - #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } - parameters := {} - has_field(vols, "hostPath") - allowedPaths := get_allowed_paths(parameters) - input_hostpath_violation(allowedPaths, vols) -} - -#function for all KINDs -has_field(object, field) = true { - object[field] -} - -#now allowed paths are null, this function will run## -get_allowed_paths(params) = out { - not params.allowedHostPath == "undefined" - out = [] -} - -input_hostpath_violation(allowedPaths, volume) { - allowedPaths == [] -} - -### below functions are for violation when user has specified the hostPath, for testing uncomment the parameter array of objects at top#### - -get_allowed_paths(params) = out { - out = params.allowedHostPath -} - -input_hostpath_violation(allowedPaths, volume) { - not input_hostpath_allowed(allowedPaths, volume) -} - -input_hostpath_allowed(allowedPaths, volume) { - allowedHostPath := allowedPaths[_] - path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) - not allowedHostPath.readOnly == true -} - -input_hostpath_allowed(allowedPaths, volume) { - allowedHostPath := allowedPaths[_] - path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) - allowedHostPath.readOnly - not writeable_input_volume_mounts(volume.name) -} - -writeable_input_volume_mounts(volume_name) { - containers := input.kubernetes_pod[_].config.spec.containers[_] - mount := containers.volumeMounts[_] - mount.name == volume_name - not mount.readOnly -} - -path_matches(prefix, path) { - a := split(trim(prefix, "/"), "/") - b := split(trim(path, "/"), "/") - prefix_matches(a, b) -} - -prefix_matches(a, b) { - count(a) <= count(b) - not any_not_equal_upto(a, b, count(a)) -} - -any_not_equal_upto(a, b, n) { - a[i] != b[i] - i < n -} diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego deleted file mode 100755 index d8d6e62cc..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedProcMount.rego +++ /dev/null @@ -1,126 +0,0 @@ -package accurics - -#rule for pod_security_policy -{{.prefix}}{{.name}}{{.suffix}}[psp.id] { - psp := input.kubernetes_pod_security_policy[_] - psp.config.spec.allowProcMountTypes != "Default" -} - -#rule for pod_security_policy terraform -{{.prefix}}{{.name}}{{.suffix}}[psp.id] { - psp := input.kubernetes_pod_security_policy[_] - psp.config.spec.allow_proc_mount_types != "Default" -} - -#rule for pod -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - parameters := {} - container := pod.config.spec.containers[_] - container.securityContext.procMount - allowedProcMount := get_allowed_proc_mount(parameters) - not input_proc_mount_type_allowed(allowedProcMount, container) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - parameters := {} - container := pod.config.spec.initContainers[_] - container.securityContext.procMount - allowedProcMount := get_allowed_proc_mount(parameters) - not input_proc_mount_type_allowed(allowedProcMount, container) -} - -#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.containers[_] - #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } - parameters := {} - container.securityContext.procMount - allowedProcMount := get_allowed_proc_mount(parameters) - not input_proc_mount_type_allowed(allowedProcMount, container) -} - -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.initContainers[_] - #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } - parameters := {} - container.securityContext.procMount - allowedProcMount := get_allowed_proc_mount(parameters) - not input_proc_mount_type_allowed(allowedProcMount, container) -} - -#rule for cron_job -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] - #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } - parameters := {} - container.securityContext.procMount - allowedProcMount := get_allowed_proc_mount(parameters) - not input_proc_mount_type_allowed(allowedProcMount, container) -} - -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] - #parameters := { 'allowedHostPath' :[{ 'readOnly': true, 'pathPrefix': '/foo' }] } - parameters := {} - container.securityContext.procMount - allowedProcMount := get_allowed_proc_mount(parameters) - not input_proc_mount_type_allowed(allowedProcMount, container) -} - -###this will get satisfied as no parameters are provided, thus checking with the baseline configuration which is checking that the procmount is default#### -get_allowed_proc_mount(params) = out { - not params.procMount - out = "default" -} - -get_allowed_proc_mount(params) = out { - not valid_proc_mount(params.procMount) - out = "default" -} - -get_allowed_proc_mount(params) = out { - out = lower(params.procMount) -} - -valid_proc_mount(str) { - lower(str) == "default" -} - -valid_proc_mount(str) { - lower(str) == "unmasked" -} - -input_proc_mount_type_allowed(allowedProcMount, c) { - allowedProcMount == "default" - lower(c.securityContext.procMount) == "default" -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego deleted file mode 100755 index cef0f216d..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowedVolumes.rego +++ /dev/null @@ -1,58 +0,0 @@ -package accurics - -####fixed the minimum set of allowed volumes, this may change as per the user#### - -#rule for pod_security_policy -{{.prefix}}{{.name}}{{.suffix}}[psp.id] { - psp := input.kubernetes_pod_security_policy[_] - secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] - volume_field := psp.config.spec.volumes[_] - not input_volume_type_allowed(volume_field, secure_volumes) -} - -#rule for pod -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] - volume_fields := {x | pod.config.spec.volumes[_][x]; x != "name"} - field := volume_fields[_] - not input_volume_type_allowed(field, secure_volumes) -} - -#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] - volume_fields := {x | kind.config.spec.template.spec.volumes[_][x]; x != "name"} - field := volume_fields[_] - not input_volume_type_allowed(field, secure_volumes) -} - -#rule for cron_job -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - secure_volumes := [{{range .secure_volumes}}{{- printf "%q" . }},{{end}}] - volume_fields := {x | cron_job.config.spec.jobTemplate.spec.template.spec.volumes[_][x]; x != "name"} - field := volume_fields[_] - not input_volume_type_allowed(field, secure_volumes) -} - -input_volume_type_allowed(field, secure_volumes) { - secure_volumes[_] == "*" -} - -input_volume_type_allowed(field, secure_volumes) { - field == secure_volumes[_] -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json deleted file mode 100755 index 3a5ba1b5b..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.73.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": false, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_cron_job", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.73", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json deleted file mode 100755 index 860d3a3a0..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.74.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": false, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_daemonset", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.74", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json deleted file mode 100755 index a873dc584..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.75.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": false, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_deployment", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.75", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json deleted file mode 100755 index 497e12df1..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.76.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": false, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_job", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.76", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json deleted file mode 100755 index 72f9f6971..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.77.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": false, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_pod", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.77", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json deleted file mode 100755 index 2ec1d9e1f..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.78.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": false, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_replicaset", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.78", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json deleted file mode 100755 index 0becff4f8..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.79.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": false, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_replication_controller", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.79", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json deleted file mode 100755 index 6c54f996b..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.80.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": false, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_stateful_set", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.80", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json deleted file mode 100755 index 8e8aebda8..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.81.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": true, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_cron_job", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.81", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json deleted file mode 100755 index 9d06ffb02..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.82.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": true, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_daemonset", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.82", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json deleted file mode 100755 index 0259e0256..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.83.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": true, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_deployment", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.83", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json deleted file mode 100755 index 646842e5b..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.84.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": true, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_job", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.84", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json deleted file mode 100755 index 59f98d1aa..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.85.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": true, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_pod", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.85", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json deleted file mode 100755 index 32ae92f81..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.86.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": true, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_replicaset", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.86", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json deleted file mode 100755 index b82fb1132..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.87.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": true, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_replication_controller", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.87", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json deleted file mode 100755 index 33f7d06b3..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/accurics.kubernetes.IAM.88.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerHasAllowedCapabilities", - "file": "containerHasAllowedCapabilities.rego", - "template_args": { - "is_init": true, - "name": "containerHasAllowedCapabilities", - "prefix": "", - "resource_type": "kubernetes_stateful_set", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Pod has extra capabilities allowed", - "reference_id": "accurics.kubernetes.IAM.88", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego deleted file mode 100644 index bfe42d7e7..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/allowed_capabilities/containerHasAllowedCapabilities.rego +++ /dev/null @@ -1,119 +0,0 @@ -package accurics - -# Checks if any extra capabilities are added -{{.prefix}}{{.name}}{{.suffix}}[api.id] { - {{- template "spec" . }} - count(spec.allowedCapabilities) > 0 -} - -# Note, no TF-equivalent - -{{- if eq .is_init true}} - -{{.prefix}}{{.name}}{{.suffix}}[api.id] { - {{- template "initContainersSecurityContext" .}} - count(initContainersSecurityContext.capabilities.add) > 0 -} - -{{.prefix}}{{.name}}{{.suffix}}[api.id] { - {{- template "initContainersSecurityContextTF" .}} - count(initContainersSecurityContextTF.capabilities.add) > 0 -} - -{{- else }} - -{{.prefix}}{{.name}}{{.suffix}}[api.id] { - {{- template "containersSecurityContext" .}} - count(containersSecurityContext.capabilities.add) > 0 -} - -{{.prefix}}{{.name}}{{.suffix}}[api.id] { - {{- template "containersSecurityContextTF" .}} - count(containersSecurityContextTF.capabilities.add) > 0 -} - -{{- end }} - - -################################## -### Template definitions below ### -################################## -{{- define "api" }} - api = input.{{.resource_type}}[_] -{{- end}} - -# resolves path to the spec key -{{- define "spec" }} - {{- template "api" . }} - {{- if eq .resource_type "kubernetes_pod" }} - spec = api.config.spec - {{- else if eq .resource_type "kubernetes_pod_security_policy" }} - spec = api.config.spec - {{- else if eq .resource_type "kubernetes_cron_job" }} - spec = api.config.spec.jobTemplate.spec.template.spec - {{- else }} - spec = api.config.spec.template.spec - {{- end }} -{{- end }} - -# resolves path to the spec key for terraform-defined k8s resources -{{- define "specTF" }} - {{- template "api" . }} - {{- if eq .resource_type "kubernetes_pod" }} - specTF = api.config.spec - {{- else if eq .resource_type "kubernetes_pod_security_policy" }} - specTF = api.config.spec - {{- else if eq .resource_type "kubernetes_cron_job" }} - specTF = api.config.spec.job_template.spec.template.spec - {{- else }} - specTF = api.config.spec.template.spec - {{- end }} -{{- end }} - -# resolves path to the containers list -{{- define "containers" }} - {{- template "spec" . }} - containers = spec.containers[_] -{{- end }} - -# resolves path to the containers' security context -{{- define "containersSecurityContext" }} - {{- template "containers" . }} - containersSecurityContext = containers.securityContext -{{- end }} - -# resolves path to the containers list for terraform-defined k8s resources -{{- define "containersTF" }} - {{- template "specTF" . }} - containersTF = specTF.containers[_] -{{- end }} - -# resolves path to the containers' security context for terraform-defined k8s resources -{{- define "containersSecurityContextTF" }} - {{- template "containersTF" . }} - containersSecurityContextTF = containersTF.security_context -{{- end }} - -# resolves path to the initContainers list -{{- define "initContainers" }} - {{- template "spec" . }} - initContainers = spec.initContainers[_] -{{- end }} - -# resolves path to the initContainers' security context -{{- define "initContainersSecurityContext" }} - {{- template "initContainers" . }} - initContainersSecurityContext = initContainers.securityContext -{{- end }} - -# resolves path to the initContainers list for terraform-defined k8s resources -{{- define "initContainersTF" }} - {{- template "specTF" . }} - initContainersTF = specTF.init_containers[_] -{{- end }} - -# resolves path to the initContainers' security context for terraform-defined k8s resources -{{- define "initContainersSecurityContextTF" }} - {{- template "initContainersTF" . }} - initContainersSecurityContextTF = initContainersTF.security_context -{{- end }} diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego deleted file mode 100755 index 7be8687f3..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/appArmorProfile.rego +++ /dev/null @@ -1,108 +0,0 @@ -package accurics - -#rule for pod security policy, will be valid for terraform pod_security_policy -{{.prefix}}{{.name}}{{.suffix}}[psp.id] { - psp := input.kubernetes_pod_security_policy[_] - psp.config.metadata.annotations["apparmor.security.beta.kubernetes.io/defaultProfileName"] != "runtime/default" -} - -#rule for pod, covers containers -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.containers[_] - not input_apparmor_allowed(container.name, pod.config.metadata) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.initContainers[_] - not input_apparmor_allowed(container.name, pod.config.metadata) -} - -#terraform init_containers -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.init_containers[_] - not input_apparmor_allowed(container.name, pod.config.metadata) -} - -##rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.containers[_] - not input_apparmor_allowed(container.name, kind.config.spec.template.metadata) -} - -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.initContainers[_] - not input_apparmor_allowed(container.name, kind.config.spec.template.metadata) -} - -#terraform init_containers -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.init_containers[_] - not input_apparmor_allowed(container.name, kind.config.spec.template.metadata) -} - -#rule for cron_job, covers containers -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] - not input_apparmor_allowed(container.name, cron_job.config.spec.jobTemplate.spec.template.metadata) -} - -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] - not input_apparmor_allowed(container.name, cron_job.config.spec.jobTemplate.spec.template.metadata) -} - -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] - not input_apparmor_allowed(container.name, cron_job.config.spec.jobTemplate.spec.template.metadata) -} - -#function for all Kinds -input_apparmor_allowed(containerName, metadata) { - metadata.annotations[key] == "runtime/default" - key == sprintf("container.apparmor.security.beta.kubernetes.io/%v", [containerName]) -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego deleted file mode 100755 index 248878b60..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/autoMountTokenEnabled.rego +++ /dev/null @@ -1,33 +0,0 @@ -package accurics - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - pod.config.spec.automountServiceAccountToken == true -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_service_account[_] - pod.config.automountServiceAccountToken == true -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - pod.config.spec.template.spec.automountServiceAccountToken == true -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - pod.config.spec.jobTemplate.spec.template.spec.automountServiceAccountToken == true -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego deleted file mode 100755 index d8d1ae40a..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capSysAdminUsed.rego +++ /dev/null @@ -1,69 +0,0 @@ -package accurics - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.containers[_] - container.securityContext.capabilities.add == "-SYS_ADMIN" -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - initcontainer := pod.config.spec.initContainers[_] - initcontainer.securityContext.capabilities.add == "-SYS_ADMIN" -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] - container.securityContext.capabilities.add == "-SYS_ADMIN" -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] - initcontainer.securityContext.capabilities.add == "-SYS_ADMIN" -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - containerCheck(pod.config.spec.template.spec) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - initContainerCheck(pod.config.spec.template.spec) -} - -initContainerCheck(spec) { - container := spec.initContainers[_] - container.securityContext.capabilities.add == "-SYS_ADMIN" -} - -containerCheck(spec) { - container := spec.containers[_] - container.securityContext.capabilities.add == "-SYS_ADMIN" -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego deleted file mode 100755 index 9920b3851..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/capabilityUsed.rego +++ /dev/null @@ -1,74 +0,0 @@ -package accurics - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod_security_policy[_] - pod.config.spec.{{.attribute}} != [] -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.containers[_] - container.{{.attribute}} != [] -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - initcontainer := pod.config.spec.initContainers[_] - initcontainer.{{.attribute}} != [] -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] - container.{{.attribute}} != [] -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] - initcontainer.{{.attribute}} != [] -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - containerCheck(pod.config.spec.template.spec) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - initContainerCheck(pod.config.spec.template.spec) -} - -initContainerCheck(spec) { - container := spec.initContainers[_] - container.{{.attribute}} != [] -} - -containerCheck(spec) { - container := spec.containers[_] - container.{{.attribute}} != [] -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego deleted file mode 100755 index 07c315aed..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/commandCheck.rego +++ /dev/null @@ -1,19 +0,0 @@ -package accurics - -{{.prefix}}{{.name}}{{.suffix}}[pod_kubeapi.id] { - pod_kubeapi := input.kubernetes_pod[_] - cmds := pod_kubeapi.config.spec.containers[_].command - {{.negation}} check(cmds) -} - -check(cmds) { - cmd := cmds[_] - startswith(cmd, "{{.argument}}") - {{.presence}} contains(cmd, "{{.param}}") - {{.optional}} -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - cmds := pod.config.spec.containers[_].imagePullPolicy != "Always" -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json deleted file mode 100755 index 56545ab51..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.105.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": false, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_cron_job", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.105", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json deleted file mode 100755 index 4d978019c..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.106.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": false, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_daemonset", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.106", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json deleted file mode 100755 index ce27d4bb3..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.108.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": false, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_job", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.108", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json deleted file mode 100755 index b740200ba..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.109.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": false, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_pod", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.109", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json deleted file mode 100755 index 19489a08a..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.110.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": false, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_replicaset", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.110", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json deleted file mode 100755 index d4a8f5515..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.111.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": false, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_replication_controller", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.111", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json deleted file mode 100755 index a18dc1c63..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.112.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": false, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_stateful_set", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.112", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json deleted file mode 100755 index d1cdd0952..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.113.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": true, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_cron_job", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.113", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json deleted file mode 100755 index 64b9de334..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.114.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": true, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_daemonset", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.114", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json deleted file mode 100755 index 2f573f89b..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.115.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": true, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_deployment", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.115", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json deleted file mode 100755 index 0f80098b5..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.116.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": true, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_job", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.116", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json deleted file mode 100755 index 9e420b8e7..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.117.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": true, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_pod", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.117", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json deleted file mode 100755 index 17d411640..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.118.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": true, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_replicaset", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.118", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json deleted file mode 100755 index 076a4927b..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.119.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": true, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_replication_controller", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.119", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json deleted file mode 100755 index e5c922b56..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/accurics.kubernetes.IAM.120.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerResourcesNotDefined", - "file": "containerResourcesNotDefined.rego", - "template_args": { - "is_init": true, - "name": "containerResourcesNotDefined", - "prefix": "", - "resource_type": "kubernetes_stateful_set", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Container does not have resource limitations defined", - "reference_id": "accurics.kubernetes.IAM.120", - "category": "Identity and Access Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego deleted file mode 100644 index b0730347b..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_resources_not_defined/containerResourcesNotDefined.rego +++ /dev/null @@ -1,111 +0,0 @@ -package accurics - -{{- if eq .is_init true}} - -{{.prefix}}{{.name}}{{.suffix}}[api.id] { - {{- template "initContainers" . }} - not initContainers.resources -} - -{{.prefix}}{{.name}}{{.suffix}}[api.id] { - {{- template "initContainersTF" . }} - not initContainersTF.resources -} - -{{- else }} - -{{.prefix}}{{.name}}{{.suffix}}[api.id] { - {{- template "containers" . }} - not containers.resources -} - -{{.prefix}}{{.name}}{{.suffix}}[api.id] { - {{- template "containersTF" . }} - not containersTF.resources -} - -{{- end }} - - -################################## -### Template definitions below ### -################################## -{{- define "api" }} - api = input.{{.resource_type}}[_] -{{- end}} - -# resolves path to the spec key -{{- define "spec" }} - {{- template "api" . }} - {{- if eq .resource_type "kubernetes_pod" }} - spec = api.config.spec - {{- else if eq .resource_type "kubernetes_pod_security_policy" }} - spec = api.config.spec - {{- else if eq .resource_type "kubernetes_cron_job" }} - spec = api.config.spec.jobTemplate.spec.template.spec - {{- else }} - spec = api.config.spec.template.spec - {{- end }} -{{- end }} - -# resolves path to the spec key for terraform-defined k8s resources -{{- define "specTF" }} - {{- template "api" . }} - {{- if eq .resource_type "kubernetes_pod" }} - specTF = api.config.spec - {{- else if eq .resource_type "kubernetes_pod_security_policy" }} - specTF = api.config.spec - {{- else if eq .resource_type "kubernetes_cron_job" }} - specTF = api.config.spec.job_template.spec.template.spec - {{- else }} - specTF = api.config.spec.template.spec - {{- end }} -{{- end }} - -# resolves path to the containers list -{{- define "containers" }} - {{- template "spec" . }} - containers = spec.containers[_] -{{- end }} - -# resolves path to the containers' security context -{{- define "containersSecurityContext" }} - {{- template "containers" . }} - containersSecurityContext = containers.securityContext -{{- end }} - -# resolves path to the containers list for terraform-defined k8s resources -{{- define "containersTF" }} - {{- template "specTF" . }} - containersTF = specTF.containers[_] -{{- end }} - -# resolves path to the containers' security context for terraform-defined k8s resources -{{- define "containersSecurityContextTF" }} - {{- template "containersTF" . }} - containersSecurityContextTF = containersTF.security_context -{{- end }} - -# resolves path to the initContainers list -{{- define "initContainers" }} - {{- template "spec" . }} - initContainers = spec.initContainers[_] -{{- end }} - -# resolves path to the initContainers' security context -{{- define "initContainersSecurityContext" }} - {{- template "initContainers" . }} - initContainersSecurityContext = initContainers.securityContext -{{- end }} - -# resolves path to the initContainers list for terraform-defined k8s resources -{{- define "initContainersTF" }} - {{- template "specTF" . }} - initContainersTF = specTF.init_containers[_] -{{- end }} - -# resolves path to the initContainers' security context for terraform-defined k8s resources -{{- define "initContainersSecurityContextTF" }} - {{- template "initContainersTF" . }} - initContainersSecurityContextTF = initContainersTF.security_context -{{- end }} diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json deleted file mode 100755 index 7962ef773..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.57.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": false, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_cron_job", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.57", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json deleted file mode 100755 index f8f70eaf8..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.58.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": false, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_daemonset", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.58", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json deleted file mode 100755 index 739c621d5..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.59.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": false, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_deployment", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.59", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json deleted file mode 100755 index f8a42a84a..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.60.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": false, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_job", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.60", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json deleted file mode 100755 index fc7091e05..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.61.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": false, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_pod", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.61", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json deleted file mode 100755 index 3fc461ffb..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.62.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": false, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_replicaset", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.62", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json deleted file mode 100755 index 59662b349..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.63.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": false, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_replication_controller", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.63", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json deleted file mode 100755 index 13cb5dc87..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.64.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": false, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_stateful_set", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.64", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json deleted file mode 100755 index 00ec55c5a..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.65.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": true, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_cron_job", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.65", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json deleted file mode 100755 index 0f4a3d56f..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.66.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": true, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_daemonset", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.66", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json deleted file mode 100755 index 42cd86828..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.67.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": true, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_deployment", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.67", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json deleted file mode 100755 index d023923ea..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.68.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": true, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_job", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.68", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json deleted file mode 100755 index c522de8fb..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.69.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": true, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_pod", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.69", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json deleted file mode 100755 index 45c9cb9d5..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.70.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": true, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_replicaset", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.70", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json deleted file mode 100755 index 4bbbdd267..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.71.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": true, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_replication_controller", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.71", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json deleted file mode 100755 index 88fc00e56..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/accurics.kubernetes.EKM.72.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "containerUsesSecretsInEnvironmentVar", - "file": "containerUsesSecretsInEnvironmentVar.rego", - "template_args": { - "is_init": true, - "name": "containerUsesSecretsInEnvironmentVar", - "prefix": "", - "resource_type": "kubernetes_stateful_set", - "suffix": "" - }, - "severity": "HIGH", - "description": "Container uses secrets in environment variables", - "reference_id": "accurics.kubernetes.EKM.72", - "category": "Encryption and Key Management", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego deleted file mode 100644 index 86c10e36e..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/container_uses_secrets_in_env/containerUsesSecretsInEnvironmentVar.rego +++ /dev/null @@ -1,115 +0,0 @@ -package accurics - -{{- if eq .is_init true}} - -{{.prefix}}{{.name}}{{.suffix}}[api.id] { - {{- template "initContainers" .}} - envVars := initContainers.env[_] - envVars.valueFrom.secretKeyRef -} - -{{.prefix}}{{.name}}{{.suffix}}[api.id] { - {{- template "initContainersTF" .}} - envVars := initContainersTF.env[_] - envVars.valueFrom.secretKeyRef -} - -{{- else }} - -{{.prefix}}{{.name}}{{.suffix}}[api.id] { - {{- template "containers" .}} - envVars := containers.env[_] - envVars.valueFrom.secretKeyRef -} - -{{.prefix}}{{.name}}{{.suffix}}[api.id] { - {{- template "containersTF" .}} - envVars := containersTF.env[_] - envVars.valueFrom.secretKeyRef -} - -{{- end }} - - -################################## -### Template definitions below ### -################################## -{{- define "api" }} - api = input.{{.resource_type}}[_] -{{- end}} - -# resolves path to the spec key -{{- define "spec" }} - {{- template "api" . }} - {{- if eq .resource_type "kubernetes_pod" }} - spec = api.config.spec - {{- else if eq .resource_type "kubernetes_pod_security_policy" }} - spec = api.config.spec - {{- else if eq .resource_type "kubernetes_cron_job" }} - spec = api.config.spec.jobTemplate.spec.template.spec - {{- else }} - spec = api.config.spec.template.spec - {{- end }} -{{- end }} - -# resolves path to the spec key for terraform-defined k8s resources -{{- define "specTF" }} - {{- template "api" . }} - {{- if eq .resource_type "kubernetes_pod" }} - specTF = api.config.spec - {{- else if eq .resource_type "kubernetes_pod_security_policy" }} - specTF = api.config.spec - {{- else if eq .resource_type "kubernetes_cron_job" }} - specTF = api.config.spec.job_template.spec.template.spec - {{- else }} - specTF = api.config.spec.template.spec - {{- end }} -{{- end }} - -# resolves path to the containers list -{{- define "containers" }} - {{- template "spec" . }} - containers = spec.containers[_] -{{- end }} - -# resolves path to the containers' security context -{{- define "containersSecurityContext" }} - {{- template "containers" . }} - containersSecurityContext = containers.securityContext -{{- end }} - -# resolves path to the containers list for terraform-defined k8s resources -{{- define "containersTF" }} - {{- template "specTF" . }} - containersTF = specTF.containers[_] -{{- end }} - -# resolves path to the containers' security context for terraform-defined k8s resources -{{- define "containersSecurityContextTF" }} - {{- template "containersTF" . }} - containersSecurityContextTF = containersTF.security_context -{{- end }} - -# resolves path to the initContainers list -{{- define "initContainers" }} - {{- template "spec" . }} - initContainers = spec.initContainers[_] -{{- end }} - -# resolves path to the initContainers' security context -{{- define "initContainersSecurityContext" }} - {{- template "initContainers" . }} - initContainersSecurityContext = initContainers.securityContext -{{- end }} - -# resolves path to the initContainers list for terraform-defined k8s resources -{{- define "initContainersTF" }} - {{- template "specTF" . }} - initContainersTF = specTF.init_containers[_] -{{- end }} - -# resolves path to the initContainers' security context for terraform-defined k8s resources -{{- define "initContainersSecurityContextTF" }} - {{- template "initContainersTF" . }} - initContainersSecurityContextTF = initContainersTF.security_context -{{- end }} diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego deleted file mode 100755 index 6d6e8e677..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/containersAsHighUID.rego +++ /dev/null @@ -1,102 +0,0 @@ -package accurics - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - pod.config.spec.securityContext.runAsUser < 1000 -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.containers[_] - container.securityContext.runAsUser < 1000 -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - initcontainer := pod.config.spec.initContainers[_] - initcontainer.securityContext.runAsUser < 1000 -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - pod.config.spec.jobTemplate.spec.template.spec.securityContext.runAsUser < 1000 -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] - container.securityContext.runAsUser < 1000 -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] - initcontainer.securityContext.runAsUser < 1000 -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod_security_policy[_] - ranges := pod.config.spec.runAsUser.ranges[_] - ranges.min < 1000 -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - pod.config.spec.template.spec.securityContext.runAsUser < 1000 -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - checkContainer(pod.config.spec.template.spec) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - checkInitContainer(pod.config.spec.template.spec) -} - -checkInitContainer(spec) { - containers := spec.initContainers[_] - containers.securityContext.runAsUser < 1000 -} - -checkContainer(spec) { - containers := spec.containers[_] - containers.securityContext.runAsUser < 1000 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego deleted file mode 100755 index f918cc215..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowedSysCalls.rego +++ /dev/null @@ -1,51 +0,0 @@ -### this pollicy depends on the parameters specified by the user/client. Here we are considering that no kernel level syscalls are allowed### -package accurics - -#rule for pod -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - forbiddenSysctls = ["kernel.*"] - sysctl := pod.config.spec.securityContext.sysctls[_].name - forbidden_sysctl(sysctl, forbiddenSysctls) -} - -##rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - forbiddenSysctls = ["kernel.*"] - sysctl := kind.config.spec.template.spec.securityContext.sysctls[_].name - forbidden_sysctl(sysctl, forbiddenSysctls) -} - -#rule for cron_job -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - forbiddenSysctls = ["kernel.*"] - sysctl := cron_job.config.spec.jobTemplate.spec.template.spec.securityContext.sysctls[_].name - forbidden_sysctl(sysctl, forbiddenSysctls) -} - -# if all syscalls are forbidden -forbidden_sysctl(sysctl, arg) { - arg[_] == "*" -} - -forbidden_sysctl(sysctl, arg) { - arg[_] == sysctl -} - -forbidden_sysctl(sysctl, arg) { - startswith(sysctl, trim(arg[_], "*")) -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json deleted file mode 100644 index 10000a6e6..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/AC-K8-DS-PO-M-0143.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "disAllowedVolumes", - "file": "disAllowedVolumes.rego", - "template_args": { - "name": "disAllowedVolumes", - "prefix": "", - "suffix": "" - }, - "severity": "MEDIUM", - "description": "Vulnerable to CVE-2020-8555 (affected version of kube-controller-manager: v1.18.0, v1.17.0 - v1.17.4, v1.16.0 - v1.16.8,< v1.15.11", - "reference_id": "AC-K8-DS-PO-M-0143", - "category": "Data Security", - "version": 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego deleted file mode 100644 index 706bbafa8..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/disallowed_volumes/disAllowedVolumes.rego +++ /dev/null @@ -1,52 +0,0 @@ -package accurics - -#rule for pod_security_policy -{{.prefix}}{{.name}}{{.suffix}}[psp.id] { - psp := input.kubernetes_pod_security_policy[_] - affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] - volume_type := psp.config.spec.volumes[_] - volNotAllowed(volume_type, affected_volumes) -} - -#rule for pod -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] - volume_types := {x | pod.config.spec.volumes[_][x]; x != "name"} - vol:= volume_types[_] - volNotAllowed(vol, affected_volumes) -} - -#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] - volume_types := {x | kind.config.spec.template.spec.volumes[_][x]; x != "name"} - vol:= volume_types[_] - volNotAllowed(vol, affected_volumes) -} - -#rule for cron_job -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - affected_volumes := ["glusterfs", "quobyte", "storageos", "scaleIO"] - volume_types := {x | cron_job.config.spec.jobTemplate.spec.template.spec.volumes[_][x]; x != "name"} - vol:= volume_types[_] - volNotAllowed(vol, affected_volumes) -} - -volNotAllowed(field, affected_volumes) { - field == affected_volumes[_] -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego deleted file mode 100755 index 890439ed6..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/dockerSockCheck.rego +++ /dev/null @@ -1,35 +0,0 @@ -package accurics - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - vol := pod.config.spec.jobTemplate.spec.template.spec.volumes[_] - socketPathCheck(vol.hostPath.path) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - vol := pod.config.spec.volumes[_] - socketPathCheck(vol.hostPath.path) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - vol := pod.config.spec.template.spec.volumes[_] - socketPathCheck(vol.hostPath.path) -} - -socketPathCheck(attrib) { - contains(attrib, "/var/run/docker") -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego deleted file mode 100755 index de75e611a..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithLatestTag.rego +++ /dev/null @@ -1,196 +0,0 @@ -package accurics - -#rule for pod, covers containers, initContainers, terraform, init_containers -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.containers[_] - checkForPodLatest(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.containers[_] - checkForPodNoTag(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.initContainers[_] - checkForPodLatest(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.initContainers[_] - checkForPodNoTag(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.init_containers[_] - checkForPodLatest(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.init_containers[_] - checkForPodNoTag(container) -} - -#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers, initContainers, terraform init_containers -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.containers[_] - checkForPodLatest(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.containers[_] - checkForPodNoTag(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.initContainers[_] - checkForPodLatest(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.init_containers[_] - checkForPodLatest(container) - } - -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.initContainers[_] - checkForPodNoTag(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.init_containers[_] - checkForPodNoTag(container) -} - -#rule for cron_job, covers containers, initContainers, terraform init_containers -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] - checkForPodLatest(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] - checkForPodNoTag(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] - checkForPodLatest(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] - checkForPodLatest(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] - checkForPodNoTag(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] - checkForPodNoTag(container) -} - -#check function for All KINDs -checkForPodLatest(arg) { - img_split := split(arg.image, ":") - tag := img_split[count(img_split) - 1] - tag == "latest" -} - -checkForPodNoTag(argument) { - img_split := split(argument.image, ":") - count(img_split) == 1 -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego deleted file mode 100755 index 6cf92e6f6..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/imageWithoutDigest.rego +++ /dev/null @@ -1,105 +0,0 @@ -package accurics - -#rule for pod, same will satisfy terraform pod, covers containers, initContainers, and terraform init_containers -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.containers[_] - satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] - not all(satisfied) -} - -#rule for init containers -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.initContainers[_] - satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] - not all(satisfied) -} - -#rule for terraform init_containers -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.init_containers[_] - satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] - not all(satisfied) -} - -#rule for deployment, daemonset, job, replica_set, replication_controller, stateful_set covers containers, initContainers, terraform init_containers -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.containers[_] - satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] - not all(satisfied) -} - -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.initContainers[_] - satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] - not all(satisfied) -} - -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.init_containers[_] - satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] - not all(satisfied) -} - -#rule for cron_job, covers containers, initContainers, terraform init_containers -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] - satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] - not all(satisfied) -} - -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] - satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] - not all(satisfied) -} - -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.init_containers[_] - satisfied := [re_match("@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+", container.image)] - not all(satisfied) -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego deleted file mode 100755 index d9746d16b..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/kubeDashboardEnabled.rego +++ /dev/null @@ -1,6 +0,0 @@ -package accurics - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - pod.config.metadata.labels.app == "kubernetes-dashboard" -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego deleted file mode 100755 index 7ce3427d2..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/otherNamespace.rego +++ /dev/null @@ -1,20 +0,0 @@ -package accurics - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_cron_job", "undefined"), - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_pod", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - pod.config.metadata.namespace == "default" -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego deleted file mode 100755 index 148651ed3..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/priviledgedContainersEnabled.rego +++ /dev/null @@ -1,11 +0,0 @@ -package accurics - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - pod.config.spec.privileged == true -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod_security_policy[_] - pod.config.spec.privileged == true -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego deleted file mode 100755 index 19eaf1aef..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/probeCheck.rego +++ /dev/null @@ -1,68 +0,0 @@ -#liveenessprobe and readinessprobe are not applicable for init containers. -package accurics - -#rule for pod -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.containers[_] - not container["{{.argument}}"] -} - -#rule for pod terraform -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.containers[_] - not container["{{.argumentTF}}"] -} - -#rule for deployment, daemonset, job, replica_Set, replication_controller, stateful_set -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.containers[_] - not container["{{.argument}}"] -} - -#rule for terraform deployment, daemonset, job, replica_Set, replication_controller, stateful_set -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.containers[_] - not container["{{.argumentTF}}"] -} - -#rule for cronjob -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] - not container["{{.argument}}"] -} - -#rule for terraform cronjob -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] - not container["{{.argumentTF}}"] -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego deleted file mode 100755 index f4bd21aeb..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secCompProfile.rego +++ /dev/null @@ -1,153 +0,0 @@ -package accurics - -#rule for pod, pod_security_policy covers containers -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_pod", "undefined"), - object.get(input, "kubernetes_pod_security_policy", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - not input_container_allowed(kind.config.metadata) -} - -#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - not input_container_allowed(kind.config.spec.template.metadata) -} - -#rule for cron_job -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - not input_container_allowed(cron_job.config.spec.jobTemplate.spec.template.metadata) -} - -input_container_allowed(metadata) { - metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"] == "runtime/default" -} - -input_container_allowed(metadata) { - metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"] == "docker/default" -} - - ####Kubernetes v1.19 or later######## - -#rule for pod covers containers and checks field seccompProfile at container security context which is found at spec.containers. -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.containers[_] - not check_seccomp(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.initContainers[_] - not check_seccomp(container) -} - -#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.containers[_] - not check_seccomp(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - container := kind.config.spec.template.spec.initContainers[_] - not check_seccomp(container) -} - -#rule for cron_job -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.containers[_] - not check_seccomp(container) -} - -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - container := cron_job.config.spec.jobTemplate.spec.template.spec.initContainers[_] - not check_seccomp(container) -} - -##rule to check seccompProfile at PodSecurityContext which is found at PodSpec## - -#rule for pod -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - not check_seccomp(pod.config.spec) -} - -#rule for deployment, daemonset, job, replica_set, stateful_set, replication_controller covers containers -{{.prefix}}{{.name}}{{.suffix}}[kind.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - kind := item[_] - not check_seccomp(kind.config.spec.template.spec) -} - -#rule for cron_job -{{.prefix}}{{.name}}{{.suffix}}[cron_job.id] { - cron_job := input.kubernetes_cron_job[_] - not check_seccomp(cron_job.config.spec.jobTemplate.spec.template.spec) -} - -#function for all Kinds and scenarios -check_seccomp(container) { - container.securityContext.seccompProfile.type == "RuntimeDefault" -} - -check_seccomp(container) { - container.securityContext.seccompProfile.type == "DockerDefault" -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego deleted file mode 100755 index 780bb4577..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/secretsAsEnvVariables.rego +++ /dev/null @@ -1,75 +0,0 @@ -package accurics - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - containers := pod.config.spec.containers[_] - env := containers.env[_] - env.valueFrom != [] -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - initcontainer := pod.config.spec.initContainers[_] - env := initcontainer.env[_] - env.valueFrom != [] -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - containers := pod.config.spec.jobTemplate.spec.template.spec.containers[_] - env := containers.env[_] - env.valueFrom != [] -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] - env := initcontainer.env[_] - env.valueFrom != [] -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - checkContainer(pod.config.spec.template.spec) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - checkInitContainer(pod.config.spec.template.spec) -} - -checkInitContainer(spec) { - containers := spec.initContainers[_] - env := containers.env[_] - env.valueFrom != [] -} - -checkContainer(spec) { - containers := spec.containers[_] - env := containers.env[_] - env.valueFrom != [] -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego deleted file mode 100755 index f76c53966..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextCheck.rego +++ /dev/null @@ -1,76 +0,0 @@ -package accurics - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - checkCorrectAttribute(pod.config.spec.jobTemplate.spec.template.spec) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - checkCorrectAttribute(pod.config.spec.template.spec) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - checkCorrectAttribute(pod.config.spec) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod_security_policy[_] - podSecurityCheck(pod.config.spec) -} - -checkCorrectAttribute(spec) { - container := spec.containers[_] - containerSecurityCheck(container) -} - -checkCorrectAttribute(spec) { - container := spec.initContainers[_] - containerSecurityCheck(container) -} - -checkCorrectAttribute(spec) { - secContext := spec.securityContext - podSecurityCheck(secContext) -} - -containerSecurityCheck(container) { - {{.not_allowed}} - container.{{.param1}}.{{.param}} == {{.value}} -} - -containerSecurityCheck(container) { - object.get(container, "{{.param1}}", "undefined") == "undefined" -} - -containerSecurityCheck(container) { - not container.{{.param1}}.{{.param}} -} - -containerSecurityCheck(container) { - {{.allowed}} - not container.{{.param1}}.{{.arg1}}.{{.arg2}} -} - -podSecurityCheck(secContext) { - {{.not_allowed}} - secContext.{{.param}} == {{.value}} -} - -podSecurityCheck(secContext) { - {{.not_allowed}} - object.get(secContext, "{{.param}}", "undefined") == "undefined" -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego deleted file mode 100755 index 5d24387ea..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/securityContextUsed.rego +++ /dev/null @@ -1,103 +0,0 @@ -package accurics - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - container := pod.config.spec.containers[_] - not container.securityContext -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - initcontainer := pod.config.spec.initContainers[_] - not initcontainer.securityContext -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - not pod.config.spec.securityContext -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - container := pod.config.spec.jobTemplate.spec.template.spec.containers[_] - not container.securityContext -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - initcontainer := pod.config.spec.jobTemplate.spec.template.spec.initContainers[_] - not initcontainer.securityContext -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - not pod.config.spec.jobTemplate.spec.template.spec.securityContext -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined"), - object.get(input, "kubernetes_cron_job", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - checkPod(pod) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined"), - object.get(input, "kubernetes_cron_job", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - checkInitContainer(pod.config.spec.template.spec) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined"), - object.get(input, "kubernetes_cron_job", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - checkContainer(pod.config.spec.template.spec) -} - -checkContainer(spec) { - containers := spec.containers[_] - not containers.securityContext -} - -checkInitContainer(spec) { - containers := spec.initContainers[_] - not containers.securityContext -} - -checkPod(pod) { - not pod.config.spec.template.spec.securityContext -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego deleted file mode 100755 index 579448722..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/specBoolCheck.rego +++ /dev/null @@ -1,36 +0,0 @@ -package accurics - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - attribute := pod.config.spec.jobTemplate.spec.template.spec - boolCheck(attribute) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - attribute := pod.config.spec - boolCheck(attribute) -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - attribute := pod.config.spec.template.spec - - boolCheck(attribute) -} - -boolCheck(attribute) { - attribute.{{.param}} == {{.value}} -} \ No newline at end of file diff --git a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego b/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego deleted file mode 100755 index f4b90f2e4..000000000 --- a/pkg/http-server/k8s_testdata/testpolicies/kubernetes_pod/tillerDeployed.rego +++ /dev/null @@ -1,35 +0,0 @@ -package accurics - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_pod[_] - pod.config.metadata.labels.app == "helm" - pod.config.metadata.labels.name == "tiller" -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - pod := input.kubernetes_cron_job[_] - pod.config.spec.jobTemplate.spec.template.metadata.labels.app == "helm" - pod.config.spec.jobTemplate.spec.template.metadata.labels.name == "tiller" -} - -{{.prefix}}{{.name}}{{.suffix}}[pod.id] { - item_list := [ - object.get(input, "kubernetes_daemonset", "undefined"), - object.get(input, "kubernetes_deployment", "undefined"), - object.get(input, "kubernetes_job", "undefined"), - object.get(input, "kubernetes_replica_set", "undefined"), - object.get(input, "kubernetes_replication_controller", "undefined"), - object.get(input, "kubernetes_stateful_set", "undefined") - ] - - item = item_list[_] - item != "undefined" - - pod := item[_] - checkPod(pod) -} - -checkPod(pod) { - pod.config.spec.template.metadata.labels.app == "helm" - pod.config.spec.template.metadata.labels.name == "tiller" -} \ No newline at end of file From 6aceaa78640434b46e3fb19c9885e094ecada0c3 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Tue, 16 Mar 2021 12:16:56 +0530 Subject: [PATCH 17/38] removing unnecessary file changes --- .gitignore | 3 --- deploy/docker-compose.yml | 1 - pkg/runtime/executor_test.go | 3 +++ 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 5dc0e2f3b..31bce5d8d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,9 +23,6 @@ docs/_build/ #vscode .vscode/ -#GoLand -.idea - /updatedFiles # Go binar diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 6a430a5ca..9210f5c88 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -4,4 +4,3 @@ services: image: accurics/terrascan:${TAG:-latest} ports: - 9010:9010 - - 443:9443 diff --git a/pkg/runtime/executor_test.go b/pkg/runtime/executor_test.go index 51f1729fb..1effb0ea2 100644 --- a/pkg/runtime/executor_test.go +++ b/pkg/runtime/executor_test.go @@ -1,9 +1,12 @@ /* Copyright (C) 2020 Accurics, Inc. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. From e8f9dac7ea5cda056f791858f20440dc02bdc390 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Thu, 18 Mar 2021 23:17:07 +0530 Subject: [PATCH 18/38] fixing html file for fetching all logs --- pkg/http-server/templates/index.html | 2 +- pkg/http-server/webhook-scan-logs.go | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/http-server/templates/index.html b/pkg/http-server/templates/index.html index 43b0bd936..69000acc8 100644 --- a/pkg/http-server/templates/index.html +++ b/pkg/http-server/templates/index.html @@ -19,7 +19,7 @@ {{range .}} - {{.CreatedAt}} + {{.CreatedAt}} {{.Status}} {{.Request}} {{.Reasoning}} diff --git a/pkg/http-server/webhook-scan-logs.go b/pkg/http-server/webhook-scan-logs.go index f6c0246c7..9222bde97 100644 --- a/pkg/http-server/webhook-scan-logs.go +++ b/pkg/http-server/webhook-scan-logs.go @@ -72,7 +72,12 @@ func (g *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) { } // The templates are saved in the docker in this location - t, _ := template.ParseFiles("/go/terrascan/index.html") + t, err := template.ParseFiles("/go/terrascan/index.html") + if err != nil { + errMsg := fmt.Sprintf("failed to parse index.html file; error: '%v'", err) + zap.S().Error(errMsg) + apiErrorResponse(w, errMsg, http.StatusInternalServerError) + } logs, err := logger.fetchLogs() if err != nil { @@ -93,7 +98,12 @@ func (g *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) { }) } - t.Execute(w, logsData) + if err := t.Execute(w, logsData); err != nil { + errMsg := fmt.Sprintf("failed to execute html template; error: '%v'", err) + zap.S().Error(errMsg) + apiErrorResponse(w, errMsg, http.StatusInternalServerError) + return + } } func (g *APIHandler) getLogByUID(w http.ResponseWriter, r *http.Request) { From bfb84a2396290e885f0205e0fd6adcb52f103529 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Fri, 19 Mar 2021 14:29:03 +0530 Subject: [PATCH 19/38] adding some unit tests --- pkg/k8s/admission-webhook/testdata/empty.json | 1 + .../admission-webhook/testdata/invalid.json | 1 + pkg/k8s/admission-webhook/testdata/valid.json | 27 ++++ .../validating-webhook_test.go | 117 ++++++++++++++++++ 4 files changed, 146 insertions(+) create mode 100644 pkg/k8s/admission-webhook/testdata/empty.json create mode 100644 pkg/k8s/admission-webhook/testdata/invalid.json create mode 100644 pkg/k8s/admission-webhook/testdata/valid.json create mode 100644 pkg/k8s/admission-webhook/validating-webhook_test.go diff --git a/pkg/k8s/admission-webhook/testdata/empty.json b/pkg/k8s/admission-webhook/testdata/empty.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/pkg/k8s/admission-webhook/testdata/empty.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkg/k8s/admission-webhook/testdata/invalid.json b/pkg/k8s/admission-webhook/testdata/invalid.json new file mode 100644 index 000000000..f9ff3aaaf --- /dev/null +++ b/pkg/k8s/admission-webhook/testdata/invalid.json @@ -0,0 +1 @@ +some invalid tf file diff --git a/pkg/k8s/admission-webhook/testdata/valid.json b/pkg/k8s/admission-webhook/testdata/valid.json new file mode 100644 index 000000000..6e3bc1a1f --- /dev/null +++ b/pkg/k8s/admission-webhook/testdata/valid.json @@ -0,0 +1,27 @@ +{ + "apiVersion": "admission.k8s.io/v1", + "kind": "AdmissionReview", + "request": { + "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", + "object": { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "creationTimestamp": "2021-02-16T19:16:01Z", + "labels": { + "run": "nginx" + }, + "name": "nginx", + "namespace": "default", + "resourceVersion": "17561", + "selfLink": "/api/v1/namespaces/default/pods/nginx", + "uid": "7a269efe-d951-49b6-a3af-e1a265cb9efe" + }, + "spec": { + "containers": [ + ] + } + }, + "operation": "CREATE" + } +} diff --git a/pkg/k8s/admission-webhook/validating-webhook_test.go b/pkg/k8s/admission-webhook/validating-webhook_test.go new file mode 100644 index 000000000..aa8482748 --- /dev/null +++ b/pkg/k8s/admission-webhook/validating-webhook_test.go @@ -0,0 +1,117 @@ +/* + Copyright (C) 2020 Accurics, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package admissionwebhook + +import ( + "io/ioutil" + "os" + "testing" +) + +func TestAuthorize(t *testing.T) { + + table := []struct { + name string + apiKey string + envK8sAPIKey string + wantErr error + }{ + { + name: "empty api key", + apiKey: "", + envK8sAPIKey: "valid", + wantErr: ErrAPIKeyMissing, + }, + { + name: "K8S_WEBHOOK_API_KEY env not set", + apiKey: "valid", + envK8sAPIKey: "", + wantErr: ErrAPIKeyEnvNotSet, + }, + { + name: "invalid api key", + apiKey: "invalid", + envK8sAPIKey: "valid", + wantErr: ErrUnauthorized, + }, + { + name: "valid api key", + apiKey: "valid", + envK8sAPIKey: "valid", + wantErr: nil, + }, + } + + for _, test := range table { + t.Run(test.name, func(t *testing.T) { + + // validating webhook object + var w ValidatingWebhook + + // set K8S_WEBHOOK_API_KEY env if not empty + if test.envK8sAPIKey != "" { + os.Setenv("K8S_WEBHOOK_API_KEY", test.envK8sAPIKey) + } + defer os.Unsetenv("K8S_WEBHOOK_API_KEY") + + err := w.Authorize(test.apiKey) + if err != test.wantErr { + t.Errorf("unexpected error; got: '%v', want: '%v'", err, test.wantErr) + } + }) + } +} + +func TestDecodeAdmissionReviewRequest(t *testing.T) { + + table := []struct { + name string + requestFile string + wantErr bool + }{ + { + name: "empty review request", + requestFile: "testdata/empty.json", + wantErr: false, + }, + { + name: "invalid review request", + requestFile: "testdata/invalid.json", + wantErr: true, + }, + { + name: "valid review request", + requestFile: "testdata/valid.json", + wantErr: false, + }, + } + + for _, test := range table { + + // read test request from file + requestBody, err := ioutil.ReadFile(test.requestFile) + if err != nil { + t.Errorf("failed to read test data, error: '%v'", err) + } + + var w ValidatingWebhook + _, err = w.DecodeAdmissionReviewRequest(requestBody) + if (err == nil) == test.wantErr { + t.Errorf("unexpected error '%v'", err) + } + } +} From be2255fb9ad2e2e82e6757d3f532f8b7e2b60e07 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Fri, 19 Mar 2021 22:10:37 +0530 Subject: [PATCH 20/38] refactor to move db logger into a separate dedicated package --- pkg/http-server/webhook-scan-logger_test.go | 78 ------------ pkg/http-server/webhook-scan-logs.go | 31 ++--- pkg/http-server/webhook-scan.go | 7 +- pkg/http-server/webhook-scan_test.go | 8 +- .../dblogs}/webhook-scan-logger.go | 66 +++++++--- pkg/k8s/dblogs/webhook-scan-logger_test.go | 116 ++++++++++++++++++ 6 files changed, 185 insertions(+), 121 deletions(-) delete mode 100644 pkg/http-server/webhook-scan-logger_test.go rename pkg/{http-server => k8s/dblogs}/webhook-scan-logger.go (70%) create mode 100644 pkg/k8s/dblogs/webhook-scan-logger_test.go diff --git a/pkg/http-server/webhook-scan-logger_test.go b/pkg/http-server/webhook-scan-logger_test.go deleted file mode 100644 index adba7f537..000000000 --- a/pkg/http-server/webhook-scan-logger_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package httpserver - -import ( - "testing" - "time" -) - -func TestLogs(t *testing.T) { - t.Run("Log a new webhook scan", func(t *testing.T) { - var logger = WebhookScanLogger{ - test: true, - } - defer logger.clearDbFilePath() - - fetchedLogs, err := logger.fetchLogs() - if len(fetchedLogs) > 0 { - t.Errorf("At the beginning no logs should exist. Got: '%v'", len(fetchedLogs)) - } - - if err != nil { - t.Errorf("Got error") - } - - var log = webhookScanLog{ - UID: "myUID", - Request: "MyRequest", - CreatedAt: time.Now(), - Allowed: true, - DeniableViolations: "MyViolations", - ViolationsSummary: "ViolationsSummary", - } - - logger.log(log) - - fetchedLogs, err = logger.fetchLogs() - if err != nil { - t.Errorf("Got error") - } - - if len(fetchedLogs) != 1 { - t.Errorf("A new log should be returned. Got: '%v' logs", len(fetchedLogs)) - } - - myFetchLog, err := logger.fetchLogByID(log.UID) - if err != nil { - t.Errorf("Got error") - } - - if len(myFetchLog.UID) < 1 { - t.Errorf("Log with ID: '%v' is not returned by fetchLogByID", log.UID) - } - - if myFetchLog.UID != log.UID { - t.Errorf("Wrong UID. Expected '%v', Got: '%v'", log.UID, myFetchLog.UID) - - } - - if myFetchLog.Allowed != log.Allowed { - t.Errorf("Wrong Allowed. Expected '%v', Got: '%v'", log.Allowed, myFetchLog.Allowed) - } - - if myFetchLog.ViolationsSummary != log.ViolationsSummary { - t.Errorf("Wrong ViolationsSummary. Expected '%v', Got: '%v'", log.ViolationsSummary, myFetchLog.ViolationsSummary) - } - - if myFetchLog.Request != log.Request { - t.Errorf("Wrong Request. Expected '%v', Got: '%v'", log.Request, myFetchLog.Request) - } - - if myFetchLog.DeniableViolations != log.DeniableViolations { - t.Errorf("Wrong DeniableViolations. Expected '%v', Got: '%v'", log.DeniableViolations, myFetchLog.DeniableViolations) - } - - if myFetchLog.CreatedAt.Unix() != log.CreatedAt.Unix() { - t.Errorf("Wrong CreatedAt. Expected '%v', Got: '%v'", log.CreatedAt, myFetchLog.CreatedAt) - } - }) -} diff --git a/pkg/http-server/webhook-scan-logs.go b/pkg/http-server/webhook-scan-logs.go index 9222bde97..7bcfbf5d5 100644 --- a/pkg/http-server/webhook-scan-logs.go +++ b/pkg/http-server/webhook-scan-logs.go @@ -23,12 +23,10 @@ import ( "net/http" "time" + "github.com/accurics/terrascan/pkg/k8s/dblogs" "github.com/accurics/terrascan/pkg/results" "github.com/gorilla/mux" "go.uber.org/zap" - - // importing sqlite driver - _ "github.com/mattn/go-sqlite3" ) type webhookDisplayedViolation struct { @@ -67,9 +65,7 @@ type webhookDisplayedShowLog struct { func (g *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) { // Return an HTML page including all the logs history - logger := WebhookScanLogger{ - test: g.test, - } + logger := dblogs.NewWebhookScanLogger() // The templates are saved in the docker in this location t, err := template.ParseFiles("/go/terrascan/index.html") @@ -79,7 +75,7 @@ func (g *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) { apiErrorResponse(w, errMsg, http.StatusInternalServerError) } - logs, err := logger.fetchLogs() + logs, err := logger.FetchLogs() if err != nil { errMsg := fmt.Sprintf("error reading logs from DB: '%v'", err) zap.S().Error(errMsg) @@ -107,21 +103,20 @@ func (g *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) { } func (g *APIHandler) getLogByUID(w http.ResponseWriter, r *http.Request) { - // Return an HTML page including the selected log - params := mux.Vars(r) + // Return an HTML page including the selected log + var ( + params = mux.Vars(r) + uid = params["uid"] + logger = dblogs.NewWebhookScanLogger() + ) - var uid = params["uid"] if len(uid) < 1 { apiErrorResponse(w, "Log UID is missing", http.StatusBadRequest) return } - logger := WebhookScanLogger{ - test: g.test, - } - - log, err := logger.fetchLogByID(uid) + log, err := logger.FetchLogByID(uid) if err != nil { errMsg := fmt.Sprintf("error reading logs from DB: '%v'", err) zap.S().Error(errMsg) @@ -153,7 +148,7 @@ func (g *APIHandler) getLogPath(host, logUID string) string { return fmt.Sprintf("https://%v/k8s/webhooks/logs/%v", host, logUID) } -func (g *APIHandler) getLogStatus(log webhookScanLog) string { +func (g *APIHandler) getLogStatus(log dblogs.WebhookScanLog) string { // Calculate a log status: // 1. !Allowed -> Rejected // 2. Allowed -> if there are violations -> Allowed with Warnings. Otherwise -> Allowed @@ -174,7 +169,7 @@ func (g *APIHandler) getLogStatus(log webhookScanLog) string { return "Allowed" } -func (g *APIHandler) getLogReasoning(log webhookScanLog) string { +func (g *APIHandler) getLogReasoning(log dblogs.WebhookScanLog) string { // Reasoning: // - In case the request is denied (rejected), show the violations that cause the denial. // - Otherwise, if there are violations, show the full violations list was found @@ -221,7 +216,7 @@ func (g *APIHandler) getLogReasoning(log webhookScanLog) string { return string(encoded) } -func (g *APIHandler) getLogRequest(log webhookScanLog) string { +func (g *APIHandler) getLogRequest(log dblogs.WebhookScanLog) string { var review webhookDisplayedReview err := json.Unmarshal([]byte(log.Request), &review) diff --git a/pkg/http-server/webhook-scan.go b/pkg/http-server/webhook-scan.go index 1a82cce98..1b6dee3f4 100644 --- a/pkg/http-server/webhook-scan.go +++ b/pkg/http-server/webhook-scan.go @@ -24,6 +24,7 @@ import ( "time" admissionWebhook "github.com/accurics/terrascan/pkg/k8s/admission-webhook" + "github.com/accurics/terrascan/pkg/k8s/dblogs" "github.com/accurics/terrascan/pkg/results" "github.com/accurics/terrascan/pkg/runtime" "github.com/gorilla/mux" @@ -155,11 +156,11 @@ func (g *APIHandler) logWebhook(output runtime.Output, encodedViolationsSummary, _ := json.Marshal(output.Violations.ViolationStore) - logger := WebhookScanLogger{ - test: g.test, + logger := dblogs.WebhookScanLogger{ + Test: g.test, } - err := logger.log(webhookScanLog{ + err := logger.Log(dblogs.WebhookScanLog{ UID: uid, Request: string(bytesAdmissionReview), Allowed: allowed, diff --git a/pkg/http-server/webhook-scan_test.go b/pkg/http-server/webhook-scan_test.go index 2be8c015c..237ed5556 100644 --- a/pkg/http-server/webhook-scan_test.go +++ b/pkg/http-server/webhook-scan_test.go @@ -10,6 +10,7 @@ import ( "os" "testing" + "github.com/accurics/terrascan/pkg/k8s/dblogs" "github.com/gorilla/mux" v1 "k8s.io/api/admission/v1" ) @@ -178,10 +179,11 @@ func TestUWebhooks(t *testing.T) { return } defer jsonFile.Close() - logger := WebhookScanLogger{ - test: true, + + logger := dblogs.WebhookScanLogger{ + Test: true, } - defer logger.clearDbFilePath() + defer logger.ClearDbFilePath() byteValue, _ := ioutil.ReadAll(jsonFile) diff --git a/pkg/http-server/webhook-scan-logger.go b/pkg/k8s/dblogs/webhook-scan-logger.go similarity index 70% rename from pkg/http-server/webhook-scan-logger.go rename to pkg/k8s/dblogs/webhook-scan-logger.go index 02b14444c..421ddf2a4 100644 --- a/pkg/http-server/webhook-scan-logger.go +++ b/pkg/k8s/dblogs/webhook-scan-logger.go @@ -1,4 +1,20 @@ -package httpserver +/* + Copyright (C) 2020 Accurics, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package dblogs import ( "database/sql" @@ -6,14 +22,18 @@ import ( "time" "go.uber.org/zap" + + // importing sqlite driver + _ "github.com/mattn/go-sqlite3" ) // WebhookScanLogger handles the logic to push scan logs to db type WebhookScanLogger struct { - test bool + Test bool } -type webhookScanLog struct { +// WebhookScanLog database model for log records +type WebhookScanLog struct { UID string Request string Allowed bool @@ -25,7 +45,13 @@ type webhookScanLog struct { // The file name where the DB is stored. Currently we use an SQLite DB var dbFileName = "k8s-admission-review-logs.db" -func (g *WebhookScanLogger) log(webhookScanLog webhookScanLog) error { +// NewWebhookScanLogger returns a new WebhookScanLogger struct +func NewWebhookScanLogger() *WebhookScanLogger { + return &WebhookScanLogger{} +} + +// Log creates a new db record for the admission request +func (g *WebhookScanLogger) Log(WebhookScanLog WebhookScanLog) error { // Insert a new Log record to the DB db, err := g.getDbHandler() @@ -42,12 +68,12 @@ func (g *WebhookScanLogger) log(webhookScanLog webhookScanLog) error { zap.S().Errorf("failed preparing SQL statement. error: '%v'", err) return err } - _, err = statement.Exec(webhookScanLog.UID, - webhookScanLog.Request, - webhookScanLog.Allowed, - webhookScanLog.ViolationsSummary, - webhookScanLog.DeniableViolations, - webhookScanLog.CreatedAt) + _, err = statement.Exec(WebhookScanLog.UID, + WebhookScanLog.Request, + WebhookScanLog.Allowed, + WebhookScanLog.ViolationsSummary, + WebhookScanLog.DeniableViolations, + WebhookScanLog.CreatedAt) if err != nil { zap.S().Errorf("failed to insert a new log. error: '%v'", err) return err @@ -56,7 +82,8 @@ func (g *WebhookScanLogger) log(webhookScanLog webhookScanLog) error { return nil } -func (g *WebhookScanLogger) fetchLogs() ([]webhookScanLog, error) { +// FetchLogs retrieves all the logs from the database +func (g *WebhookScanLogger) FetchLogs() ([]WebhookScanLog, error) { // Fetch the entire logs in the DB, ordered by created_at DESC (the most updated will be at the top) db, err := g.getDbHandler() @@ -71,7 +98,7 @@ func (g *WebhookScanLogger) fetchLogs() ([]webhookScanLog, error) { return nil, err } - var result []webhookScanLog + var result []WebhookScanLog defer row.Close() for row.Next() { var id int @@ -83,7 +110,7 @@ func (g *WebhookScanLogger) fetchLogs() ([]webhookScanLog, error) { var createdAt time.Time row.Scan(&id, &uid, &request, &allowed, &violationsSummary, &deniableViolations, &createdAt) - result = append(result, webhookScanLog{ + result = append(result, WebhookScanLog{ UID: uid, Request: request, Allowed: allowed, @@ -96,7 +123,8 @@ func (g *WebhookScanLogger) fetchLogs() ([]webhookScanLog, error) { return result, nil } -func (g *WebhookScanLogger) fetchLogByID(logUID string) (*webhookScanLog, error) { +// FetchLogByID retreives a single record based on request ID from the database +func (g *WebhookScanLogger) FetchLogByID(logUID string) (*WebhookScanLog, error) { // Fetch a specific log by its request UID db, err := g.getDbHandler() @@ -122,7 +150,7 @@ func (g *WebhookScanLogger) fetchLogByID(logUID string) (*webhookScanLog, error) var createdAt time.Time row.Scan(&id, &uid, &request, &allowed, &violationsSummary, &deniableViolations, &createdAt) - return &webhookScanLog{ + return &WebhookScanLog{ UID: uid, Request: request, Allowed: allowed, @@ -132,7 +160,7 @@ func (g *WebhookScanLogger) fetchLogByID(logUID string) (*webhookScanLog, error) }, nil } - return &webhookScanLog{}, nil + return &WebhookScanLog{}, nil } func (g *WebhookScanLogger) initDBIfNeeded() error { @@ -184,14 +212,14 @@ func (g *WebhookScanLogger) getDbHandler() (*sql.DB, error) { } func (g *WebhookScanLogger) dbFilePath() string { - if g.test { + if g.Test { return "./" + dbFileName } // This is where the DB file should be located in the container (It is going to be saved in the host machine volume) return "/data/k8s-admission-review-logs.db" } -// Used for Tests only - clear the DB file after the tests are done -func (g *WebhookScanLogger) clearDbFilePath() { +// ClearDbFilePath used for Tests only - clear the DB file after the tests are done +func (g *WebhookScanLogger) ClearDbFilePath() { os.Remove(g.dbFilePath()) } diff --git a/pkg/k8s/dblogs/webhook-scan-logger_test.go b/pkg/k8s/dblogs/webhook-scan-logger_test.go new file mode 100644 index 000000000..7386801d2 --- /dev/null +++ b/pkg/k8s/dblogs/webhook-scan-logger_test.go @@ -0,0 +1,116 @@ +/* + Copyright (C) 2020 Accurics, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package dblogs + +import ( + "testing" + "time" +) + +func TestLogs(t *testing.T) { + + var logger = WebhookScanLogger{ + Test: true, + } + defer logger.ClearDbFilePath() + + // insert a new db record + var log = WebhookScanLog{ + UID: "myUID", + Request: "MyRequest", + CreatedAt: time.Now(), + Allowed: true, + DeniableViolations: "MyViolations", + ViolationsSummary: "ViolationsSummary", + } + + t.Run("initialize db", func(t *testing.T) { + + // no logs exist in db, should return 0 logs + fetchedLogs, err := logger.FetchLogs() + if len(fetchedLogs) > 0 { + t.Errorf("no logs should exist in db; got: '%v' logs", len(fetchedLogs)) + } + if err != nil { + t.Errorf("unexpected error: '%v'", err) + } + }) + + t.Run("insert db record", func(t *testing.T) { + if err := logger.Log(log); err != nil { + t.Errorf("unexpected error: '%v'", err) + } + }) + + // test case 3: fetch db record by log ID + myFetchLog, err := logger.FetchLogByID(log.UID) + if err != nil { + t.Errorf("unexpected error: '%v'", err) + } + + t.Run("one db record", func(t *testing.T) { + fetchedLogs, err := logger.FetchLogs() + if err != nil { + t.Errorf("unexpected error: '%v'", err) + } + if len(fetchedLogs) != 1 { + t.Errorf("db has one log, got: '%v' logs", len(fetchedLogs)) + } + }) + + t.Run("fetch record by id", func(t *testing.T) { + + if len(myFetchLog.UID) < 1 { + t.Errorf("Log with ID: '%v' is not returned by fetchLogByID", log.UID) + } + + if myFetchLog.UID != log.UID { + t.Errorf("Wrong UID. Expected '%v', Got: '%v'", log.UID, myFetchLog.UID) + + } + }) + + t.Run("verify allowed", func(t *testing.T) { + if myFetchLog.Allowed != log.Allowed { + t.Errorf("Wrong Allowed. Expected '%v', Got: '%v'", log.Allowed, myFetchLog.Allowed) + } + }) + + t.Run("verify violations summary", func(t *testing.T) { + if myFetchLog.ViolationsSummary != log.ViolationsSummary { + t.Errorf("Wrong ViolationsSummary. Expected '%v', Got: '%v'", log.ViolationsSummary, myFetchLog.ViolationsSummary) + } + }) + + t.Run("verify request", func(t *testing.T) { + if myFetchLog.Request != log.Request { + t.Errorf("Wrong Request. Expected '%v', Got: '%v'", log.Request, myFetchLog.Request) + } + }) + + t.Run("verify deniable violations", func(t *testing.T) { + if myFetchLog.DeniableViolations != log.DeniableViolations { + t.Errorf("Wrong DeniableViolations. Expected '%v', Got: '%v'", log.DeniableViolations, myFetchLog.DeniableViolations) + } + }) + + t.Run("verify timestamp", func(t *testing.T) { + if myFetchLog.CreatedAt.Unix() != log.CreatedAt.Unix() { + t.Errorf("Wrong CreatedAt. Expected '%v', Got: '%v'", log.CreatedAt, myFetchLog.CreatedAt) + } + }) +} From 66fc76ec2e06126aef8a952dd66dd58383fd24de Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Sat, 20 Mar 2021 20:11:45 +0530 Subject: [PATCH 21/38] refactor validate handler to move specific functionality into webhook package --- pkg/http-server/webhook-scan-logs.go | 7 +- pkg/http-server/webhook-scan.go | 91 +------------- pkg/k8s/admission-webhook/interface.go | 5 +- .../admission-webhook/validating-webhook.go | 116 ++++++++++++++++-- pkg/k8s/dblogs/webhook-scan-logger.go | 7 ++ 5 files changed, 124 insertions(+), 102 deletions(-) diff --git a/pkg/http-server/webhook-scan-logs.go b/pkg/http-server/webhook-scan-logs.go index 7bcfbf5d5..c7272b34a 100644 --- a/pkg/http-server/webhook-scan-logs.go +++ b/pkg/http-server/webhook-scan-logs.go @@ -88,7 +88,7 @@ func (g *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) { logsData = append(logsData, webhookDisplayedIndexScanLog{ CreatedAt: log.CreatedAt, Status: g.getLogStatus(log), - LogURL: g.getLogPath(r.Host, log.UID), + LogURL: logger.GetLogURL(r.Host, log.UID), Reasoning: g.getLogReasoning(log), Request: g.getLogRequest(log), }) @@ -143,11 +143,6 @@ func (g *APIHandler) getLogByUID(w http.ResponseWriter, r *http.Request) { t.Execute(w, displayedScanLog) } -func (g *APIHandler) getLogPath(host, logUID string) string { - // Use this as the link to show the a specific log - return fmt.Sprintf("https://%v/k8s/webhooks/logs/%v", host, logUID) -} - func (g *APIHandler) getLogStatus(log dblogs.WebhookScanLog) string { // Calculate a log status: // 1. !Allowed -> Rejected diff --git a/pkg/http-server/webhook-scan.go b/pkg/http-server/webhook-scan.go index 1b6dee3f4..c2beb121f 100644 --- a/pkg/http-server/webhook-scan.go +++ b/pkg/http-server/webhook-scan.go @@ -21,24 +21,18 @@ import ( "fmt" "io/ioutil" "net/http" - "time" admissionWebhook "github.com/accurics/terrascan/pkg/k8s/admission-webhook" - "github.com/accurics/terrascan/pkg/k8s/dblogs" - "github.com/accurics/terrascan/pkg/results" - "github.com/accurics/terrascan/pkg/runtime" "github.com/gorilla/mux" "go.uber.org/zap" v1 "k8s.io/api/admission/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // validateK8SWebhook handles the incoming validating admission webhook from kubernetes API server func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) { var ( - currentTime = time.Now() params = mux.Vars(r) apiKey = params["apiKey"] validatingWebhook = admissionWebhook.NewValidatingWebhook(g.configFile) @@ -79,56 +73,18 @@ func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) } // process the admission review request - output, allowed, denyViolations, err := validatingWebhook.ProcessWebhook(requestedAdmissionReview) - if err != nil { - if err == admissionWebhook.ErrEmptyAdmissionReview { - g.sendResponseAdmissionReview(w, requestedAdmissionReview, true, output, "") - return - } - apiErrorResponse(w, err.Error(), http.StatusInternalServerError) - return - } - - logPath := g.getLogPath(r.Host, string(requestedAdmissionReview.Request.UID)) - - // Log the request in the DB - err = g.logWebhook(output, string(requestedAdmissionReview.Request.UID), body, denyViolations, currentTime, allowed) - if err != nil { + admissionResponse, err := validatingWebhook.ProcessWebhook(requestedAdmissionReview, r.Host) + if err != nil && err != admissionWebhook.ErrEmptyAdmissionReview { apiErrorResponse(w, err.Error(), http.StatusInternalServerError) return } // Send the correct response according to the result - g.sendResponseAdmissionReview(w, requestedAdmissionReview, allowed, output, logPath) + g.sendResponseAdmissionReview(w, admissionResponse) } -func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, - requestedAdmissionReview v1.AdmissionReview, - allowed bool, - output runtime.Output, - logPath string) { - responseAdmissionReview := &v1.AdmissionReview{} - responseAdmissionReview.SetGroupVersionKind(requestedAdmissionReview.GroupVersionKind()) - - responseAdmissionReview.Response = &v1.AdmissionResponse{ - UID: requestedAdmissionReview.Request.UID, - Allowed: allowed, - } - - if output.Violations.ViolationStore != nil { - // Means we ran the engines and we have results - if allowed { - if len(output.Violations.ViolationStore.Violations) > 0 { - // In case there are no denial violations, just return the log URL as a warning - responseAdmissionReview.Response.Warnings = []string{logPath} - } - } else { - // In case the request was denied, return 403 and the log URL as an error message - responseAdmissionReview.Response.Result = &metav1.Status{Message: logPath, Code: 403} - } - } - - respBytes, err := json.Marshal(responseAdmissionReview) +func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, admissionResponse *v1.AdmissionReview) { + respBytes, err := json.Marshal(admissionResponse) if err != nil { msg := fmt.Sprintf("failed to serialize admission review response: %v", err) zap.S().Error(msg) @@ -138,40 +94,3 @@ func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, zap.S().Debugf("Response result: %+v", string(respBytes)) apiResponse(w, string(respBytes), http.StatusOK) } - -func (g *APIHandler) logWebhook(output runtime.Output, - uid string, - bytesAdmissionReview []byte, - denyViolations []results.Violation, - currentTime time.Time, - allowed bool) error { - var deniedViolationsEncoded string - - if len(denyViolations) < 1 { - deniedViolationsEncoded = "" - } else { - d, _ := json.Marshal(denyViolations) - deniedViolationsEncoded = string(d) - } - - encodedViolationsSummary, _ := json.Marshal(output.Violations.ViolationStore) - - logger := dblogs.WebhookScanLogger{ - Test: g.test, - } - - err := logger.Log(dblogs.WebhookScanLog{ - UID: uid, - Request: string(bytesAdmissionReview), - Allowed: allowed, - DeniableViolations: deniedViolationsEncoded, - ViolationsSummary: string(encodedViolationsSummary), - CreatedAt: currentTime, - }) - if err != nil { - zap.S().Error("error logging scan result: '%v'", err) - return err - } - - return nil -} diff --git a/pkg/k8s/admission-webhook/interface.go b/pkg/k8s/admission-webhook/interface.go index 3b66859c0..946308531 100644 --- a/pkg/k8s/admission-webhook/interface.go +++ b/pkg/k8s/admission-webhook/interface.go @@ -17,9 +17,8 @@ package admissionwebhook import ( - "github.com/accurics/terrascan/pkg/results" - "github.com/accurics/terrascan/pkg/runtime" admissionv1 "k8s.io/api/admission/v1" + v1 "k8s.io/api/admission/v1" ) // AdmissionWebhook interface needs to be implemented by all k8s admission @@ -35,5 +34,5 @@ type AdmissionWebhook interface { // ProcessWebhook processes the incoming AdmissionReview and creates // a AdmissionResponse - ProcessWebhook(review admissionv1.AdmissionReview) (runtime.Output, bool, []results.Violation, error) + ProcessWebhook(review admissionv1.AdmissionReview, serverURL string) (*v1.AdmissionReview, error) } diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index f00ea2058..84aa5ac98 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -17,15 +17,21 @@ package admissionwebhook import ( + "encoding/json" "fmt" "os" + "time" "github.com/accurics/terrascan/pkg/config" + "github.com/accurics/terrascan/pkg/k8s/dblogs" "github.com/accurics/terrascan/pkg/results" "github.com/accurics/terrascan/pkg/runtime" "github.com/accurics/terrascan/pkg/utils" "go.uber.org/zap" + admissionv1 "k8s.io/api/admission/v1" + v1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtimeK8s "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" ) @@ -34,12 +40,17 @@ import ( // the kubernetes API server and decides whether the admission request from // the kubernetes client should be allowed or not type ValidatingWebhook struct { - configFile string + configFile string + requestBody []byte + dblogger *dblogs.WebhookScanLogger } // NewValidatingWebhook returns a new, empty ValidatingWebhook struct func NewValidatingWebhook(configFile string) AdmissionWebhook { - return ValidatingWebhook{configFile: configFile} + return ValidatingWebhook{ + configFile: configFile, + dblogger: dblogs.NewWebhookScanLogger(), + } } var ( @@ -91,6 +102,7 @@ func (w ValidatingWebhook) DecodeAdmissionReviewRequest(requestBody []byte) (adm deserializer = codecs.UniversalDeserializer() requestedAdmissionReview admissionv1.AdmissionReview ) + w.requestBody = requestBody admissionv1.AddToScheme(scheme) // decode incoming admission request @@ -106,12 +118,19 @@ func (w ValidatingWebhook) DecodeAdmissionReviewRequest(requestBody []byte) (adm // ProcessWebhook processes the incoming AdmissionReview and creates // a response -func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) (output runtime.Output, allowed bool, denyViolations []results.Violation, err error) { +func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview, serverURL string) (*v1.AdmissionReview, error) { + + var ( + output runtime.Output + denyViolations []results.Violation + logURL = w.dblogger.GetLogURL(serverURL, string(review.Request.UID)) + allowed = false + ) // In case the object is nil => an operation of DELETE happened, just return 'allow' since there is nothing to check if len(review.Request.Object.Raw) < 1 { zap.S().Info(ErrEmptyAdmissionReview, zap.Any("admission review object", review)) - return output, true, denyViolations, ErrEmptyAdmissionReview + return w.createResponseAdmissionReview(review, true, output, logURL), ErrEmptyAdmissionReview } // Save the object into a temp file for the policy engines @@ -120,7 +139,7 @@ func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) (o if err != nil { msg := "failed to create temp file for validating admission review request" zap.S().Error(msg, zap.Error(err)) - return output, true, denyViolations, fmt.Errorf("%s; error: %w", msg, err) + return w.createResponseAdmissionReview(review, allowed, output, logURL), fmt.Errorf("%s; error: %w", msg, err) } // Run the policy engines @@ -128,14 +147,26 @@ func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview) (o if err != nil { msg := "failed to evaluate terrascan policies" zap.S().Errorf(msg, zap.Error(err)) - return output, allowed, denyViolations, fmt.Errorf("%s; error: %w", msg, err) + return w.createResponseAdmissionReview(review, allowed, output, logURL), fmt.Errorf("%s; error: %w", msg, err) } // Calculate if there are anydeny violations denyViolations, err = w.getDenyViolations(output) + if err != nil { + msg := "failed to figure out denied violations" + zap.S().Errorf(msg, zap.Error(err)) + return w.createResponseAdmissionReview(review, allowed, output, logURL), fmt.Errorf("%s; error: %w", msg, err) + } allowed = len(denyViolations) < 1 - return output, allowed, denyViolations, nil + // Log the request in the DB + err = w.logWebhook(output, string(review.Request.UID), denyViolations, allowed) + if err != nil { + msg := "failed to log validating admission review request into database" + zap.S().Error(msg, zap.Error(err)) + } + + return w.createResponseAdmissionReview(review, allowed, output, logURL), nil } func (w ValidatingWebhook) scanK8sFile(filePath string) (runtime.Output, error) { @@ -193,6 +224,77 @@ func (w ValidatingWebhook) getDeniedViolations(violations results.ViolationStore return denyViolations } +func (w ValidatingWebhook) logWebhook(output runtime.Output, + uid string, + denyViolations []results.Violation, + allowed bool) error { + + var ( + currentTime = time.Now() + deniedViolationsEncoded string + ) + + // encode denied violations into a string + if len(denyViolations) < 1 { + deniedViolationsEncoded = "" + } else { + d, _ := json.Marshal(denyViolations) + deniedViolationsEncoded = string(d) + } + + encodedViolationsSummary, _ := json.Marshal(output.Violations.ViolationStore) + + // insert the webhook log into db + err := w.dblogger.Log(dblogs.WebhookScanLog{ + UID: uid, + Request: string(w.requestBody), + Allowed: allowed, + DeniableViolations: deniedViolationsEncoded, + ViolationsSummary: string(encodedViolationsSummary), + CreatedAt: currentTime, + }) + if err != nil { + zap.S().Error("error logging scan result: '%v'", err) + return err + } + + return nil +} + +// createAdmissionResponse creates a admission review response which is sent +// to calling kubernetes API server +func (w ValidatingWebhook) createResponseAdmissionReview( + requestedAdmissionReview v1.AdmissionReview, + allowed bool, + output runtime.Output, + logPath string) *v1.AdmissionReview { + + // create an admission review request to be sent as response + responseAdmissionReview := &v1.AdmissionReview{} + responseAdmissionReview.SetGroupVersionKind(requestedAdmissionReview.GroupVersionKind()) + + // populate admission response + responseAdmissionReview.Response = &v1.AdmissionResponse{ + UID: requestedAdmissionReview.Request.UID, + Allowed: allowed, + } + + if output.Violations.ViolationStore != nil { + // Means we ran the engines and we have results + if allowed { + if len(output.Violations.ViolationStore.Violations) > 0 { + // In case there are no denial violations, just return the log URL as a warning + responseAdmissionReview.Response.Warnings = []string{logPath} + } + } else { + // In case the request was denied, return 403 and the log URL as an error message + responseAdmissionReview.Response.Result = &metav1.Status{Message: logPath, Code: 403} + } + } + + return responseAdmissionReview +} + type webhookDenyRuleMatcher struct { } diff --git a/pkg/k8s/dblogs/webhook-scan-logger.go b/pkg/k8s/dblogs/webhook-scan-logger.go index 421ddf2a4..3fc17fbcb 100644 --- a/pkg/k8s/dblogs/webhook-scan-logger.go +++ b/pkg/k8s/dblogs/webhook-scan-logger.go @@ -18,6 +18,7 @@ package dblogs import ( "database/sql" + "fmt" "os" "time" @@ -163,6 +164,12 @@ func (g *WebhookScanLogger) FetchLogByID(logUID string) (*WebhookScanLog, error) return &WebhookScanLog{}, nil } +// GetLogURL returns a url to the UI page for reviewing the validating admission request log +func (g *WebhookScanLogger) GetLogURL(host, logUID string) string { + // Use this as the link to show the a specific log + return fmt.Sprintf("https://%v/k8s/webhooks/logs/%v", host, logUID) +} + func (g *WebhookScanLogger) initDBIfNeeded() error { // Check where the SQL file exists. If it does do nothing. Otherwise, create the DB file and the Logs table. if _, err := os.Stat(g.dbFilePath()); os.IsNotExist(err) { From 6a1cbef6c7dfb01e34b94451b8e2666e16fafab9 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Sat, 20 Mar 2021 20:24:27 +0530 Subject: [PATCH 22/38] making staticcheck happy --- pkg/k8s/admission-webhook/interface.go | 3 +-- pkg/k8s/admission-webhook/validating-webhook.go | 11 +++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pkg/k8s/admission-webhook/interface.go b/pkg/k8s/admission-webhook/interface.go index 946308531..6cc31ab7a 100644 --- a/pkg/k8s/admission-webhook/interface.go +++ b/pkg/k8s/admission-webhook/interface.go @@ -18,7 +18,6 @@ package admissionwebhook import ( admissionv1 "k8s.io/api/admission/v1" - v1 "k8s.io/api/admission/v1" ) // AdmissionWebhook interface needs to be implemented by all k8s admission @@ -34,5 +33,5 @@ type AdmissionWebhook interface { // ProcessWebhook processes the incoming AdmissionReview and creates // a AdmissionResponse - ProcessWebhook(review admissionv1.AdmissionReview, serverURL string) (*v1.AdmissionReview, error) + ProcessWebhook(review admissionv1.AdmissionReview, serverURL string) (*admissionv1.AdmissionReview, error) } diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index 84aa5ac98..ff9857097 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -30,7 +30,6 @@ import ( "go.uber.org/zap" admissionv1 "k8s.io/api/admission/v1" - v1 "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtimeK8s "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" @@ -118,7 +117,7 @@ func (w ValidatingWebhook) DecodeAdmissionReviewRequest(requestBody []byte) (adm // ProcessWebhook processes the incoming AdmissionReview and creates // a response -func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview, serverURL string) (*v1.AdmissionReview, error) { +func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview, serverURL string) (*admissionv1.AdmissionReview, error) { var ( output runtime.Output @@ -264,17 +263,17 @@ func (w ValidatingWebhook) logWebhook(output runtime.Output, // createAdmissionResponse creates a admission review response which is sent // to calling kubernetes API server func (w ValidatingWebhook) createResponseAdmissionReview( - requestedAdmissionReview v1.AdmissionReview, + requestedAdmissionReview admissionv1.AdmissionReview, allowed bool, output runtime.Output, - logPath string) *v1.AdmissionReview { + logPath string) *admissionv1.AdmissionReview { // create an admission review request to be sent as response - responseAdmissionReview := &v1.AdmissionReview{} + responseAdmissionReview := &admissionv1.AdmissionReview{} responseAdmissionReview.SetGroupVersionKind(requestedAdmissionReview.GroupVersionKind()) // populate admission response - responseAdmissionReview.Response = &v1.AdmissionResponse{ + responseAdmissionReview.Response = &admissionv1.AdmissionResponse{ UID: requestedAdmissionReview.Request.UID, Allowed: allowed, } From 0f67bb1b3e1a67d61bbc99dec1be671d4b3ebe09 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Mon, 22 Mar 2021 19:17:37 +0530 Subject: [PATCH 23/38] improve error message send as part of kubernetes admission response --- pkg/k8s/admission-webhook/validating-webhook.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index ff9857097..a3ab4a899 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -268,6 +268,8 @@ func (w ValidatingWebhook) createResponseAdmissionReview( output runtime.Output, logPath string) *admissionv1.AdmissionReview { + errMsg := fmt.Sprintf("For more details please visit %q", logPath) + // create an admission review request to be sent as response responseAdmissionReview := &admissionv1.AdmissionReview{} responseAdmissionReview.SetGroupVersionKind(requestedAdmissionReview.GroupVersionKind()) @@ -283,11 +285,11 @@ func (w ValidatingWebhook) createResponseAdmissionReview( if allowed { if len(output.Violations.ViolationStore.Violations) > 0 { // In case there are no denial violations, just return the log URL as a warning - responseAdmissionReview.Response.Warnings = []string{logPath} + responseAdmissionReview.Response.Warnings = []string{errMsg} } } else { // In case the request was denied, return 403 and the log URL as an error message - responseAdmissionReview.Response.Result = &metav1.Status{Message: logPath, Code: 403} + responseAdmissionReview.Response.Result = &metav1.Status{Message: errMsg, Code: 403} } } From 09e55dc16b5f4a1c27b1729b986328e5fbdfd764 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Tue, 30 Mar 2021 22:45:32 +0530 Subject: [PATCH 24/38] fixing unit tests for http server --- pkg/http-server/webhook-scan_test.go | 3 ++- pkg/k8s/admission-webhook/validating-webhook.go | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/http-server/webhook-scan_test.go b/pkg/http-server/webhook-scan_test.go index 237ed5556..f3933e312 100644 --- a/pkg/http-server/webhook-scan_test.go +++ b/pkg/http-server/webhook-scan_test.go @@ -235,7 +235,8 @@ func TestUWebhooks(t *testing.T) { logPath = response.Response.Result.Message } - expectedLogPath := fmt.Sprintf("https://%v/k8s/webhooks/logs/705ab4f5-6393-11e8-b7cc-42010a800002", req.Host) + subLogPath := fmt.Sprintf("https://%v/k8s/webhooks/logs/705ab4f5-6393-11e8-b7cc-42010a800002", req.Host) + expectedLogPath := fmt.Sprintf("For more details please visit %q", subLogPath) if logPath != expectedLogPath { t.Errorf("Mismach Log path. Got: %v, expected: %v", logPath, expectedLogPath) diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index a3ab4a899..72e16f1a0 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -18,6 +18,7 @@ package admissionwebhook import ( "encoding/json" + "flag" "fmt" "os" "time" @@ -176,9 +177,13 @@ func (w ValidatingWebhook) scanK8sFile(filePath string) (runtime.Output, error) result runtime.Output ) - executor, err = runtime.NewExecutor("k8s", "v1", []string{"k8s"}, - filePath, "", w.configFile, []string{}, []string{}, []string{}, []string{}, "") - + if flag.Lookup("test.v") != nil { + executor, err = runtime.NewExecutor("k8s", "v1", []string{"k8s"}, + filePath, "", w.configFile, []string{"../policies/opa/rego/k8s"}, []string{}, []string{}, []string{}, "") + } else { + executor, err = runtime.NewExecutor("k8s", "v1", []string{"k8s"}, + filePath, "", w.configFile, []string{}, []string{}, []string{}, []string{}, "") + } if err != nil { zap.S().Errorf("failed to create runtime executer: '%v'", err) return result, err From ffad7ad4e134e005e6eed342ed79d681e331af5a Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Wed, 31 Mar 2021 09:36:38 +0530 Subject: [PATCH 25/38] go mod tidy --- go.mod | 3 +-- go.sum | 6 ++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 90c9c221b..e0ec479f9 100644 --- a/go.mod +++ b/go.mod @@ -32,8 +32,7 @@ require ( github.com/zclconf/go-cty v1.7.1 go.uber.org/zap v1.16.0 golang.org/x/mod v0.4.2 // indirect - golang.org/x/sys v0.0.0-20210324051608-47abb6519492 - golang.org/x/tools v0.1.0 // indirect + golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 gopkg.in/src-d/go-git.v4 v4.13.1 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b helm.sh/helm/v3 v3.4.0 diff --git a/go.sum b/go.sum index 1b9a34761..4ce737ac6 100644 --- a/go.sum +++ b/go.sum @@ -412,7 +412,9 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0 h1:pMen7vLs8nvgEYhywH3KDWJIJTeEr2ULsVWHWYHQyBs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -578,6 +580,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= @@ -854,6 +857,7 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -942,6 +946,7 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= @@ -1310,6 +1315,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= From ef752999d82f27208e4cf2b823e468e8e4f53502 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Thu, 1 Apr 2021 14:58:13 +0530 Subject: [PATCH 26/38] adding review comments --- pkg/cli/server.go | 14 +++++++------- pkg/http-server/webhook-scan-logs.go | 3 ++- pkg/http-server/webhook-scan.go | 1 + pkg/http-server/webhook-scan_test.go | 3 ++- pkg/k8s/admission-webhook/validating-webhook.go | 6 +++++- .../admission-webhook/validating-webhook_test.go | 7 ++++--- 6 files changed, 21 insertions(+), 13 deletions(-) diff --git a/pkg/cli/server.go b/pkg/cli/server.go index be6de0091..f7dcc5ca0 100644 --- a/pkg/cli/server.go +++ b/pkg/cli/server.go @@ -23,13 +23,13 @@ import ( var ( // Port at which API server will listen - Port string + port string // CertFile Certificate file path, required in order to enable secure HTTP server - CertFile string + certFile string // PrivateKeyFile Private key file path, required in order to enable secure HTTP server - PrivateKeyFile string + privateKeyFile string ) var serverCmd = &cobra.Command{ @@ -46,12 +46,12 @@ Run Terrascan as an API server that inspects incoming IaC (Infrastructure-as-Cod } func server(cmd *cobra.Command, args []string) { - httpserver.Start(Port, ConfigFile, CertFile, PrivateKeyFile) + httpserver.Start(port, ConfigFile, certFile, privateKeyFile) } func init() { - serverCmd.Flags().StringVarP(&PrivateKeyFile, "key-path", "", "", "private key file path") - serverCmd.Flags().StringVarP(&CertFile, "cert-path", "", "", "certificate file path") - serverCmd.Flags().StringVarP(&Port, "port", "p", httpserver.GatewayDefaultPort, "server port") + serverCmd.Flags().StringVarP(&privateKeyFile, "key-path", "", "", "private key file path") + serverCmd.Flags().StringVarP(&certFile, "cert-path", "", "", "certificate file path") + serverCmd.Flags().StringVarP(&port, "port", "p", httpserver.GatewayDefaultPort, "server port") RegisterCommand(rootCmd, serverCmd) } diff --git a/pkg/http-server/webhook-scan-logs.go b/pkg/http-server/webhook-scan-logs.go index c7272b34a..517dd0806 100644 --- a/pkg/http-server/webhook-scan-logs.go +++ b/pkg/http-server/webhook-scan-logs.go @@ -73,6 +73,7 @@ func (g *APIHandler) getLogs(w http.ResponseWriter, r *http.Request) { errMsg := fmt.Sprintf("failed to parse index.html file; error: '%v'", err) zap.S().Error(errMsg) apiErrorResponse(w, errMsg, http.StatusInternalServerError) + return } logs, err := logger.FetchLogs() @@ -154,7 +155,7 @@ func (g *APIHandler) getLogStatus(log dblogs.WebhookScanLog) string { var violationStore results.ViolationStore err := json.Unmarshal([]byte(log.ViolationsSummary), &violationStore) if err != nil { - zap.S().Errorf("Failed to ..") + zap.S().Errorf("failed to decode violation results", zap.Error(err)) } if len(violationStore.Violations) > 0 { diff --git a/pkg/http-server/webhook-scan.go b/pkg/http-server/webhook-scan.go index c2beb121f..30775fbc3 100644 --- a/pkg/http-server/webhook-scan.go +++ b/pkg/http-server/webhook-scan.go @@ -89,6 +89,7 @@ func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, admissio msg := fmt.Sprintf("failed to serialize admission review response: %v", err) zap.S().Error(msg) apiErrorResponse(w, msg, http.StatusInternalServerError) + return } zap.S().Debugf("Response result: %+v", string(respBytes)) diff --git a/pkg/http-server/webhook-scan_test.go b/pkg/http-server/webhook-scan_test.go index f3933e312..b13f53a0e 100644 --- a/pkg/http-server/webhook-scan_test.go +++ b/pkg/http-server/webhook-scan_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "testing" "github.com/accurics/terrascan/pkg/k8s/dblogs" @@ -16,7 +17,7 @@ import ( ) func TestUWebhooks(t *testing.T) { - testFilePath := "./k8s_testdata/testconfig.json" + testFilePath := filepath.Join("k8s_testdata", "testconfig.json") testAPIKey := "Test-API-KEY" testEnvAPIKey := "Test-API-KEY" testConfigFile := "" diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index 72e16f1a0..ea761e81f 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -21,6 +21,7 @@ import ( "flag" "fmt" "os" + "path/filepath" "time" "github.com/accurics/terrascan/pkg/config" @@ -65,6 +66,9 @@ var ( // ErrEmptyAdmissionReview empty admission review request ErrEmptyAdmissionReview = fmt.Errorf("empty admission review request") + + // test policies path + testPoliciesPath = filepath.Join("../", "policies", "opa", "rego", "k8s") ) // Authorize checks if the incoming webhooks have valid apiKey @@ -179,7 +183,7 @@ func (w ValidatingWebhook) scanK8sFile(filePath string) (runtime.Output, error) if flag.Lookup("test.v") != nil { executor, err = runtime.NewExecutor("k8s", "v1", []string{"k8s"}, - filePath, "", w.configFile, []string{"../policies/opa/rego/k8s"}, []string{}, []string{}, []string{}, "") + filePath, "", w.configFile, []string{testPoliciesPath}, []string{}, []string{}, []string{}, "") } else { executor, err = runtime.NewExecutor("k8s", "v1", []string{"k8s"}, filePath, "", w.configFile, []string{}, []string{}, []string{}, []string{}, "") diff --git a/pkg/k8s/admission-webhook/validating-webhook_test.go b/pkg/k8s/admission-webhook/validating-webhook_test.go index aa8482748..544e4da3c 100644 --- a/pkg/k8s/admission-webhook/validating-webhook_test.go +++ b/pkg/k8s/admission-webhook/validating-webhook_test.go @@ -19,6 +19,7 @@ package admissionwebhook import ( "io/ioutil" "os" + "path/filepath" "testing" ) @@ -85,17 +86,17 @@ func TestDecodeAdmissionReviewRequest(t *testing.T) { }{ { name: "empty review request", - requestFile: "testdata/empty.json", + requestFile: filepath.Join("testdata", "empty.json"), wantErr: false, }, { name: "invalid review request", - requestFile: "testdata/invalid.json", + requestFile: filepath.Join("testdata", "invalid.json"), wantErr: true, }, { name: "valid review request", - requestFile: "testdata/valid.json", + requestFile: filepath.Join("testdata", "valid.json"), wantErr: false, }, } From 6863010dd0978b08a63a30846204637c63998a27 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Thu, 1 Apr 2021 23:49:14 +0530 Subject: [PATCH 27/38] sonar linter fixes --- pkg/http-server/assets/jsonTree.js | 53 ++++++++++++++---------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/pkg/http-server/assets/jsonTree.js b/pkg/http-server/assets/jsonTree.js index 3de131a7d..738eaf795 100644 --- a/pkg/http-server/assets/jsonTree.js +++ b/pkg/http-server/assets/jsonTree.js @@ -99,7 +99,9 @@ var jsonTree = (function() { * @param Parent {Function} - a parent constructor */ inherits : (function() { - var F = function() {}; + var F = function() { + // This is intentional + }; return function(Child, Parent) { F.prototype = Parent.prototype; @@ -220,16 +222,13 @@ var jsonTree = (function() { var self = this, el = document.createElement('li'), labelEl, - template = function(label, val) { - var str = '\ - \ - "' + - label + - '" : \ - \ - \ - ' + - val + + template = function(datalabel, value) { + var str = ''+ + '"' + datalabel + + '" : ' + + '' + + '' + + value + '' + (!isLast ? ',' : '') + ''; @@ -440,26 +439,24 @@ var jsonTree = (function() { var self = this, el = document.createElement('li'), - template = function(label, sym) { + template = function(datalabel, sym) { var comma = (!isLast) ? ',' : '', - str = '\ -
          \ -
          \ - ' + sym[0] + '\ - \ -
            \ - ' + sym[1] + '' + + str = '
            ' + + '
            ' + + '' + sym[0] + '' + + '' + + '
              ' + + '' + sym[1] + '' + '
              ' + comma + '
              '; - if (label !== null) { - str = '\ - \ - ' + + if (datalabel !== null) { + str = '' + + '' + '' + - '"' + label + - '" : \ - ' + str; + '"' + datalabel + + '" : ' + + '' + str; } return str; @@ -513,8 +510,8 @@ var jsonTree = (function() { self.childNodes = childNodes; self.childNodesUl = childNodesUl; - utils.forEachNode(val, function(label, node, isLast) { - self.addChild(new Node(label, node, isLast)); + utils.forEachNode(val, function(datalabel, node, isLastNode) { + self.addChild(new Node(datalabel, node, isLastNode)); }); self.isEmpty = !Boolean(childNodes.length); From bdb11b55b857f421f9972fa10e7182b468c65802 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Fri, 2 Apr 2021 22:04:53 +0530 Subject: [PATCH 28/38] fixing code smells and bugs in UI --- pkg/http-server/assets/moment.js | 1040 ++++++++++--------- pkg/http-server/assets/webhook-scan-logs.js | 10 +- 2 files changed, 529 insertions(+), 521 deletions(-) diff --git a/pkg/http-server/assets/moment.js b/pkg/http-server/assets/moment.js index 43bb38035..aeae3bf89 100644 --- a/pkg/http-server/assets/moment.js +++ b/pkg/http-server/assets/moment.js @@ -4,10 +4,14 @@ //! license : MIT //! momentjs.com -;(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - global.moment = factory() +(function (global, factory) { + + if (typeof exports === 'object' && typeof module !== 'undefined') { + module.exports = factory(); + } + typeof define === 'function' && define.amd ? define(factory) : + global.moment = factory(); + }(this, (function () { 'use strict'; var hookCallback; @@ -101,8 +105,8 @@ return a; } - function createUTC(input, format, locale, strict) { - return createLocalOrUTC(input, format, locale, strict, true).utc(); + function createUTC(input, formatdata, localedata, strict) { + return createLocalOrUTC(input, formatdata, localedata, strict, true).utc(); } function defaultParsingFlags() { @@ -205,51 +209,51 @@ var momentProperties = (hooks.momentProperties = []), updateInProgress = false; - function copyConfig(to, from) { + function copyConfig(toVar, fromVar) { var i, prop, val; if (!isUndefined(from._isAMomentObject)) { - to._isAMomentObject = from._isAMomentObject; + toVar._isAMomentObject = fromVar._isAMomentObject; } - if (!isUndefined(from._i)) { - to._i = from._i; + if (!isUndefined(fromVar._i)) { + toVar._i = fromVar._i; } - if (!isUndefined(from._f)) { - to._f = from._f; + if (!isUndefined(fromVar._f)) { + toVar._f = fromVar._f; } - if (!isUndefined(from._l)) { - to._l = from._l; + if (!isUndefined(fromVar._l)) { + toVar._l = fromVar._l; } - if (!isUndefined(from._strict)) { - to._strict = from._strict; + if (!isUndefined(fromVar._strict)) { + toVar._strict = fromVar._strict; } - if (!isUndefined(from._tzm)) { - to._tzm = from._tzm; + if (!isUndefined(fromVar._tzm)) { + toVar._tzm = fromVar._tzm; } - if (!isUndefined(from._isUTC)) { - to._isUTC = from._isUTC; + if (!isUndefined(fromVar._isUTC)) { + toVar._isUTC = fromVar._isUTC; } - if (!isUndefined(from._offset)) { - to._offset = from._offset; + if (!isUndefined(fromVar._offset)) { + toVar._offset = fromVar._offset; } - if (!isUndefined(from._pf)) { - to._pf = getParsingFlags(from); + if (!isUndefined(fromVar._pf)) { + toVar._pf = getParsingFlags(fromVar); } - if (!isUndefined(from._locale)) { - to._locale = from._locale; + if (!isUndefined(fromVar._locale)) { + toVar._locale = fromVar._locale; } if (momentProperties.length > 0) { for (i = 0; i < momentProperties.length; i++) { prop = momentProperties[i]; - val = from[prop]; + val = fromVar[prop]; if (!isUndefined(val)) { to[prop] = val; } } } - return to; + return toVar; } // Moment prototype object @@ -361,7 +365,6 @@ this._config = config; // Lenient ordinal parsing accepts just a number in addition to // number + (possibly) stuff coming from _dayOfMonthOrdinalParse. - // TODO: Remove "ordinalParse" fallback in next major release. this._dayOfMonthOrdinalParseLenient = new RegExp( (this._dayOfMonthOrdinalParse.source || this._ordinalParse.source) + '|' + @@ -430,20 +433,22 @@ sameElse: 'L', }; - function calendar(key, mom, now) { + function calendar(key, mom, nowVar) { var output = this._calendar[key] || this._calendar['sameElse']; - return isFunction(output) ? output.call(mom, now) : output; + return isFunction(output) ? output.call(mom, nowVar) : output; } function zeroFill(number, targetLength, forceSign) { var absNumber = '' + Math.abs(number), zerosToFill = targetLength - absNumber.length, - sign = number >= 0; - return ( - (sign ? (forceSign ? '+' : '') : '-') + - Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + - absNumber - ); + signVar = number >= 0; + if (signVar) { + return forceSign ? '+' : ''; + } else { + return '-' + + Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + + absNumber; + } } var formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g, @@ -455,26 +460,26 @@ // padded: ['MM', 2] // ordinal: 'Mo' // callback: function () { this.month() + 1 } - function addFormatToken(token, padded, ordinal, callback) { + function addFormatToken(tokenVar, padded, ordinalVar, callback) { var func = callback; if (typeof callback === 'string') { func = function () { return this[callback](); }; } - if (token) { - formatTokenFunctions[token] = func; + if (tokenVar) { + formatTokenFunctions[tokenVar] = func; } if (padded) { formatTokenFunctions[padded[0]] = function () { return zeroFill(func.apply(this, arguments), padded[1], padded[2]); }; } - if (ordinal) { - formatTokenFunctions[ordinal] = function () { + if (ordinalVar) { + formatTokenFunctions[ordinalVar] = function () { return this.localeData().ordinal( func.apply(this, arguments), - token + tokenVar ); }; } @@ -487,16 +492,16 @@ return input.replace(/\\/g, ''); } - function makeFormatFunction(format) { - var array = format.match(formattingTokens), + function makeFormatFunction(formatVar) { + var array = formatVar.match(formattingTokens), i, length; - for (i = 0, length = array.length; i < length; i++) { + for (var j = 0, lengthArray = array.length; j < lengthArray; j++) { if (formatTokenFunctions[array[i]]) { - array[i] = formatTokenFunctions[array[i]]; + array[j] = formatTokenFunctions[array[i]]; } else { - array[i] = removeFormattingTokens(array[i]); + array[j] = removeFormattingTokens(array[i]); } } @@ -505,7 +510,7 @@ i; for (i = 0; i < length; i++) { output += isFunction(array[i]) - ? array[i].call(mom, format) + ? array[i].call(mom, formatVar) : array[i]; } return output; @@ -513,28 +518,28 @@ } // format date using native date object - function formatMoment(m, format) { + function formatMoment(m, formatVar) { if (!m.isValid()) { return m.localeData().invalidDate(); } - format = expandFormat(format, m.localeData()); - formatFunctions[format] = - formatFunctions[format] || makeFormatFunction(format); + formatVar = expandFormat(formatVar, m.localeData()); + formatFunctions[formatVar] = + formatFunctions[formatVar] || makeFormatFunction(formatVar); - return formatFunctions[format](m); + return formatFunctions[formatVar](m); } - function expandFormat(format, locale) { + function expandFormat(formatVar, localeVar) { var i = 5; function replaceLongDateFormatTokens(input) { - return locale.longDateFormat(input) || input; + return localeVar.longDateFormat(input) || input; } localFormattingTokens.lastIndex = 0; - while (i >= 0 && localFormattingTokens.test(format)) { - format = format.replace( + while (i >= 0 && localFormattingTokens.test(formatVar)) { + formatVar = formatVar.replace( localFormattingTokens, replaceLongDateFormatTokens ); @@ -542,7 +547,7 @@ i -= 1; } - return format; + return formatVar; } var defaultLongDateFormat = { @@ -555,11 +560,11 @@ }; function longDateFormat(key) { - var format = this._longDateFormat[key], + var formatVar = this._longDateFormat[key], formatUpper = this._longDateFormat[key.toUpperCase()]; - if (format || !formatUpper) { - return format; + if (formatVar || !formatUpper) { + return formatVar; } this._longDateFormat[key] = formatUpper @@ -619,9 +624,9 @@ : output.replace(/%d/i, number); } - function pastFuture(diff, output) { - var format = this._relativeTime[diff > 0 ? 'future' : 'past']; - return isFunction(format) ? format(output) : format.replace(/%s/i, output); + function pastFuture(diffVar, output) { + var formatVar = this._relativeTime[diffVar > 0 ? 'future' : 'past']; + return isFunction(formatVar) ? format(output) : format.replace(/%s/i, output); } var aliases = {}; @@ -711,9 +716,11 @@ } function get(mom, unit) { - return mom.isValid() - ? mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]() - : NaN; + if (mom.isValid()) { + return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); + } else { + return NaN; + } } function set$1(mom, unit, value) { @@ -786,17 +793,17 @@ regexes = {}; - function addRegexToken(token, regex, strictRegex) { - regexes[token] = isFunction(regex) + function addRegexToken(tokenVar, regex, strictRegex) { + regexes[tokenVar] = isFunction(regex) ? regex - : function (isStrict, localeData) { + : function (isStrict, localeDataVar) { return isStrict && strictRegex ? strictRegex : regex; }; } - function getParseRegexForToken(token, config) { - if (!hasOwnProp(regexes, token)) { - return new RegExp(unescapeFormat(token)); + function getParseRegexForToken(tokenVar, config) { + if (!hasOwnProp(regexes, tokenVar)) { + return new RegExp(unescapeFormat(tokenVar)); } return regexes[token](config._strict, config._locale); @@ -825,32 +832,32 @@ var tokens = {}; - function addParseToken(token, callback) { + function addParseToken(tokenVar, callback) { var i, func = callback; - if (typeof token === 'string') { - token = [token]; + if (typeof tokenVar === 'string') { + tokenVar = [tokenVar]; } if (isNumber(callback)) { func = function (input, array) { array[callback] = toInt(input); }; } - for (i = 0; i < token.length; i++) { - tokens[token[i]] = func; + for (i = 0; i < tokenVar.length; i++) { + tokens[tokenVar[i]] = func; } } - function addWeekParseToken(token, callback) { - addParseToken(token, function (input, array, config, token) { + function addWeekParseToken(tokenVar, callback) { + addParseToken(tokenVar, function (input, array, config, tokenVar_var) { config._w = config._w || {}; - callback(input, config._w, config, token); + callback(input, config._w, config, tokenVar_var); }); } - function addTimeToArrayFromToken(token, input, config) { - if (input != null && hasOwnProp(tokens, token)) { - tokens[token](input, config._a, config, token); + function addTimeToArrayFromToken(tokenVar, input, config) { + if (input != null && hasOwnProp(tokens, tokenVar)) { + tokens[tokenVar](input, config._a, config, tokenVar); } } @@ -891,11 +898,11 @@ } var modMonth = mod(month, 12); year += (month - modMonth) / 12; - return modMonth === 1 - ? isLeapYear(year) - ? 29 - : 28 - : 31 - ((modMonth % 7) % 2); + if (modMonth === 1) { + return isLeapYear(year) ? 29 : 28; + } else { + return 31 - ((modMonth % 7) % 2); + } } // FORMATTING @@ -904,12 +911,12 @@ return this.month() + 1; }); - addFormatToken('MMM', 0, 0, function (format) { - return this.localeData().monthsShort(this, format); + addFormatToken('MMM', 0, 0, function (formatVar) { + return this.localeData().monthsShort(this, formatVar); }); - addFormatToken('MMMM', 0, 0, function (format) { - return this.localeData().months(this, format); + addFormatToken('MMMM', 0, 0, function (formatVar) { + return this.localeData().months(this, formatVar); }); // ALIASES @@ -924,19 +931,19 @@ addRegexToken('M', match1to2); addRegexToken('MM', match1to2, match2); - addRegexToken('MMM', function (isStrict, locale) { - return locale.monthsShortRegex(isStrict); + addRegexToken('MMM', function (isStrict, localeVar) { + return localeVar.monthsShortRegex(isStrict); }); - addRegexToken('MMMM', function (isStrict, locale) { - return locale.monthsRegex(isStrict); + addRegexToken('MMMM', function (isStrict, localeVar) { + return localeVar.monthsRegex(isStrict); }); addParseToken(['M', 'MM'], function (input, array) { array[MONTH] = toInt(input) - 1; }); - addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { - var month = config._locale.monthsParse(input, token, config._strict); + addParseToken(['MMM', 'MMMM'], function (input, array, config, tokenVar) { + var month = config._locale.monthsParse(input, tokenVar, config._strict); // if we didn't find a month name, mark the date as invalid. if (month != null) { array[MONTH] = month; @@ -957,35 +964,40 @@ defaultMonthsShortRegex = matchWord, defaultMonthsRegex = matchWord; - function localeMonths(m, format) { + function localeMonths(m, formatVar) { if (!m) { return isArray(this._months) ? this._months : this._months['standalone']; } - return isArray(this._months) - ? this._months[m.month()] - : this._months[ - (this._months.isFormat || MONTHS_IN_FORMAT).test(format) + + if (isArray(this._months)) { + return this._months[m.month()]; + } else { + return this._months[ + (this._months.isFormat || MONTHS_IN_FORMAT).test(formatVar) ? 'format' : 'standalone' ][m.month()]; + } } - function localeMonthsShort(m, format) { + function localeMonthsShort(m, formatVar) { if (!m) { return isArray(this._monthsShort) ? this._monthsShort : this._monthsShort['standalone']; } - return isArray(this._monthsShort) - ? this._monthsShort[m.month()] - : this._monthsShort[ - MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone' + if (isArray(this._monthsShort)) { + return this._monthsShort[m.month()]; + } else { + return this._monthsShort[ + MONTHS_IN_FORMAT.test(formatVar) ? 'format' : 'standalone' ][m.month()]; + } } - function handleStrictParse(monthName, format, strict) { + function handleStrictParse(monthName, formatVar, strict) { var i, ii, mom, @@ -1006,7 +1018,7 @@ } if (strict) { - if (format === 'MMM') { + if (formatVar === 'MMM') { ii = indexOf.call(this._shortMonthsParse, llc); return ii !== -1 ? ii : null; } else { @@ -1014,7 +1026,7 @@ return ii !== -1 ? ii : null; } } else { - if (format === 'MMM') { + if (formatVar === 'MMM') { ii = indexOf.call(this._shortMonthsParse, llc); if (ii !== -1) { return ii; @@ -1032,11 +1044,11 @@ } } - function localeMonthsParse(monthName, format, strict) { + function localeMonthsParse(monthName, formatVar, strict) { var i, mom, regex; if (this._monthsParseExact) { - return handleStrictParse.call(this, monthName, format, strict); + return handleStrictParse.call(this, monthName, formatVar, strict); } if (!this._monthsParse) { @@ -1045,7 +1057,6 @@ this._shortMonthsParse = []; } - // TODO: add sorting // Sorting makes sure if one month (or abbr) is a prefix of another // see sorting in computeMonthsParse for (i = 0; i < 12; i++) { @@ -1073,14 +1084,6 @@ this._longMonthsParse[i].test(monthName) ) { return i; - } else if ( - strict && - format === 'MMM' && - this._shortMonthsParse[i].test(monthName) - ) { - return i; - } else if (!strict && this._monthsParse[i].test(monthName)) { - return i; } } } @@ -1100,7 +1103,6 @@ value = toInt(value); } else { value = mom.localeData().monthsParse(value); - // TODO: Another silent failure? if (!isNumber(value)) { return mom; } @@ -1398,9 +1400,9 @@ input, week, config, - token + tokenVar ) { - week[token.substr(0, 1)] = toInt(input); + week[tokenVar.substr(0, 1)] = toInt(input); }); // HELPERS @@ -1440,16 +1442,16 @@ addFormatToken('d', 0, 'do', 'day'); - addFormatToken('dd', 0, 0, function (format) { - return this.localeData().weekdaysMin(this, format); + addFormatToken('dd', 0, 0, function (formatVar) { + return this.localeData().weekdaysMin(this, formatVar); }); - addFormatToken('ddd', 0, 0, function (format) { - return this.localeData().weekdaysShort(this, format); + addFormatToken('ddd', 0, 0, function (formatVar) { + return this.localeData().weekdaysShort(this, formatVar); }); - addFormatToken('dddd', 0, 0, function (format) { - return this.localeData().weekdays(this, format); + addFormatToken('dddd', 0, 0, function (formatVar) { + return this.localeData().weekdays(this, formatVar); }); addFormatToken('e', 0, 0, 'weekday'); @@ -1471,18 +1473,18 @@ addRegexToken('d', match1to2); addRegexToken('e', match1to2); addRegexToken('E', match1to2); - addRegexToken('dd', function (isStrict, locale) { - return locale.weekdaysMinRegex(isStrict); + addRegexToken('dd', function (isStrict, localeVar) { + return localeVar.weekdaysMinRegex(isStrict); }); - addRegexToken('ddd', function (isStrict, locale) { - return locale.weekdaysShortRegex(isStrict); + addRegexToken('ddd', function (isStrict, localeVar) { + return localeVar.weekdaysShortRegex(isStrict); }); - addRegexToken('dddd', function (isStrict, locale) { - return locale.weekdaysRegex(isStrict); + addRegexToken('dddd', function (isStrict, localeVar) { + return localeVar.weekdaysRegex(isStrict); }); - addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config, token) { - var weekday = config._locale.weekdaysParse(input, token, config._strict); + addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config, tokenVar) { + var weekday = config._locale.weekdaysParse(input, tokenVar, config._strict); // if we didn't get a weekday name, mark the date as invalid if (weekday != null) { week.d = weekday; @@ -1491,13 +1493,13 @@ } }); - addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { - week[token] = toInt(input); + addWeekParseToken(['d', 'e', 'E'], function (input, week, config, tokenVar) { + week[tokenVar] = toInt(input); }); // HELPERS - function parseWeekday(input, locale) { + function parseWeekday(input, localeVar) { if (typeof input !== 'string') { return input; } @@ -1506,7 +1508,7 @@ return parseInt(input, 10); } - input = locale.weekdaysParse(input); + input = localeVar.weekdaysParse(input); if (typeof input === 'number') { return input; } @@ -1514,9 +1516,9 @@ return null; } - function parseIsoWeekday(input, locale) { + function parseIsoWeekday(input, localeVar) { if (typeof input === 'string') { - return locale.weekdaysParse(input) % 7 || 7; + return localeVar.weekdaysParse(input) % 7 || 7; } return isNaN(input) ? null : input; } @@ -1535,38 +1537,40 @@ defaultWeekdaysShortRegex = matchWord, defaultWeekdaysMinRegex = matchWord; - function localeWeekdays(m, format) { - var weekdays = isArray(this._weekdays) - ? this._weekdays - : this._weekdays[ - m && m !== true && this._weekdays.isFormat.test(format) + function localeWeekdays(m, formatVar) { + if (isArray(this._weekdays)) { + var weekdays = this._weekdays; + } else { + weekdays = this._weekdays[ + m && m !== true && this._weekdays.isFormat.test(formatVar) ? 'format' : 'standalone' ]; - return m === true - ? shiftWeekdays(weekdays, this._week.dow) - : m - ? weekdays[m.day()] - : weekdays; + } + if (m) { + return shiftWeekdays(weekdays, this._week.dow); + } else { + return m ? weekdays[m.day()] : weekdays; + } } function localeWeekdaysShort(m) { - return m === true - ? shiftWeekdays(this._weekdaysShort, this._week.dow) - : m - ? this._weekdaysShort[m.day()] - : this._weekdaysShort; + if (m) { + return shiftWeekdays(this._weekdaysShort, this._week.dow); + } else { + return m ? this._weekdaysShort[m.day()] : this._weekdaysShort; + } } function localeWeekdaysMin(m) { - return m === true - ? shiftWeekdays(this._weekdaysMin, this._week.dow) - : m - ? this._weekdaysMin[m.day()] - : this._weekdaysMin; + if (m) { + return shiftWeekdays(this._weekdaysMin, this._week.dow); + } else { + return m ? this._weekdaysMin[m.day()] : this._weekdaysMin; + } } - function handleStrictParse$1(weekdayName, format, strict) { + function handleStrictParse$1(weekdayName, formatVar, strict) { var i, ii, mom, @@ -1591,10 +1595,10 @@ } if (strict) { - if (format === 'dddd') { + if (formatVar === 'dddd') { ii = indexOf.call(this._weekdaysParse, llc); return ii !== -1 ? ii : null; - } else if (format === 'ddd') { + } else if (formatVar === 'ddd') { ii = indexOf.call(this._shortWeekdaysParse, llc); return ii !== -1 ? ii : null; } else { @@ -1602,7 +1606,7 @@ return ii !== -1 ? ii : null; } } else { - if (format === 'dddd') { + if (formatVar === 'dddd') { ii = indexOf.call(this._weekdaysParse, llc); if (ii !== -1) { return ii; @@ -1613,7 +1617,7 @@ } ii = indexOf.call(this._minWeekdaysParse, llc); return ii !== -1 ? ii : null; - } else if (format === 'ddd') { + } else if (formatVar === 'ddd') { ii = indexOf.call(this._shortWeekdaysParse, llc); if (ii !== -1) { return ii; @@ -1639,11 +1643,11 @@ } } - function localeWeekdaysParse(weekdayName, format, strict) { + function localeWeekdaysParse(weekdayName, formatVar, strict) { var i, mom, regex; if (this._weekdaysParseExact) { - return handleStrictParse$1.call(this, weekdayName, format, strict); + return handleStrictParse$1.call(this, weekdayName, formatVar, strict); } if (!this._weekdaysParse) { @@ -1684,23 +1688,26 @@ // test the regex if ( strict && - format === 'dddd' && + formatVar === 'dddd' && this._fullWeekdaysParse[i].test(weekdayName) ) { return i; - } else if ( + } + if ( strict && format === 'ddd' && this._shortWeekdaysParse[i].test(weekdayName) ) { return i; - } else if ( + } + if ( strict && format === 'dd' && this._minWeekdaysParse[i].test(weekdayName) ) { return i; - } else if (!strict && this._weekdaysParse[i].test(weekdayName)) { + } + if (!strict && this._weekdaysParse[i].test(weekdayName)) { return i; } } @@ -1898,8 +1905,8 @@ ); }); - function meridiem(token, lowercase) { - addFormatToken(token, 0, 0, function () { + function meridiem(tokenVar, lowercase) { + addFormatToken(tokenVar, 0, 0, function () { return this.localeData().meridiem( this.hours(), this.minutes(), @@ -1920,8 +1927,8 @@ // PARSING - function matchMeridiem(isStrict, locale) { - return locale._meridiemParse; + function matchMeridiem(isStrict, localeVar) { + return localeVar._meridiemParse; } addRegexToken('a', matchMeridiem); @@ -1993,8 +2000,8 @@ // this rule. getSetHour = makeGetSet('Hours', true); - function localeMeridiem(hours, minutes, isLower) { - if (hours > 11) { + function localeMeridiem(hoursVar, minutesVar, isLower) { + if (hoursVar > 11) { return isLower ? 'pm' : 'PM'; } else { return isLower ? 'am' : 'AM'; @@ -2048,7 +2055,7 @@ var i = 0, j, next, - locale, + localeVar, split; while (i < names.length) { @@ -2057,9 +2064,9 @@ next = normalizeLocale(names[i + 1]); next = next ? next.split('-') : null; while (j > 0) { - locale = loadLocale(split.slice(0, j).join('-')); - if (locale) { - return locale; + localeVar = loadLocale(split.slice(0, j).join('-')); + if (localeVar) { + return localeVar; } if ( next && @@ -2079,7 +2086,6 @@ function loadLocale(name) { var oldLocale = null, aliasedRequire; - // TODO: Find a better way to register and load all the locales in Node if ( locales[name] === undefined && typeof module !== 'undefined' && @@ -2113,7 +2119,6 @@ } if (data) { - // moment.duration._locale = moment._locale = data; globalLocale = data; } else { if (typeof console !== 'undefined' && console.warn) { @@ -2130,7 +2135,7 @@ function defineLocale(name, config) { if (config !== null) { - var locale, + var localeVar, parentConfig = baseConfig; config.abbr = name; if (locales[name] != null) { @@ -2146,9 +2151,9 @@ if (locales[config.parentLocale] != null) { parentConfig = locales[config.parentLocale]._config; } else { - locale = loadLocale(config.parentLocale); - if (locale != null) { - parentConfig = locale._config; + localeVar = loadLocale(config.parentLocale); + if (localeVar != null) { + parentConfig = localeVar._config; } else { if (!localeFamilies[config.parentLocale]) { localeFamilies[config.parentLocale] = []; @@ -2184,7 +2189,7 @@ function updateLocale(name, config) { if (config != null) { - var locale, + var localeVar, tmpLocale, parentConfig = baseConfig; @@ -2204,9 +2209,9 @@ // undefined otherwise). config.abbr = name; } - locale = new Locale(config); - locale.parentLocale = locales[name]; - locales[name] = locale; + localeVar = new Locale(config); + localeVar.parentLocale = locales[name]; + locales[name] = localeVar; } // backwards compat for now: also set the locale @@ -2229,7 +2234,7 @@ // returns locale data function getLocale(key) { - var locale; + var localeVar; if (key && key._locale && key._locale._abbr) { key = key._locale._abbr; @@ -2241,9 +2246,9 @@ if (!isArray(key)) { //short-circuit everything else - locale = loadLocale(key); - if (locale) { - return locale; + localeVar = loadLocale(key); + if (localeVar) { + return localeVar; } key = [key]; } @@ -2448,7 +2453,6 @@ function checkWeekday(weekdayStr, parsedInput, config) { if (weekdayStr) { - // TODO: Replace the vanilla JS Date object with an independent day-of-week check. var weekdayProvided = defaultLocaleWeekdaysShort.indexOf(weekdayStr), weekdayActual = new Date( parsedInput[0], @@ -2620,10 +2624,12 @@ // Zero out whatever was not defaulted, including time for (; i < 7; i++) { - config._a[i] = input[i] = - config._a[i] == null ? (i === 2 ? 1 : 0) : config._a[i]; + if (config._a[i] == null) { + config._a[i] = input[i] = i === 2 ? 1 : 0; + } else { + config._a[i] = input[i] = config._a[i]; + } } - // Check for 24:00:00.000 if ( config._a[HOUR] === 24 && @@ -2671,7 +2677,6 @@ dow = 1; doy = 4; - // TODO: We need to take the current isoWeekYear, but that depends on // how we interpret now (local, utc, fixed offset). So create // a now version of current config (take local/utc/offset flags, and // create now). @@ -2725,14 +2730,17 @@ } // constant that refers to the ISO standard - hooks.ISO_8601 = function () {}; + hooks.ISO_8601 = function () { + // This is intentional + }; // constant that refers to the RFC 2822 form - hooks.RFC_2822 = function () {}; + hooks.RFC_2822 = function () { + // This is intentional + }; // date from string and format string function configFromStringAndFormat(config) { - // TODO: Move this to another part of the creation flow to prevent circular deps if (config._f === hooks.ISO_8601) { configFromISO(config); return; @@ -2748,19 +2756,19 @@ var string = '' + config._i, i, parsedInput, - tokens, - token, + tokensVar, + tokenVar, skipped, stringLength = string.length, totalParsedInputLength = 0, era; - tokens = + tokensVar = expandFormat(config._f, config._locale).match(formattingTokens) || []; - for (i = 0; i < tokens.length; i++) { - token = tokens[i]; - parsedInput = (string.match(getParseRegexForToken(token, config)) || + for (i = 0; i < tokensVar.length; i++) { + tokenVar = tokensVar[i]; + parsedInput = (string.match(getParseRegexForToken(tokenVar, config)) || [])[0]; if (parsedInput) { skipped = string.substr(0, string.indexOf(parsedInput)); @@ -2773,15 +2781,15 @@ totalParsedInputLength += parsedInput.length; } // don't parse if it's not a known token - if (formatTokenFunctions[token]) { + if (formatTokenFunctions[tokenVar]) { if (parsedInput) { getParsingFlags(config).empty = false; } else { - getParsingFlags(config).unusedTokens.push(token); + getParsingFlags(config).unusedTokens.push(tokenVar); } - addTimeToArrayFromToken(token, parsedInput, config); + addTimeToArrayFromToken(tokenVar, parsedInput, config); } else if (config._strict && !parsedInput) { - getParsingFlags(config).unusedTokens.push(token); + getParsingFlags(config).unusedTokens.push(tokenVar); } } @@ -2820,18 +2828,18 @@ checkOverflow(config); } - function meridiemFixWrap(locale, hour, meridiem) { + function meridiemFixWrap(localeVar, hour, meridiemVar) { var isPm; - if (meridiem == null) { + if (meridiemVar == null) { // nothing to do return hour; } - if (locale.meridiemHour != null) { - return locale.meridiemHour(hour, meridiem); - } else if (locale.isPM != null) { + if (localeVar.meridiemHour != null) { + return localeVar.meridiemHour(hour, meridiemVar); + } else if (localeVar.isPM != null) { // Fallback - isPm = locale.isPM(meridiem); + isPm = localeVar.isPM(meridiemVar); if (isPm && hour < 12) { hour += 12; } @@ -2936,11 +2944,11 @@ function prepareConfig(config) { var input = config._i, - format = config._f; + formatVar = config._f; config._locale = config._locale || getLocale(config._l); - if (input === null || (format === undefined && input === '')) { + if (input === null || (formatVar === undefined && input === '')) { return createInvalid({ nullInput: true }); } @@ -2952,9 +2960,9 @@ return new Moment(checkOverflow(input)); } else if (isDate(input)) { config._d = input; - } else if (isArray(format)) { + } else if (isArray(formatVar)) { configFromStringAndArray(config); - } else if (format) { + } else if (formatVar) { configFromStringAndFormat(config); } else { configFromInput(config); @@ -2990,17 +2998,17 @@ } } - function createLocalOrUTC(input, format, locale, strict, isUTC) { + function createLocalOrUTC(input, formatVar, localeVar, strict, isUTC) { var c = {}; - if (format === true || format === false) { - strict = format; - format = undefined; + if (formatVar === true || formatVar === false) { + strict = formatVar; + formatVar = undefined; } - if (locale === true || locale === false) { - strict = locale; - locale = undefined; + if (localeVar === true || localeVar === false) { + strict = localeVar; + localeVar = undefined; } if ( @@ -3013,16 +3021,16 @@ // https://github.com/moment/moment/issues/1423 c._isAMomentObject = true; c._useUTC = c._isUTC = isUTC; - c._l = locale; + c._l = localeVar; c._i = input; - c._f = format; + c._f = formatVar; c._strict = strict; return createFromConfig(c); } - function createLocal(input, format, locale, strict) { - return createLocalOrUTC(input, format, locale, strict, false); + function createLocal(input, formatVar, localeVar, strict) { + return createLocalOrUTC(input, formatVar, localeVar, strict, false); } var prototypeMin = deprecate( @@ -3070,7 +3078,6 @@ return res; } - // TODO: Use [].sort instead? function min() { var args = [].slice.call(arguments, 0); @@ -3139,31 +3146,31 @@ function Duration(duration) { var normalizedInput = normalizeObjectUnits(duration), - years = normalizedInput.year || 0, + yearsVar = normalizedInput.year || 0, quarters = normalizedInput.quarter || 0, - months = normalizedInput.month || 0, - weeks = normalizedInput.week || normalizedInput.isoWeek || 0, - days = normalizedInput.day || 0, - hours = normalizedInput.hour || 0, - minutes = normalizedInput.minute || 0, - seconds = normalizedInput.second || 0, - milliseconds = normalizedInput.millisecond || 0; + monthsVar = normalizedInput.month || 0, + weeksVar = normalizedInput.week || normalizedInput.isoWeek || 0, + daysVar = normalizedInput.day || 0, + hoursVar = normalizedInput.hour || 0, + minutesVar = normalizedInput.minute || 0, + secondsVar = normalizedInput.second || 0, + millisecondsVar = normalizedInput.millisecond || 0; this._isValid = isDurationValid(normalizedInput); // representation for dateAddRemove this._milliseconds = - +milliseconds + - seconds * 1e3 + // 1000 - minutes * 6e4 + // 1000 * 60 - hours * 1000 * 60 * 60; //using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978 + +millisecondsVar + + secondsVar * 1e3 + // 1000 + minutesVar * 6e4 + // 1000 * 60 + hoursVar * 1000 * 60 * 60; //using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978 // Because of dateAddRemove treats 24 hours as different from a // day when working around DST, we need to store them separately - this._days = +days + weeks * 7; + this._days = +daysVar + weeksVar * 7; // It is impossible to translate months into days without knowing // which months you are are talking about, so we have to store // it separately. - this._months = +months + quarters * 3 + years * 12; + this._months = +monthsVar + quarters * 3 + yearsVar * 12; this._data = {}; @@ -3203,19 +3210,19 @@ // FORMATTING - function offset(token, separator) { - addFormatToken(token, 0, 0, function () { - var offset = this.utcOffset(), - sign = '+'; - if (offset < 0) { - offset = -offset; - sign = '-'; + function offset(tokenVar, separator) { + addFormatToken(tokenVar, 0, 0, function () { + var offsetVar = this.utcOffset(), + signVar = '+'; + if (offsetVar < 0) { + offsetVar = -offsetVar; + signVar = '-'; } return ( - sign + - zeroFill(~~(offset / 60), 2) + + signVar + + zeroFill(~~(offsetVar / 60), 2) + separator + - zeroFill(~~offset % 60, 2) + zeroFill(~~offsetVar % 60, 2) ); }); } @@ -3243,7 +3250,7 @@ var matches = (string || '').match(matcher), chunk, parts, - minutes; + minutesVar; if (matches === null) { return null; @@ -3251,22 +3258,26 @@ chunk = matches[matches.length - 1] || []; parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; - minutes = +(parts[1] * 60) + toInt(parts[2]); + minutesVar = +(parts[1] * 60) + toInt(parts[2]); - return minutes === 0 ? 0 : parts[0] === '+' ? minutes : -minutes; + if (minutesVar === 0) { + return 0; + } else { + return parts[0] === '+' ? minutesVar : -minutesVar; + } } // Return a moment from input, that is local/utc/zone equivalent to model. function cloneWithOffset(input, model) { - var res, diff; + var res, diffVar; if (model._isUTC) { res = model.clone(); - diff = + diffVar = (isMoment(input) || isDate(input) ? input.valueOf() : createLocal(input).valueOf()) - res.valueOf(); // Use low-level api, because this fn is low-level api. - res._d.setTime(res._d.valueOf() + diff); + res._d.setTime(res._d.valueOf() + diffVar); hooks.updateOffset(res, false); return res; } else { @@ -3284,7 +3295,9 @@ // This function will be called whenever a moment is mutated. // It is intended to keep the offset in sync with the timezone. - hooks.updateOffset = function () {}; + hooks.updateOffset = function () { + // This is intentional + }; // MOMENTS @@ -3299,7 +3312,7 @@ // _changeInProgress == true case, then we have to adjust, because // there is no such time in the given timezone. function getSetOffset(input, keepLocalTime, keepMinutes) { - var offset = this._offset || 0, + var offsetVar = this._offset || 0, localAdjust; if (!this.isValid()) { return input != null ? this : NaN; @@ -3321,11 +3334,11 @@ if (localAdjust != null) { this.add(localAdjust, 'm'); } - if (offset !== input) { + if (offsetVar !== input) { if (!keepLocalTime || this._changeInProgress) { addSubtract( this, - createDuration(input - offset, 'm'), + createDuration(input - offsetVar, 'm'), 1, false ); @@ -3337,7 +3350,7 @@ } return this; } else { - return this._isUTC ? offset : getDateOffset(this); + return this._isUTC ? offsetVar : getDateOffset(this); } } @@ -3446,7 +3459,7 @@ var duration = input, // matching against regexp is expensive, do it on demand match = null, - sign, + signVar, ret, diffRes; @@ -3464,25 +3477,25 @@ duration.milliseconds = +input; } } else if ((match = aspNetRegex.exec(input))) { - sign = match[1] === '-' ? -1 : 1; + signVar = match[1] === '-' ? -1 : 1; duration = { y: 0, - d: toInt(match[DATE]) * sign, - h: toInt(match[HOUR]) * sign, - m: toInt(match[MINUTE]) * sign, - s: toInt(match[SECOND]) * sign, - ms: toInt(absRound(match[MILLISECOND] * 1000)) * sign, // the millisecond decimal point is included in the match + d: toInt(match[DATE]) * signVar, + h: toInt(match[HOUR]) * signVar, + m: toInt(match[MINUTE]) * signVar, + s: toInt(match[SECOND]) * signVar, + ms: toInt(absRound(match[MILLISECOND] * 1000)) * signVar, // the millisecond decimal point is included in the match }; } else if ((match = isoRegex.exec(input))) { - sign = match[1] === '-' ? -1 : 1; + signVar = match[1] === '-' ? -1 : 1; duration = { - y: parseIso(match[2], sign), - M: parseIso(match[3], sign), - w: parseIso(match[4], sign), - d: parseIso(match[5], sign), - h: parseIso(match[6], sign), - m: parseIso(match[7], sign), - s: parseIso(match[8], sign), + y: parseIso(match[2], signVar), + M: parseIso(match[3], signVar), + w: parseIso(match[4], signVar), + d: parseIso(match[5], signVar), + h: parseIso(match[6], signVar), + m: parseIso(match[7], signVar), + s: parseIso(match[8], signVar), }; } else if (duration == null) { // checks for null or undefined @@ -3517,13 +3530,13 @@ createDuration.fn = Duration.prototype; createDuration.invalid = createInvalid$1; - function parseIso(inp, sign) { + function parseIso(inp, signVar) { // We'd normally use ~~inp for this, but unfortunately it also // converts floats to ints. // inp may be undefined, so careful calling replace on it. var res = inp && parseFloat(inp.replace(',', '.')); // apply sign while we're at it - return (isNaN(res) ? 0 : res) * sign; + return (isNaN(res) ? 0 : res) * signVar; } function positiveMomentsDifference(base, other) { @@ -3558,7 +3571,6 @@ return res; } - // TODO: remove 'name' arg after deprecation is removed function createAdder(direction, name) { return function (val, period) { var dur, tmp; @@ -3585,9 +3597,9 @@ } function addSubtract(mom, duration, isAdding, updateOffset) { - var milliseconds = duration._milliseconds, - days = absRound(duration._days), - months = absRound(duration._months); + var millisecondsVar = duration._milliseconds, + daysVar = absRound(duration._days), + monthsVar = absRound(duration._months); if (!mom.isValid()) { // No op @@ -3596,17 +3608,17 @@ updateOffset = updateOffset == null ? true : updateOffset; - if (months) { - setMonth(mom, get(mom, 'Month') + months * isAdding); + if (monthsVar) { + setMonth(mom, get(mom, 'Month') + monthsVar * isAdding); } - if (days) { - set$1(mom, 'Date', get(mom, 'Date') + days * isAdding); + if (daysVar) { + set$1(mom, 'Date', get(mom, 'Date') + daysVar * isAdding); } - if (milliseconds) { - mom._d.setTime(mom._d.valueOf() + milliseconds * isAdding); + if (millisecondsVar) { + mom._d.setTime(mom._d.valueOf() + millisecondsVar * isAdding); } if (updateOffset) { - hooks.updateOffset(mom, days || months); + hooks.updateOffset(mom, daysVar || monthsVar); } } @@ -3617,7 +3629,6 @@ return typeof input === 'string' || input instanceof String; } - // type MomentInput = Moment | Date | string | number | (number | string)[] | MomentInputObject | void; // null | undefined function isMomentInput(input) { return ( isMoment(input) || @@ -3705,21 +3716,23 @@ return objectTest && propertyTest; } - function getCalendarFormat(myMoment, now) { - var diff = myMoment.diff(now, 'days', true); - return diff < -6 - ? 'sameElse' - : diff < -1 - ? 'lastWeek' - : diff < 0 - ? 'lastDay' - : diff < 1 - ? 'sameDay' - : diff < 2 - ? 'nextDay' - : diff < 7 - ? 'nextWeek' - : 'sameElse'; + function getCalendarFormat(myMoment, nowVar) { + var diffVar = myMoment.diff(now, 'days', true); + if (diffVar < -6) { + return 'sameElse'; + } else if (diffVar < -1) { + return 'lastWeek'; + } else if (diffVar < 0) { + return 'lastDay'; + } else if (diffVar < 1) { + return 'sameDay'; + } else if (diffVar < 2) { + return 'nextDay'; + } else if (diffVar < 7) { + return 'nextWeek'; + } else { + return 'sameElse'; + } } function calendar$1(time, formats) { @@ -3738,17 +3751,17 @@ } // We want to compare the start of today, vs this. // Getting start-of-today depends on whether we're local/utc/offset or not. - var now = time || createLocal(), - sod = cloneWithOffset(now, this).startOf('day'), - format = hooks.calendarFormat(this, sod) || 'sameElse', + var nowVar = time || createLocal(), + sod = cloneWithOffset(nowVar, this).startOf('day'), + formatVar = hooks.calendarFormat(this, sod) || 'sameElse', output = formats && - (isFunction(formats[format]) - ? formats[format].call(this, now) - : formats[format]); + (isFunction(formats[formatVar]) + ? formats[formatVar].call(this, nowVar) + : formats[formatVar]); return this.format( - output || this.localeData().calendar(format, this, createLocal(now)) + output || this.localeData().calendar(format, this, createLocal(nowVar)) ); } @@ -3782,9 +3795,9 @@ } } - function isBetween(from, to, units, inclusivity) { - var localFrom = isMoment(from) ? from : createLocal(from), - localTo = isMoment(to) ? to : createLocal(to); + function isBetween(fromVar, toVar, units, inclusivity) { + var localFrom = isMoment(fromVar) ? fromVar : createLocal(fromVar), + localTo = isMoment(toVar) ? toVar : createLocal(toVar); if (!(this.isValid() && localFrom.isValid() && localTo.isValid())) { return false; } @@ -4290,9 +4303,9 @@ input, array, config, - token + tokenVar ) { - var era = config._locale.erasParse(input, token, config._strict); + var era = config._locale.erasParse(input, tokenVar, config._strict); if (era) { getParsingFlags(config).era = era; } else { @@ -4307,7 +4320,7 @@ addRegexToken('yo', matchEraYearOrdinal); addParseToken(['y', 'yy', 'yyy', 'yyyy'], YEAR); - addParseToken(['yo'], function (input, array, config, token) { + addParseToken(['yo'], function (input, array, config, tokenVar) { var match; if (config._locale._eraYearOrdinalRegex) { match = input.match(config._locale._eraYearOrdinalRegex); @@ -4320,35 +4333,30 @@ } }); - function localeEras(m, format) { + function localeEras(m, formatVar) { var i, l, date, eras = this._eras || getLocale('en')._eras; for (i = 0, l = eras.length; i < l; ++i) { - switch (typeof eras[i].since) { - case 'string': - // truncate time - date = hooks(eras[i].since).startOf('day'); - eras[i].since = date.valueOf(); - break; + if (typeof eras[i].since === 'string') { + // truncate time + date = hooks(eras[i].since).startOf('day'); + eras[i].since = date.valueOf(); } - switch (typeof eras[i].until) { - case 'undefined': + if (typeof eras[i].until === 'undefined') { eras[i].until = +Infinity; - break; - case 'string': - // truncate time - date = hooks(eras[i].until).startOf('day').valueOf(); - eras[i].until = date.valueOf(); - break; - } + } else if (typeof eras[i].until === 'string') { + // truncate time + date = hooks(eras[i].until).startOf('day').valueOf(); + eras[i].until = date.valueOf(); + } } return eras; } - function localeErasParse(eraName, format, strict) { + function localeErasParse(eraName, formatVar, strict) { var i, l, eras = this.eras(), @@ -4363,7 +4371,7 @@ narrow = eras[i].narrow.toUpperCase(); if (strict) { - switch (format) { + switch (formatVar) { case 'N': case 'NN': case 'NNN': @@ -4506,20 +4514,20 @@ return isStrict ? this._erasNarrowRegex : this._erasRegex; } - function matchEraAbbr(isStrict, locale) { - return locale.erasAbbrRegex(isStrict); + function matchEraAbbr(isStrict, localeVar) { + return localeVar.erasAbbrRegex(isStrict); } - function matchEraName(isStrict, locale) { - return locale.erasNameRegex(isStrict); + function matchEraName(isStrict, localeVar) { + return localeVar.erasNameRegex(isStrict); } - function matchEraNarrow(isStrict, locale) { - return locale.erasNarrowRegex(isStrict); + function matchEraNarrow(isStrict, localeVar) { + return localeVar.erasNarrowRegex(isStrict); } - function matchEraYearOrdinal(isStrict, locale) { - return locale._eraYearOrdinalRegex || matchUnsigned; + function matchEraYearOrdinal(isStrict, localeVar) { + return localeVar._eraYearOrdinalRegex || matchUnsigned; } function computeErasParse() { @@ -4560,8 +4568,8 @@ return this.isoWeekYear() % 100; }); - function addWeekYearFormatToken(token, getter) { - addFormatToken(0, [token, token.length], 0, getter); + function addWeekYearFormatToken(tokenVar, getter) { + addFormatToken(0, [tokenVar, tokenVar.length], 0, getter); } addWeekYearFormatToken('gggg', 'weekYear'); @@ -4594,13 +4602,13 @@ input, week, config, - token + tokenVar ) { - week[token.substr(0, 2)] = toInt(input); + week[tokenVar.substr(0, 2)] = toInt(input); }); - addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { - week[token] = hooks.parseTwoDigitYear(input); + addWeekParseToken(['gg', 'GG'], function (input, week, config, tokenVar) { + week[tokenVar] = hooks.parseTwoDigitYear(input); }); // MOMENTS @@ -4710,11 +4718,10 @@ addRegexToken('D', match1to2); addRegexToken('DD', match1to2, match2); - addRegexToken('Do', function (isStrict, locale) { - // TODO: Remove "ordinalParse" fallback in next major release. + addRegexToken('Do', function (isStrict, localeVar) { return isStrict - ? locale._dayOfMonthOrdinalParse || locale._ordinalParse - : locale._dayOfMonthOrdinalParseLenient; + ? localeVar._dayOfMonthOrdinalParse || localeVar._ordinalParse + : localeVar._dayOfMonthOrdinalParseLenient; }); addParseToken(['D', 'DD'], DATE); @@ -5030,28 +5037,28 @@ proto$1.isPM = localeIsPM; proto$1.meridiem = localeMeridiem; - function get$1(format, index, field, setter) { - var locale = getLocale(), + function get$1(formatVar, index, field, setter) { + var localeVar = getLocale(), utc = createUTC().set(setter, index); - return locale[field](utc, format); + return localeVar[field](utc, formatVar); } - function listMonthsImpl(format, index, field) { - if (isNumber(format)) { - index = format; - format = undefined; + function listMonthsImpl(formatVar, index, field) { + if (isNumber(formatVar)) { + index = formatVar; + formatVar = undefined; } - format = format || ''; + formatVar = formatVar || ''; if (index != null) { - return get$1(format, index, field, 'month'); + return get$1(formatVar, index, field, 'month'); } var i, out = []; for (i = 0; i < 12; i++) { - out[i] = get$1(format, i, field, 'month'); + out[i] = get$1(formatVar, i, field, 'month'); } return out; } @@ -5064,34 +5071,34 @@ // (true, 5) // (true, fmt, 5) // (true, fmt) - function listWeekdaysImpl(localeSorted, format, index, field) { + function listWeekdaysImpl(localeSorted, formatVar, index, field) { if (typeof localeSorted === 'boolean') { - if (isNumber(format)) { - index = format; - format = undefined; + if (isNumber(formatVar)) { + index = formatVar; + formatVar = undefined; } - format = format || ''; + formatVar = formatVar || ''; } else { - format = localeSorted; - index = format; + formatVar = localeSorted; + index = formatVar; localeSorted = false; - if (isNumber(format)) { - index = format; - format = undefined; + if (isNumber(formatVar)) { + index = formatVar; + formatVar = undefined; } - format = format || ''; + formatVar = formatVar || ''; } - var locale = getLocale(), - shift = localeSorted ? locale._week.dow : 0, + var localeVar = getLocale(), + shift = localeSorted ? localeVar._week.dow : 0, i, out = []; if (index != null) { - return get$1(format, (index + shift) % 7, field, 'day'); + return get$1(formatVar, (index + shift) % 7, field, 'day'); } for (i = 0; i < 7; i++) { @@ -5100,24 +5107,24 @@ return out; } - function listMonths(format, index) { - return listMonthsImpl(format, index, 'months'); + function listMonths(formatVar, index) { + return listMonthsImpl(formatVar, index, 'months'); } - function listMonthsShort(format, index) { - return listMonthsImpl(format, index, 'monthsShort'); + function listMonthsShort(formatVar, index) { + return listMonthsImpl(formatVar, index, 'monthsShort'); } - function listWeekdays(localeSorted, format, index) { - return listWeekdaysImpl(localeSorted, format, index, 'weekdays'); + function listWeekdays(localeSorted, formatVar, index) { + return listWeekdaysImpl(localeSorted, formatVar, index, 'weekdays'); } - function listWeekdaysShort(localeSorted, format, index) { - return listWeekdaysImpl(localeSorted, format, index, 'weekdaysShort'); + function listWeekdaysShort(localeSorted, formatVar, index) { + return listWeekdaysImpl(localeSorted, formatVar, index, 'weekdaysShort'); } - function listWeekdaysMin(localeSorted, format, index) { - return listWeekdaysImpl(localeSorted, format, index, 'weekdaysMin'); + function listWeekdaysMin(localeSorted, formatVar, index) { + return listWeekdaysImpl(localeSorted, formatVar, index, 'weekdaysMin'); } getSetGlobalLocale('en', { @@ -5141,17 +5148,19 @@ ], dayOfMonthOrdinalParse: /\d{1,2}(th|st|nd|rd)/, ordinal: function (number) { + var output = ''; var b = number % 10, - output = - toInt((number % 100) / 10) === 1 - ? 'th' - : b === 1 - ? 'st' - : b === 2 - ? 'nd' - : b === 3 - ? 'rd' - : 'th'; + if (toInt((number % 100) / 10) === 1) { + output = 'th'; + } else if (b === 1) { + output = 'st'; + } else if (b === 2) { + output = 'nd'; + } else if (b === 3) { + output = 'rd'; + } else { + output = 'th'; + } return number + output; }, }); @@ -5215,116 +5224,115 @@ } function bubble() { - var milliseconds = this._milliseconds, - days = this._days, - months = this._months, + var millisecondsVar = this._milliseconds, + daysVar = this._days, + monthsVar = this._months, data = this._data, - seconds, - minutes, - hours, - years, + secondsVar, + minutesVar, + hoursVar, + yearsVar, monthsFromDays; // if we have a mix of positive and negative values, bubble down first // check: https://github.com/moment/moment/issues/2166 if ( !( - (milliseconds >= 0 && days >= 0 && months >= 0) || - (milliseconds <= 0 && days <= 0 && months <= 0) + (millisecondsVar >= 0 && daysVar >= 0 && monthsVar >= 0) || + (millisecondsVar <= 0 && daysVar <= 0 && monthsVar <= 0) ) ) { - milliseconds += absCeil(monthsToDays(months) + days) * 864e5; - days = 0; - months = 0; + millisecondsVar += absCeil(monthsToDays(monthsVar) + daysVar) * 864e5; + daysVar = 0; + monthsVar = 0; } // The following code bubbles up values, see the tests for // examples of what that means. - data.milliseconds = milliseconds % 1000; + data.milliseconds = millisecondsVar % 1000; - seconds = absFloor(milliseconds / 1000); - data.seconds = seconds % 60; + seconds = absFloor(millisecondsVar / 1000); + data.seconds = secondsVar % 60; - minutes = absFloor(seconds / 60); - data.minutes = minutes % 60; + minutesVar = absFloor(secondsVar / 60); + data.minutes = minutesVar % 60; - hours = absFloor(minutes / 60); - data.hours = hours % 24; + hoursVar = absFloor(minutesVar / 60); + data.hours = hoursVar % 24; - days += absFloor(hours / 24); + daysVar += absFloor(hours / 24); // convert days to months - monthsFromDays = absFloor(daysToMonths(days)); - months += monthsFromDays; - days -= absCeil(monthsToDays(monthsFromDays)); + monthsFromDays = absFloor(daysToMonths(daysVar)); + monthsVar += monthsFromDays; + daysVar -= absCeil(monthsToDays(monthsFromDays)); // 12 months -> 1 year - years = absFloor(months / 12); - months %= 12; + yearsVar = absFloor(monthsVar / 12); + monthsVar %= 12; - data.days = days; - data.months = months; - data.years = years; + data.days = daysVar; + data.months = monthsVar; + data.years = yearsVar; return this; } - function daysToMonths(days) { + function daysToMonths(daysVar) { // 400 years have 146097 days (taking into account leap year rules) // 400 years have 12 months === 4800 - return (days * 4800) / 146097; + return (daysVar * 4800) / 146097; } - function monthsToDays(months) { + function monthsToDays(monthsVar) { // the reverse of daysToMonths - return (months * 146097) / 4800; + return (monthsVar * 146097) / 4800; } function as(units) { if (!this.isValid()) { return NaN; } - var days, - months, - milliseconds = this._milliseconds; + var daysVar, + monthsVar, + millisecondsVar = this._milliseconds; units = normalizeUnits(units); if (units === 'month' || units === 'quarter' || units === 'year') { - days = this._days + milliseconds / 864e5; - months = this._months + daysToMonths(days); + daysVar = this._days + millisecondsVar / 864e5; + monthsVar = this._months + daysToMonths(daysVar); switch (units) { case 'month': - return months; + return monthsVar; case 'quarter': - return months / 3; + return monthsVar / 3; case 'year': - return months / 12; + return monthsVar / 12; } } else { // handle milliseconds separately because of floating point math errors (issue #1867) - days = this._days + Math.round(monthsToDays(this._months)); + daysVar = this._days + Math.round(monthsToDays(this._months)); switch (units) { case 'week': - return days / 7 + milliseconds / 6048e5; + return daysVar / 7 + millisecondsVar / 6048e5; case 'day': - return days + milliseconds / 864e5; + return daysVar + millisecondsVar / 864e5; case 'hour': - return days * 24 + milliseconds / 36e5; + return daysVar * 24 + millisecondsVar / 36e5; case 'minute': - return days * 1440 + milliseconds / 6e4; + return daysVar * 1440 + millisecondsVar / 6e4; case 'second': - return days * 86400 + milliseconds / 1000; + return daysVar * 86400 + millisecondsVar / 1000; // Math.floor prevents floating point math errors here case 'millisecond': - return Math.floor(days * 864e5) + milliseconds; + return Math.floor(daysVar * 864e5) + millisecondsVar; default: throw new Error('Unknown unit ' + units); } } } - // TODO: Use this.as('ms')? function valueOf$1() { if (!this.isValid()) { return NaN; @@ -5392,43 +5400,43 @@ }; // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize - function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { - return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + function substituteTimeAgo(string, number, withoutSuffix, isFuture, localeVar) { + return localeVar.relativeTime(number || 1, !!withoutSuffix, string, isFuture); } - function relativeTime$1(posNegDuration, withoutSuffix, thresholds, locale) { + function relativeTime$1(posNegDuration, withoutSuffix, thresholdsVar, localeVar) { var duration = createDuration(posNegDuration).abs(), - seconds = round(duration.as('s')), - minutes = round(duration.as('m')), - hours = round(duration.as('h')), - days = round(duration.as('d')), - months = round(duration.as('M')), - weeks = round(duration.as('w')), - years = round(duration.as('y')), + secondsVar = round(duration.as('s')), + minutesVar = round(duration.as('m')), + hoursVar = round(duration.as('h')), + daysVar = round(duration.as('d')), + monthsVar = round(duration.as('M')), + weeksVar= round(duration.as('w')), + yearsVar = round(duration.as('y')), a = - (seconds <= thresholds.ss && ['s', seconds]) || - (seconds < thresholds.s && ['ss', seconds]) || - (minutes <= 1 && ['m']) || - (minutes < thresholds.m && ['mm', minutes]) || - (hours <= 1 && ['h']) || - (hours < thresholds.h && ['hh', hours]) || - (days <= 1 && ['d']) || - (days < thresholds.d && ['dd', days]); - - if (thresholds.w != null) { + (secondsVar <= thresholdsVar.ss && ['s', secondsVar]) || + (secondsVar < thresholdsVar.s && ['ss', secondsVar]) || + (minutesVar <= 1 && ['m']) || + (minutesVar < thresholdsVar.m && ['mm', minutesVar]) || + (hoursVar <= 1 && ['h']) || + (hoursVar < thresholdsVar.h && ['hh', hoursVar]) || + (daysVar <= 1 && ['d']) || + (daysVar < thresholdsVar.d && ['dd', daysVar]); + + if (thresholdsVar.w != null) { a = a || - (weeks <= 1 && ['w']) || - (weeks < thresholds.w && ['ww', weeks]); + (weeksVar <= 1 && ['w']) || + (weeksVar < thresholdsVar.w && ['ww', weeksVar]); } a = a || - (months <= 1 && ['M']) || - (months < thresholds.M && ['MM', months]) || - (years <= 1 && ['y']) || ['yy', years]; + (monthsVar <= 1 && ['M']) || + (monthsVar < thresholdsVar.M && ['MM', monthsVar]) || + (yearsVar <= 1 && ['y']) || ['yy', yearsVar]; a[2] = withoutSuffix; a[3] = +posNegDuration > 0; - a[4] = locale; + a[4] = localeVar; return substituteTimeAgo.apply(null, a); } @@ -5466,7 +5474,7 @@ var withSuffix = false, th = thresholds, - locale, + localeVar, output; if (typeof argWithSuffix === 'object') { @@ -5483,8 +5491,8 @@ } } - locale = this.localeData(); - output = relativeTime$1(this, !withSuffix, th, locale); + localeVar = this.localeData(); + output = relativeTime$1(this, !withSuffix, th, localeVar); if (withSuffix) { output = locale.pastFuture(+this, output); @@ -5511,12 +5519,12 @@ return this.localeData().invalidDate(); } - var seconds = abs$1(this._milliseconds) / 1000, - days = abs$1(this._days), - months = abs$1(this._months), - minutes, - hours, - years, + var secondsVar = abs$1(this._milliseconds) / 1000, + daysVar = abs$1(this._days), + monthsVar = abs$1(this._months), + minutesVar, + hoursVar, + yearsVar, s, total = this.asSeconds(), totalSign, @@ -5531,17 +5539,17 @@ } // 3600 seconds -> 60 minutes -> 1 hour - minutes = absFloor(seconds / 60); - hours = absFloor(minutes / 60); - seconds %= 60; - minutes %= 60; + minutesVar = absFloor(secondsVar / 60); + hoursVar = absFloor(minutesVar / 60); + secondsVar %= 60; + minutesVar %= 60; // 12 months -> 1 year - years = absFloor(months / 12); - months %= 12; + yearsVar = absFloor(monthsVar / 12); + monthsVar %= 12; // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js - s = seconds ? seconds.toFixed(3).replace(/\.?0+$/, '') : ''; + s = secondsVar ? secondsVar.toFixed(3).replace(/\.?0+$/, '') : ''; totalSign = total < 0 ? '-' : ''; ymSign = sign(this._months) !== sign(total) ? '-' : ''; @@ -5551,13 +5559,13 @@ return ( totalSign + 'P' + - (years ? ymSign + years + 'Y' : '') + - (months ? ymSign + months + 'M' : '') + - (days ? daysSign + days + 'D' : '') + - (hours || minutes || seconds ? 'T' : '') + - (hours ? hmsSign + hours + 'H' : '') + - (minutes ? hmsSign + minutes + 'M' : '') + - (seconds ? hmsSign + s + 'S' : '') + (yearsVar ? ymSign + yearsVar + 'Y' : '') + + (monthsVar ? ymSign + monthsVar + 'M' : '') + + (daysVar ? daysSign + daysVar + 'D' : '') + + (hoursVar || minutesVar || secondsVar ? 'T' : '') + + (hoursVar ? hmsSign + hoursVar + 'H' : '') + + (minutesVar ? hmsSign + minutesVar + 'M' : '') + + (secondsVar ? hmsSign + s + 'S' : '') ); } @@ -5583,12 +5591,12 @@ proto$2.get = get$2; proto$2.milliseconds = milliseconds; proto$2.seconds = seconds; - proto$2.minutes = minutes; - proto$2.hours = hours; - proto$2.days = days; + proto$2.minutes = minutesVar; + proto$2.hours = hoursVar; + proto$2.days = daysVar; proto$2.weeks = weeks; - proto$2.months = months; - proto$2.years = years; + proto$2.months = monthsVar; + proto$2.years = yearsVar; proto$2.humanize = humanize; proto$2.toISOString = toISOString$1; proto$2.toString = toISOString$1; diff --git a/pkg/http-server/assets/webhook-scan-logs.js b/pkg/http-server/assets/webhook-scan-logs.js index 86553323b..6018fdacf 100644 --- a/pkg/http-server/assets/webhook-scan-logs.js +++ b/pkg/http-server/assets/webhook-scan-logs.js @@ -14,8 +14,8 @@ for (var i = 0; i < jsonElements.length; i++) { // Replace all time-object elements to be in the 'DD/MM/YYYY hh:mm:ss A' format of moment.js let timeElements = document.getElementsByClassName("time-object") -for (var i = 0; i < timeElements.length; i++) { - let element = timeElements[i] +for (let val of timeElements) { + let element = val; let elapsedTimeUntilNow = Date.now() - new Date(element.innerText) if (elapsedTimeUntilNow / 1000 < 120) { // In case elapsed less than 2 minutes, show "A few seconds ago" @@ -28,8 +28,8 @@ for (var i = 0; i < timeElements.length; i++) { // Change the colors of the review status let statusElements = document.getElementsByClassName("review-status") -for (var i = 0; i < statusElements.length; i++) { - let element = statusElements[i] +for (let val of statusElements) { + let element = val; switch (element.innerText) { case "Allowed": element.classList.add("allowed") @@ -41,4 +41,4 @@ for (var i = 0; i < statusElements.length; i++) { element.classList.add("warn") break } -} +} \ No newline at end of file From 5bb303c8d062d50e8e0fc9934cdebbf43cbc9671 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Sat, 3 Apr 2021 03:09:37 +0530 Subject: [PATCH 29/38] trying to make sonar lint happy --- pkg/http-server/assets/jsonTree.js | 5 +- pkg/http-server/assets/moment.js | 59 +++++++++---------- pkg/http-server/assets/webhook-scan-logs.js | 4 +- .../admission-webhook/validating-webhook.go | 7 ++- 4 files changed, 35 insertions(+), 40 deletions(-) diff --git a/pkg/http-server/assets/jsonTree.js b/pkg/http-server/assets/jsonTree.js index 738eaf795..6fb0f8dba 100644 --- a/pkg/http-server/assets/jsonTree.js +++ b/pkg/http-server/assets/jsonTree.js @@ -223,7 +223,7 @@ var jsonTree = (function() { el = document.createElement('li'), labelEl, template = function(datalabel, value) { - var str = ''+ + return ''+ '"' + datalabel + '" : ' + '' + @@ -232,8 +232,6 @@ var jsonTree = (function() { '' + (!isLast ? ',' : '') + ''; - - return str; }; self.label = label; @@ -255,7 +253,6 @@ var jsonTree = (function() { if (e.shiftKey) { document.getSelection().removeAllRanges(); alert(self.getJSONPath()); - return; } }, false); } diff --git a/pkg/http-server/assets/moment.js b/pkg/http-server/assets/moment.js index aeae3bf89..b515d31b4 100644 --- a/pkg/http-server/assets/moment.js +++ b/pkg/http-server/assets/moment.js @@ -508,10 +508,10 @@ return function (mom) { var output = '', i; - for (i = 0; i < length; i++) { - output += isFunction(array[i]) - ? array[i].call(mom, formatVar) - : array[i]; + for (j = 0; j < length; j++) { + output += isFunction(array[j]) + ? array[j].call(mom, formatVar) + : array[j]; } return output; }; @@ -1080,7 +1080,7 @@ // test the regex if ( strict && - format === 'MMMM' && + format == 'MMMM' && this._longMonthsParse[i].test(monthName) ) { return i; @@ -1695,14 +1695,14 @@ } if ( strict && - format === 'ddd' && + format == 'ddd' && this._shortWeekdaysParse[i].test(weekdayName) ) { return i; } if ( strict && - format === 'dd' && + format == 'dd' && this._minWeekdaysParse[i].test(weekdayName) ) { return i; @@ -2265,25 +2265,26 @@ a = m._a; if (a && getParsingFlags(m).overflow === -2) { - overflow = - a[MONTH] < 0 || a[MONTH] > 11 - ? MONTH - : a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) - ? DATE - : a[HOUR] < 0 || - a[HOUR] > 24 || - (a[HOUR] === 24 && - (a[MINUTE] !== 0 || - a[SECOND] !== 0 || - a[MILLISECOND] !== 0)) - ? HOUR - : a[MINUTE] < 0 || a[MINUTE] > 59 - ? MINUTE - : a[SECOND] < 0 || a[SECOND] > 59 - ? SECOND - : a[MILLISECOND] < 0 || a[MILLISECOND] > 999 - ? MILLISECOND - : -1; + if (a[MONTH] < 0 || a[MONTH] > 11) { + overflow = MONTH; + } else if (a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH])) { + overflow = DATE; + } else if (a[HOUR] < 0 || + a[HOUR] > 24 || + (a[HOUR] === 24 && + (a[MINUTE] !== 0 || + a[SECOND] !== 0 || + a[MILLISECOND] !== 0))) { + overflow = HOUR; + } else if (a[MINUTE] < 0 || a[MINUTE] > 59) { + overflow = MINUTE; + } else if (a[SECOND] < 0 || a[SECOND] > 59) { + overflow = SECOND; + } else if (a[MILLISECOND] < 0 || a[MILLISECOND] > 999) { + overflow = MILLISECOND; + } else { + overflow = -1; + } if ( getParsingFlags(m)._overflowDayOfYear && @@ -5378,11 +5379,7 @@ var milliseconds = makeGetter('milliseconds'), seconds = makeGetter('seconds'), - minutes = makeGetter('minutes'), - hours = makeGetter('hours'), - days = makeGetter('days'), - months = makeGetter('months'), - years = makeGetter('years'); + hours = makeGetter('hours'); function weeks() { return absFloor(this.days() / 7); diff --git a/pkg/http-server/assets/webhook-scan-logs.js b/pkg/http-server/assets/webhook-scan-logs.js index 6018fdacf..283d98035 100644 --- a/pkg/http-server/assets/webhook-scan-logs.js +++ b/pkg/http-server/assets/webhook-scan-logs.js @@ -1,7 +1,7 @@ // Replace all json-objects elements to be a JSON tree let jsonElements = document.getElementsByClassName("json-object") -for (var i = 0; i < jsonElements.length; i++) { - let element = jsonElements[i] +for (let val of jsonElements) { + let element = val; if (element.innerText.length < 1) { continue } diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index ea761e81f..3a3bcf710 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -129,6 +129,7 @@ func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview, se denyViolations []results.Violation logURL = w.dblogger.GetLogURL(serverURL, string(review.Request.UID)) allowed = false + errMsg = "%s; error: %w" ) // In case the object is nil => an operation of DELETE happened, just return 'allow' since there is nothing to check @@ -143,7 +144,7 @@ func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview, se if err != nil { msg := "failed to create temp file for validating admission review request" zap.S().Error(msg, zap.Error(err)) - return w.createResponseAdmissionReview(review, allowed, output, logURL), fmt.Errorf("%s; error: %w", msg, err) + return w.createResponseAdmissionReview(review, allowed, output, logURL), fmt.Errorf(errMsg, msg, err) } // Run the policy engines @@ -151,7 +152,7 @@ func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview, se if err != nil { msg := "failed to evaluate terrascan policies" zap.S().Errorf(msg, zap.Error(err)) - return w.createResponseAdmissionReview(review, allowed, output, logURL), fmt.Errorf("%s; error: %w", msg, err) + return w.createResponseAdmissionReview(review, allowed, output, logURL), fmt.Errorf(errMsg, msg, err) } // Calculate if there are anydeny violations @@ -159,7 +160,7 @@ func (w ValidatingWebhook) ProcessWebhook(review admissionv1.AdmissionReview, se if err != nil { msg := "failed to figure out denied violations" zap.S().Errorf(msg, zap.Error(err)) - return w.createResponseAdmissionReview(review, allowed, output, logURL), fmt.Errorf("%s; error: %w", msg, err) + return w.createResponseAdmissionReview(review, allowed, output, logURL), fmt.Errorf(errMsg, msg, err) } allowed = len(denyViolations) < 1 From 90b842947c7be276d327ae1da2637f8777ed41f7 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Sat, 3 Apr 2021 21:34:13 +0530 Subject: [PATCH 30/38] fixing html file bugs --- pkg/http-server/templates/index.html | 2 +- pkg/http-server/templates/show.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/http-server/templates/index.html b/pkg/http-server/templates/index.html index 69000acc8..ce20559d0 100644 --- a/pkg/http-server/templates/index.html +++ b/pkg/http-server/templates/index.html @@ -7,7 +7,7 @@ - +
              diff --git a/pkg/http-server/templates/show.html b/pkg/http-server/templates/show.html index 7b3633086..4f3a6c439 100644 --- a/pkg/http-server/templates/show.html +++ b/pkg/http-server/templates/show.html @@ -8,7 +8,7 @@
              -
              Time
              +
              From 056a9f2a5d85e1d197ae99c09907347422c1a70b Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala <30405568+kanchwala-yusuf@users.noreply.github.com> Date: Mon, 5 Apr 2021 09:53:01 +0530 Subject: [PATCH 31/38] Update docs/getting-started/admission-controller-webhooks-usage.md Co-authored-by: Devang Gaur --- docs/getting-started/admission-controller-webhooks-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/admission-controller-webhooks-usage.md b/docs/getting-started/admission-controller-webhooks-usage.md index 5db33da16..274caa951 100644 --- a/docs/getting-started/admission-controller-webhooks-usage.md +++ b/docs/getting-started/admission-controller-webhooks-usage.md @@ -29,7 +29,7 @@ Run Terrascan docker image in your server using the following command: `` is a key used for authentication between your K8s environment and the Terrascan server. Generate your preferred key and use it here. `` is a directory path in your server where both the certificate and the private key .pem files are stored. -In addition, this directory is used to write save the webhook logs. (An SQLite file) +In addition, this directory is used to save the webhook logs. (An SQLite file) You can specify a config file that specifies which policies to use in the scan and which violations should lead to rejection. From 0ef2a8d2916b7a4abcd8f57a69b2f59136822aaf Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala <30405568+kanchwala-yusuf@users.noreply.github.com> Date: Mon, 5 Apr 2021 09:53:43 +0530 Subject: [PATCH 32/38] Update pkg/http-server/start.go Co-authored-by: Devang Gaur --- pkg/http-server/start.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/http-server/start.go b/pkg/http-server/start.go index ab7cf24c4..bf9199ade 100644 --- a/pkg/http-server/start.go +++ b/pkg/http-server/start.go @@ -74,7 +74,7 @@ func (g *APIServer) start(routes []*Route, port, certFile, privateKeyFile string go func() { var err error - if certFile != "" { + if certFile != "" && privateKeyFile != "" { // In case a certificate file is specified, the server support TLS err = server.ListenAndServeTLS(certFile, privateKeyFile) } else { From e405999774f901209e5d36512b0fe2cf32427ba1 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Tue, 6 Apr 2021 20:16:01 +0530 Subject: [PATCH 33/38] fixing documentation bullets --- .../admission-controller-webhooks-usage.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/getting-started/admission-controller-webhooks-usage.md b/docs/getting-started/admission-controller-webhooks-usage.md index 274caa951..dde8fb824 100644 --- a/docs/getting-started/admission-controller-webhooks-usage.md +++ b/docs/getting-started/admission-controller-webhooks-usage.md @@ -15,11 +15,11 @@ In this guide, we'll demonstrate how Terrascan can be configured to: Your Terrascan instance has the following requirements for being able to scan K8s configurations. 1. Be accessible via HTTPS. Make sure your cloud firewall is configured to allow this. -1. Have a valid SSL certificate for the served domain name. To do that, choose one of our suggested methods: - 1. Use a subdomain of your choosing (e.g dev-terrascan-k8s.accurics.com) and create a valid certificate for this subdomain through your SSL certificate provider. [Let's Encrypt](https://letsencrypt.org/) is a free, simple to use certificate authority you can use. - 1. Use a reverse-proxy to serve SSL requests; for example, use Cloudflare Flexible to get a certificate by a trusted-CA to your [self-signed certificate](https://www.digitalocean.com/community/tutorials/openssl-essentials-working-with-ssl-certificates-private-keys-and-csrs). - 1. Generate a self-signed certificate and have your K8s cluster trust it. To add a trusted CA to ca-pemstore, as demonstrated in [paraspatidar's blog post](https://medium.com/@paraspatidar/add-ssl-tls-certificate-or-pem-file-to-kubernetes-pod-s-trusted-root-ca-store-7bed5cd683d). -1. Use the Terrascan docker as demonstrated in this document, or run it from the sources. +2. Have a valid SSL certificate for the served domain name. To do that, choose one of our suggested methods: + - Use a subdomain of your choosing (e.g dev-terrascan-k8s.accurics.com) and create a valid certificate for this subdomain through your SSL certificate provider. [Let's Encrypt](https://letsencrypt.org/) is a free, simple to use certificate authority you can use. + - Use a reverse-proxy to serve SSL requests; for example, use Cloudflare Flexible to get a certificate by a trusted-CA to your [self-signed certificate](https://www.digitalocean.com/community/tutorials/openssl-essentials-working-with-ssl-certificates-private-keys-and-csrs). + - Generate a self-signed certificate and have your K8s cluster trust it. To add a trusted CA to ca-pemstore, as demonstrated in [paraspatidar's blog post](https://medium.com/@paraspatidar/add-ssl-tls-certificate-or-pem-file-to-kubernetes-pod-s-trusted-root-ca-store-7bed5cd683d). +3. Use the Terrascan docker as demonstrated in this document, or run it from the sources. ### Run Terrascan webhook service Run Terrascan docker image in your server using the following command: From d9e2a71aba970c2b346668a84ad522bc805ca754 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Tue, 6 Apr 2021 20:25:46 +0530 Subject: [PATCH 34/38] updating documentation --- docs/getting-started/admission-controller-webhooks-usage.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/getting-started/admission-controller-webhooks-usage.md b/docs/getting-started/admission-controller-webhooks-usage.md index dde8fb824..3a682b979 100644 --- a/docs/getting-started/admission-controller-webhooks-usage.md +++ b/docs/getting-started/admission-controller-webhooks-usage.md @@ -53,6 +53,7 @@ You can specify the following configurations: * **scan-rules** - one or more rules to scan * **skip-rules** - one or more rules to skip while scanning * **severity** - the minimal level of severity of the policies to be scanned +* **category** - the list of type of categories of the policies to be scanned * **k8s-deny-rules** - specify the rules that should cause a rejection of the admission request From f31634c1438de31953857a101ab480aecd34ad83 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Wed, 7 Apr 2021 10:45:02 +0530 Subject: [PATCH 35/38] fix: admission request is saved in db logs --- pkg/http-server/webhook-scan-logs.go | 8 +++--- pkg/http-server/webhook-scan.go | 27 +++++++++---------- .../admission-webhook/validating-webhook.go | 8 +++--- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/pkg/http-server/webhook-scan-logs.go b/pkg/http-server/webhook-scan-logs.go index 517dd0806..db79afe21 100644 --- a/pkg/http-server/webhook-scan-logs.go +++ b/pkg/http-server/webhook-scan-logs.go @@ -175,14 +175,14 @@ func (g *APIHandler) getLogReasoning(log dblogs.WebhookScanLog) string { if !log.Allowed { err := json.Unmarshal([]byte(log.DeniableViolations), &violations) if err != nil { - zap.S().Errorf("Failed to deserialize deniable violations summary. Error: %v", err.Error()) + zap.S().Errorf("failed to deserialize deniable violations summary. Error: %v", err.Error()) return "" } } else { var violationStore results.ViolationStore err := json.Unmarshal([]byte(log.ViolationsSummary), &violationStore) if err != nil { - zap.S().Errorf("Failed to deserialize violations summary. Error: %v", err.Error()) + zap.S().Errorf("failed to deserialize violations summary. Error: %v", err.Error()) return "" } @@ -218,13 +218,13 @@ func (g *APIHandler) getLogRequest(log dblogs.WebhookScanLog) string { err := json.Unmarshal([]byte(log.Request), &review) if err != nil { - zap.S().Errorf("Failed to deserialize request. Error: %v", err.Error()) + zap.S().Errorf("failed to deserialize request. Error: %v", err.Error()) return "{}" } result, err := json.Marshal(review.Request) if err != nil { - zap.S().Errorf("Failed to serialize request. Error: %v", err.Error()) + zap.S().Errorf("failed to serialize request. Error: %v", err.Error()) return "{}" } diff --git a/pkg/http-server/webhook-scan.go b/pkg/http-server/webhook-scan.go index 30775fbc3..ff88b7fc1 100644 --- a/pkg/http-server/webhook-scan.go +++ b/pkg/http-server/webhook-scan.go @@ -33,11 +33,20 @@ import ( func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) { var ( - params = mux.Vars(r) - apiKey = params["apiKey"] - validatingWebhook = admissionWebhook.NewValidatingWebhook(g.configFile) + params = mux.Vars(r) + apiKey = params["apiKey"] ) + // Read the request into byte array + body, err := ioutil.ReadAll(r.Body) + if err != nil { + msg := fmt.Sprintf("failed to read validating admission webhook request body, error: '%v'", err) + apiErrorResponse(w, msg, http.StatusBadRequest) + return + } + zap.S().Debugf("scanning configuration webhook request: %+v", string(body)) + + validatingWebhook := admissionWebhook.NewValidatingWebhook(g.configFile, body) // Validate if authorized (API key is specified and matched the server one (saved in an environment variable) if err := validatingWebhook.Authorize(apiKey); err != nil { switch err { @@ -51,16 +60,6 @@ func (g *APIHandler) validateK8SWebhook(w http.ResponseWriter, r *http.Request) return } - // Read the request into byte array - body, err := ioutil.ReadAll(r.Body) - if err != nil { - msg := fmt.Sprintf("failed to read validating admission webhook request body, error: '%v'", err) - apiErrorResponse(w, msg, http.StatusBadRequest) - return - } - - zap.S().Debugf("scanning configuration webhook request: %+v", string(body)) - // decode incoming admission review request requestedAdmissionReview, err := validatingWebhook.DecodeAdmissionReviewRequest(body) if err != nil { @@ -92,6 +91,6 @@ func (g *APIHandler) sendResponseAdmissionReview(w http.ResponseWriter, admissio return } - zap.S().Debugf("Response result: %+v", string(respBytes)) + zap.S().Debugf("response result: %+v", string(respBytes)) apiResponse(w, string(respBytes), http.StatusOK) } diff --git a/pkg/k8s/admission-webhook/validating-webhook.go b/pkg/k8s/admission-webhook/validating-webhook.go index 3a3bcf710..a6dfc894c 100644 --- a/pkg/k8s/admission-webhook/validating-webhook.go +++ b/pkg/k8s/admission-webhook/validating-webhook.go @@ -47,10 +47,11 @@ type ValidatingWebhook struct { } // NewValidatingWebhook returns a new, empty ValidatingWebhook struct -func NewValidatingWebhook(configFile string) AdmissionWebhook { +func NewValidatingWebhook(configFile string, body []byte) AdmissionWebhook { return ValidatingWebhook{ - configFile: configFile, - dblogger: dblogs.NewWebhookScanLogger(), + configFile: configFile, + dblogger: dblogs.NewWebhookScanLogger(), + requestBody: body, } } @@ -106,7 +107,6 @@ func (w ValidatingWebhook) DecodeAdmissionReviewRequest(requestBody []byte) (adm deserializer = codecs.UniversalDeserializer() requestedAdmissionReview admissionv1.AdmissionReview ) - w.requestBody = requestBody admissionv1.AddToScheme(scheme) // decode incoming admission request From a53ce45f3627b599b21b96f1ebc39fcc9bf24d0d Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Wed, 7 Apr 2021 20:23:02 +0530 Subject: [PATCH 36/38] fixing go mod files --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 4ce737ac6..8db3db276 100644 --- a/go.sum +++ b/go.sum @@ -1135,8 +1135,6 @@ golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 8d82d909b5bde54f69cd6c14c6d33da854abb1d5 Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Thu, 8 Apr 2021 10:11:36 +0530 Subject: [PATCH 37/38] serving the CSS locally instead of fetching from internet --- pkg/http-server/assets/bootstrap.min.css | 7 +++++++ pkg/http-server/templates/show.html | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 pkg/http-server/assets/bootstrap.min.css diff --git a/pkg/http-server/assets/bootstrap.min.css b/pkg/http-server/assets/bootstrap.min.css new file mode 100644 index 000000000..92e3fe871 --- /dev/null +++ b/pkg/http-server/assets/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.3.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:calc(1rem + .4rem);padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion>.card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion>.card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.accordion>.card .card-header{margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-sm .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-md .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-lg .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-xl .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush .list-group-item:last-child{margin-bottom:-1px}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{margin-bottom:0;border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #dee2e6;border-bottom-right-radius:.3rem;border-bottom-left-radius:.3rem}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:0s .6s opacity}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/pkg/http-server/templates/show.html b/pkg/http-server/templates/show.html index 4f3a6c439..a340b98e2 100644 --- a/pkg/http-server/templates/show.html +++ b/pkg/http-server/templates/show.html @@ -2,7 +2,7 @@ K8s Admission Review Logs - + From 1911323c9cc2c9774353f167cab7b79402241ccd Mon Sep 17 00:00:00 2001 From: Yusuf Kanchwala Date: Thu, 8 Apr 2021 16:51:15 +0530 Subject: [PATCH 38/38] accommodating review comments on documentation --- .../admission-controller-webhooks-usage.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/getting-started/admission-controller-webhooks-usage.md b/docs/getting-started/admission-controller-webhooks-usage.md index 3a682b979..d2a954fe7 100644 --- a/docs/getting-started/admission-controller-webhooks-usage.md +++ b/docs/getting-started/admission-controller-webhooks-usage.md @@ -24,7 +24,7 @@ Your Terrascan instance has the following requirements for being able to scan K8 ### Run Terrascan webhook service Run Terrascan docker image in your server using the following command: ```bash - sudo docker run -p 443:9443 -v :/data -u root -e K8S_WEBHOOK_API_KEY=> accurics/terrascan server --cert-path /data/cert.pem --key-path /data/key.pem + sudo docker run -p 443:9443 -v :/data -u root -e K8S_WEBHOOK_API_KEY= accurics/terrascan server --cert-path /data/cert.pem --key-path /data/key.pem -c /data/config.toml ``` `` is a key used for authentication between your K8s environment and the Terrascan server. Generate your preferred key and use it here. @@ -33,7 +33,7 @@ In addition, this directory is used to save the webhook logs. (An SQLite file) You can specify a config file that specifies which policies to use in the scan and which violations should lead to rejection. -A config file example: ```my_terrscan_config.toml``` +A config file example: ```config.toml``` ```bash [severity] level = "medium" @@ -60,11 +60,6 @@ You can specify the following configurations: * **denied-categories** - one or more policy categories that are not allowed in the detected violations * **denied-severity** - the minimal level of severity that should cause a rejection -In order to use a configuration file, add it as a command line argument: - -``` -c /data/my_terrscan_config.toml``` - - ### Configure K8s to send webhooks Configure a new ```ValidatingWebhookConfiguration``` in your Kubernetes environment and specify your Terrascan server endpoint.
              UID