From 8856c8dde71fa8e526f20fd5f3068dce090f79ca Mon Sep 17 00:00:00 2001 From: Omer Levi Hevroni Date: Sun, 23 Dec 2018 10:55:08 +0200 Subject: [PATCH] refactor: break to 2 apis - decrypt and encrypt (#31) * refactor: break to 2 apis - decrypt and encrypt * fix dockerfile + black box tests * initial commit - adding chart * fix the chart * add kamus cli * some fixes * enforce HTTPs urls * use caporal logger * fix encrypt.js * fix missing http * added cert pinning * fix CR comments --- .dockerignore | 6 +- .gitignore | 3 +- Dockerfile | 20 +- Hamuste.sln | 44 +- chart/Chart.yaml | 12 + chart/templates/NOTES.txt | 19 + chart/templates/_helpers.tpl | 31 + chart/templates/autoscaling-decryptor.yaml | 19 + chart/templates/autoscaling-encryptor.yaml | 19 + chart/templates/configmap-decryptor.yaml | 7 + chart/templates/configmap-encryptor.yaml | 7 + chart/templates/deployment-decryptor.yaml | 65 ++ chart/templates/deployment-encryptor.yaml | 100 ++ chart/templates/ingress.yaml | 40 + chart/templates/role.yaml | 9 + chart/templates/rolebinding.yaml | 13 + chart/templates/secret.yaml | 8 + chart/templates/service-decryptor.yaml | 26 + chart/templates/service-encryptor.yaml | 30 + chart/templates/serviceaccount.yaml | 5 + chart/test.sh | 54 + chart/values.yaml | 61 ++ cli/.gitignore | 1 + cli/encrypt.js | 140 +++ cli/index.js | 20 + cli/is-docker.js | 32 + cli/package.json | 31 + cli/yarn.lock | 946 ++++++++++++++++++ .../Controllers/DecryptController.cs} | 39 +- .../Controllers/MonitoringController.cs | 0 .../ErrorHandlingMiddleware.cs | 0 .../Extensions/LoggingExtensions.cs | 0 .../KubernetesAuthenticationHandler.cs | 75 ++ .../KubernetesAuthenticationOptions.cs | 0 .../Models/DecryptRequest.cs | 0 src/{ => decrypt-api}/Program.cs | 0 .../Properties/launchSettings.json | 0 src/{ => decrypt-api}/Startup.cs | 0 .../appsettings.Development.json | 0 src/{ => decrypt-api}/appsettings.json | 0 .../decrypt-api.csproj} | 4 +- .../Controllers/EncryptController.cs | 54 + .../Controllers/MonitoringController.cs | 26 + src/encrypt-api/ErrorHandlingMiddleware.cs | 42 + .../Extensions/LoggingExtensions.cs | 12 + .../KubernetesAuthenticationHandler.cs | 0 .../KubernetesAuthenticationOptions.cs | 8 + .../Models/EncryptRequest.cs | 0 src/encrypt-api/Program.cs | 28 + .../Properties/launchSettings.json | 27 + src/encrypt-api/Startup.cs | 121 +++ src/encrypt-api/appsettings.Development.json | 34 + src/encrypt-api/appsettings.json | 18 + src/encrypt-api/encrypt-api.csproj | 33 + .../AzureKeyVaultKeyManagement.cs | 0 .../EnvelopeEncryptionDecorator.cs | 0 .../IKeyManagement.cs | 0 .../SymmetricKeyManagement.cs | 2 +- src/key-managment/key-managment.csproj | 15 + tests/blackbox/EncryptControllerTests.cs | 80 +- ...-ff46d3b4-0aad-422d-8215-e99cff8f3188.json | 2 +- .../compose/docker-compose.local.yaml | 9 +- tests/blackbox/compose/docker-compose.yaml | 12 +- tests/integration/integration.csproj | 9 +- tests/unit/unit.csproj | 9 +- 65 files changed, 2299 insertions(+), 128 deletions(-) create mode 100644 chart/Chart.yaml create mode 100644 chart/templates/NOTES.txt create mode 100644 chart/templates/_helpers.tpl create mode 100644 chart/templates/autoscaling-decryptor.yaml create mode 100644 chart/templates/autoscaling-encryptor.yaml create mode 100644 chart/templates/configmap-decryptor.yaml create mode 100644 chart/templates/configmap-encryptor.yaml create mode 100644 chart/templates/deployment-decryptor.yaml create mode 100644 chart/templates/deployment-encryptor.yaml create mode 100644 chart/templates/ingress.yaml create mode 100644 chart/templates/role.yaml create mode 100644 chart/templates/rolebinding.yaml create mode 100644 chart/templates/secret.yaml create mode 100644 chart/templates/service-decryptor.yaml create mode 100644 chart/templates/service-encryptor.yaml create mode 100644 chart/templates/serviceaccount.yaml create mode 100755 chart/test.sh create mode 100644 chart/values.yaml create mode 100644 cli/.gitignore create mode 100644 cli/encrypt.js create mode 100644 cli/index.js create mode 100644 cli/is-docker.js create mode 100644 cli/package.json create mode 100644 cli/yarn.lock rename src/{Controllers/EncryptController.cs => decrypt-api/Controllers/DecryptController.cs} (59%) rename src/{ => decrypt-api}/Controllers/MonitoringController.cs (100%) rename src/{ => decrypt-api}/ErrorHandlingMiddleware.cs (100%) rename src/{ => decrypt-api}/Extensions/LoggingExtensions.cs (100%) create mode 100644 src/decrypt-api/KubernetesAuthentication/KubernetesAuthenticationHandler.cs rename src/{ => decrypt-api}/KubernetesAuthentication/KubernetesAuthenticationOptions.cs (100%) rename src/{ => decrypt-api}/Models/DecryptRequest.cs (100%) rename src/{ => decrypt-api}/Program.cs (100%) rename src/{ => decrypt-api}/Properties/launchSettings.json (100%) rename src/{ => decrypt-api}/Startup.cs (100%) rename src/{ => decrypt-api}/appsettings.Development.json (100%) rename src/{ => decrypt-api}/appsettings.json (100%) rename src/{Hamuste.csproj => decrypt-api/decrypt-api.csproj} (93%) create mode 100644 src/encrypt-api/Controllers/EncryptController.cs create mode 100644 src/encrypt-api/Controllers/MonitoringController.cs create mode 100644 src/encrypt-api/ErrorHandlingMiddleware.cs create mode 100644 src/encrypt-api/Extensions/LoggingExtensions.cs rename src/{ => encrypt-api}/KubernetesAuthentication/KubernetesAuthenticationHandler.cs (100%) create mode 100644 src/encrypt-api/KubernetesAuthentication/KubernetesAuthenticationOptions.cs rename src/{ => encrypt-api}/Models/EncryptRequest.cs (100%) create mode 100644 src/encrypt-api/Program.cs create mode 100644 src/encrypt-api/Properties/launchSettings.json create mode 100644 src/encrypt-api/Startup.cs create mode 100644 src/encrypt-api/appsettings.Development.json create mode 100644 src/encrypt-api/appsettings.json create mode 100644 src/encrypt-api/encrypt-api.csproj rename src/{KeyManagement => key-managment}/AzureKeyVaultKeyManagement.cs (100%) rename src/{KeyManagement => key-managment}/EnvelopeEncryptionDecorator.cs (100%) rename src/{KeyManagement => key-managment}/IKeyManagement.cs (100%) rename src/{KeyManagement => key-managment}/SymmetricKeyManagement.cs (98%) create mode 100644 src/key-managment/key-managment.csproj diff --git a/.dockerignore b/.dockerignore index eb9da8c7a..5d7722e60 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,6 @@ bin\ -obj\ \ No newline at end of file +obj\ +cmd/ +**/bin/* +**/obj/* +**/vendor \ No newline at end of file diff --git a/.gitignore b/.gitignore index 96729a78c..94e4c97f8 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,5 @@ job/ **/report.json **/report.json -node_modules/ \ No newline at end of file +node_modules/ +**/vendor/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 37d10c879..51fbb9f5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,26 @@ FROM microsoft/dotnet:2.1-sdk AS build-env +ARG PROJECT_NAME=decrypt-api + WORKDIR /app # Copy csproj and restore as distinct layers -COPY ./src/Hamuste.csproj ./ -RUN dotnet restore +COPY ./src/$PROJECT_NAME/$PROJECT_NAME.csproj ./$PROJECT_NAME/$PROJECT_NAME.csproj +COPY ./src/key-managment/key-managment.csproj ./key-managment/key-managment.csproj +RUN dotnet restore $PROJECT_NAME/$PROJECT_NAME.csproj # Copy everything else and build -COPY ./src ./ -RUN dotnet publish -c Release -o ./obj/Docker/publish +COPY ./src/$PROJECT_NAME ./$PROJECT_NAME +COPY ./src/key-managment ./key-managment +RUN dotnet publish $PROJECT_NAME/$PROJECT_NAME.csproj -c Release -o ./obj/Docker/publish # Build runtime image -FROM microsoft/dotnet:2.1-aspnetcore-runtime as release +FROM microsoft/dotnet:2.1.6-aspnetcore-runtime as release +ARG PROJECT_NAME=decrypt-api +ENV PROJECT_NAME_ENV=$PROJECT_NAME RUN groupadd -r dotnet && useradd --no-log-init -r -g dotnet -d /home/dotnet -ms /bin/bash dotnet USER dotnet WORKDIR /home/dotnet/app ENV ASPNETCORE_URLS=http://+:9999 -COPY --from=build-env /app/obj/Docker/publish . -ENTRYPOINT ["dotnet", "Hamuste.dll"] \ No newline at end of file +COPY --from=build-env /app/$PROJECT_NAME/obj/Docker/publish . +ENTRYPOINT dotnet $PROJECT_NAME_ENV.dll \ No newline at end of file diff --git a/Hamuste.sln b/Hamuste.sln index 8397dc89c..11f9a78e4 100644 --- a/Hamuste.sln +++ b/Hamuste.sln @@ -8,10 +8,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "blackbox", "tests\blackbox\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "unit", "tests\unit\unit.csproj", "{3F737829-7340-49FA-893D-4845C5F882AD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hamuste", "src\Hamuste.csproj", "{A12BBF7B-19E2-43CD-B230-DC6D4CABAAC1}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "integration", "tests\integration\integration.csproj", "{EE33CBB2-857E-47AE-BC8E-8C9CC23488D2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "key-managment", "src\key-managment\key-managment.csproj", "{30BF661E-BA0D-42CE-AA14-3EAAD47D78BD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "decrypt-api", "src\decrypt-api\decrypt-api.csproj", "{250FAE91-D1C3-4BE2-ABCB-400882AB235D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "encrypt-api", "src\encrypt-api\encrypt-api.csproj", "{E69C788D-77EC-4C83-842A-425978A715FD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -82,6 +86,42 @@ Global {EE33CBB2-857E-47AE-BC8E-8C9CC23488D2}.Release|x64.Build.0 = Release|Any CPU {EE33CBB2-857E-47AE-BC8E-8C9CC23488D2}.Release|x86.ActiveCfg = Release|Any CPU {EE33CBB2-857E-47AE-BC8E-8C9CC23488D2}.Release|x86.Build.0 = Release|Any CPU + {30BF661E-BA0D-42CE-AA14-3EAAD47D78BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30BF661E-BA0D-42CE-AA14-3EAAD47D78BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30BF661E-BA0D-42CE-AA14-3EAAD47D78BD}.Debug|x64.ActiveCfg = Debug|Any CPU + {30BF661E-BA0D-42CE-AA14-3EAAD47D78BD}.Debug|x64.Build.0 = Debug|Any CPU + {30BF661E-BA0D-42CE-AA14-3EAAD47D78BD}.Debug|x86.ActiveCfg = Debug|Any CPU + {30BF661E-BA0D-42CE-AA14-3EAAD47D78BD}.Debug|x86.Build.0 = Debug|Any CPU + {30BF661E-BA0D-42CE-AA14-3EAAD47D78BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30BF661E-BA0D-42CE-AA14-3EAAD47D78BD}.Release|Any CPU.Build.0 = Release|Any CPU + {30BF661E-BA0D-42CE-AA14-3EAAD47D78BD}.Release|x64.ActiveCfg = Release|Any CPU + {30BF661E-BA0D-42CE-AA14-3EAAD47D78BD}.Release|x64.Build.0 = Release|Any CPU + {30BF661E-BA0D-42CE-AA14-3EAAD47D78BD}.Release|x86.ActiveCfg = Release|Any CPU + {30BF661E-BA0D-42CE-AA14-3EAAD47D78BD}.Release|x86.Build.0 = Release|Any CPU + {250FAE91-D1C3-4BE2-ABCB-400882AB235D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {250FAE91-D1C3-4BE2-ABCB-400882AB235D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {250FAE91-D1C3-4BE2-ABCB-400882AB235D}.Debug|x64.ActiveCfg = Debug|Any CPU + {250FAE91-D1C3-4BE2-ABCB-400882AB235D}.Debug|x64.Build.0 = Debug|Any CPU + {250FAE91-D1C3-4BE2-ABCB-400882AB235D}.Debug|x86.ActiveCfg = Debug|Any CPU + {250FAE91-D1C3-4BE2-ABCB-400882AB235D}.Debug|x86.Build.0 = Debug|Any CPU + {250FAE91-D1C3-4BE2-ABCB-400882AB235D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {250FAE91-D1C3-4BE2-ABCB-400882AB235D}.Release|Any CPU.Build.0 = Release|Any CPU + {250FAE91-D1C3-4BE2-ABCB-400882AB235D}.Release|x64.ActiveCfg = Release|Any CPU + {250FAE91-D1C3-4BE2-ABCB-400882AB235D}.Release|x64.Build.0 = Release|Any CPU + {250FAE91-D1C3-4BE2-ABCB-400882AB235D}.Release|x86.ActiveCfg = Release|Any CPU + {250FAE91-D1C3-4BE2-ABCB-400882AB235D}.Release|x86.Build.0 = Release|Any CPU + {E69C788D-77EC-4C83-842A-425978A715FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E69C788D-77EC-4C83-842A-425978A715FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E69C788D-77EC-4C83-842A-425978A715FD}.Debug|x64.ActiveCfg = Debug|Any CPU + {E69C788D-77EC-4C83-842A-425978A715FD}.Debug|x64.Build.0 = Debug|Any CPU + {E69C788D-77EC-4C83-842A-425978A715FD}.Debug|x86.ActiveCfg = Debug|Any CPU + {E69C788D-77EC-4C83-842A-425978A715FD}.Debug|x86.Build.0 = Debug|Any CPU + {E69C788D-77EC-4C83-842A-425978A715FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E69C788D-77EC-4C83-842A-425978A715FD}.Release|Any CPU.Build.0 = Release|Any CPU + {E69C788D-77EC-4C83-842A-425978A715FD}.Release|x64.ActiveCfg = Release|Any CPU + {E69C788D-77EC-4C83-842A-425978A715FD}.Release|x64.Build.0 = Release|Any CPU + {E69C788D-77EC-4C83-842A-425978A715FD}.Release|x86.ActiveCfg = Release|Any CPU + {E69C788D-77EC-4C83-842A-425978A715FD}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 000000000..a30b7eae4 --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +description: An open source, git-ops, zero-trust secrets encryption and decryption solution for Kubernetes applications +name: kamus +version: 0.1.0 +keywords: + - gitops + - secrets +sources: + - https://github.com/Soluto/Kamus +maintainers: + - name: Omer Levi Hevroni + - name: Shai Katz diff --git a/chart/templates/NOTES.txt b/chart/templates/NOTES.txt new file mode 100644 index 000000000..fd13fe4ea --- /dev/null +++ b/chart/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http://{{ . }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "kamus.name" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "kamus.name" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "kamus.name" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.externalPort }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "kamus.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:{{ .Values.service.internalPort }} +{{- end }} diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl new file mode 100644 index 000000000..ddbcb68b1 --- /dev/null +++ b/chart/templates/_helpers.tpl @@ -0,0 +1,31 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "kamus.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "appsettings.secret.json" }} +{{ printf "{\n\t\"ActiveDirectory\": { " }} +{{ if .Values.activeDirectory}} +{{ printf "\t\t\"ClientSecret\": \"%s\" " .Values.activeDirectory.clientSecret }} +{{- end -}} +{{ if .Values.keyManagment.AES}} +{{ printf "\"KeyManagement\": { \n\t\t\"AES\": { \"Key\": \"%s\" } }" .Values.keyManagment.AES.key }} +{{- end -}} +{{ printf "} \n}"}} +{{- end }} + +"KeyManagement": { + "Provider": "AESKey", + "AES": { + "Key": "rWnWbaFutavdoeqUiVYMNJGvmjQh31qaIej/vAxJ9G0=" + }, + "KeyVault": { + "Name": "k8spoc", + "KeyType": "RSA", + "KeyLength": "2048", + "MaximumDataLength": "214" + } + } \ No newline at end of file diff --git a/chart/templates/autoscaling-decryptor.yaml b/chart/templates/autoscaling-decryptor.yaml new file mode 100644 index 000000000..5efe0421f --- /dev/null +++ b/chart/templates/autoscaling-decryptor.yaml @@ -0,0 +1,19 @@ +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ template "kamus.name" . }}-decryptor + namespace: {{ .Values.team }} + labels: + app: {{ template "kamus.name" . }} + component: decryptor + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + scaleTargetRef: + apiVersion: apps/v1beta1 + kind: Deployment + name: {{ template "kamus.name" . }}-decryptor + minReplicas: {{ .Values.autoscale.minReplicas }} + maxReplicas: {{ .Values.autoscale.maxReplicas }} + targetCPUUtilizationPercentage: {{ .Values.autoscale.targetCPU }} \ No newline at end of file diff --git a/chart/templates/autoscaling-encryptor.yaml b/chart/templates/autoscaling-encryptor.yaml new file mode 100644 index 000000000..48187fdb7 --- /dev/null +++ b/chart/templates/autoscaling-encryptor.yaml @@ -0,0 +1,19 @@ +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ template "kamus.name" . }}-encryptor + namespace: {{ .Values.team }} + labels: + app: {{ template "kamus.name" . }} + component: encryptor + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + scaleTargetRef: + apiVersion: apps/v1beta1 + kind: Deployment + name: {{ template "kamus.name" . }}-encryptor + minReplicas: {{ .Values.autoscale.minReplicas }} + maxReplicas: {{ .Values.autoscale.maxReplicas }} + targetCPUUtilizationPercentage: {{ .Values.autoscale.targetCPU }} \ No newline at end of file diff --git a/chart/templates/configmap-decryptor.yaml b/chart/templates/configmap-decryptor.yaml new file mode 100644 index 000000000..a2396d0ac --- /dev/null +++ b/chart/templates/configmap-decryptor.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "kamus.name" . }}-decryptor + namespace: {{ .Values.team }} +data: + KeyManagement__Provider: {{ .Values.keyManagment.provider }} \ No newline at end of file diff --git a/chart/templates/configmap-encryptor.yaml b/chart/templates/configmap-encryptor.yaml new file mode 100644 index 000000000..79fa61dc7 --- /dev/null +++ b/chart/templates/configmap-encryptor.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "kamus.name" . }}-encryptor + namespace: {{ .Values.team }} +data: + KeyManagement__Provider: {{ .Values.keyManagment.provider }} \ No newline at end of file diff --git a/chart/templates/deployment-decryptor.yaml b/chart/templates/deployment-decryptor.yaml new file mode 100644 index 000000000..79a841fcf --- /dev/null +++ b/chart/templates/deployment-decryptor.yaml @@ -0,0 +1,65 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: {{ template "kamus.name" . }}-decryptor + namespace: {{ .Values.team }} + labels: + app: {{ template "kamus.name" . }} + component: decryptor + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + strategy: + rollingUpdate: + maxUnavailable: {{ .Values.maxUnavailable }} + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "kamus.name" . }} + release: {{ .Release.Name }} + component: decryptor + revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + template: + metadata: + labels: + app: {{ template "kamus.name" . }} + release: {{ .Release.Name }} + component: decryptor + spec: + serviceAccountName: {{ template "kamus.name" . }} + automountServiceAccountToken: true + containers: + - name: decryptor-api + image: {{ .Values.image.repository }}/kamus:decryptor-{{ .Values.image.version }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + volumeMounts: + - name: secret-volume + mountPath: /app/secrets + ports: + - containerPort: 9999 + livenessProbe: + httpGet: + path: /api/v1/isAlive + port: 9999 + readinessProbe: + httpGet: + path: /api/v1/isAlive + port: 9999 + resources: +{{ toYaml .Values.resources | indent 12 }} + envFrom: + - configMapRef: + name: {{ template "kamus.name" . }}-decryptor + {{- if .Values.imagePullSecrets }} + imagePullSecrets: + - name: {{ toYaml .Values.imagePullSecrets }} + {{- end }} + volumes: + - name: secret-volume + secret: + secretName: {{ template "kamus.name" . }} + {{- if .Values.nodeSelector }} + nodeSelector: +{{ toYaml .Values.nodeSelector | indent 8 }} + {{- end }} diff --git a/chart/templates/deployment-encryptor.yaml b/chart/templates/deployment-encryptor.yaml new file mode 100644 index 000000000..558ece710 --- /dev/null +++ b/chart/templates/deployment-encryptor.yaml @@ -0,0 +1,100 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: {{ template "kamus.name" . }}-encryptor + namespace: {{ .Values.team }} + labels: + app: {{ template "kamus.name" . }} + component: encryptor + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + strategy: + rollingUpdate: + maxUnavailable: {{ .Values.maxUnavailable }} + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "kamus.name" . }} + component: encryptor + release: {{ .Release.Name }} + revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + template: + metadata: + labels: + app: {{ template "kamus.name" . }} + component: encryptor + release: {{ .Release.Name }} + spec: + serviceAccountName: {{ template "kamus.name" . }} + automountServiceAccountToken: false + containers: + - name: encryptor-api + image: {{ .Values.image.repository }}/kamus:encryptor-{{ .Values.image.version }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + volumeMounts: + - name: secret-volume + mountPath: /app/secrets + ports: + - containerPort: 9999 + livenessProbe: + httpGet: + path: /api/v1/isAlive + port: 9999 + readinessProbe: + httpGet: + path: /api/v1/isAlive + port: 9999 + resources: +{{ toYaml .Values.resources | indent 12 }} + envFrom: + - configMapRef: + name: {{ template "kamus.name" . }}-encryptor + {{- if .Values.useAirbag }} + - name: "airbag" + image: "soluto/airbag:0.0.8" + ports: + - containerPort: {{ .Values.airbag.airbagPort }} + env: + - name: BACKEND_HOST_NAME + value: {{ .Values.airbag.backendHostName | quote }} + - name: BACKEND_SERVICE_PORT + value: "9999" + - name: UNAUTHENTICATED_ROUTES + value: '/api/v1/isAlive,/metrics' + - name: COLLECT_METRICS + value: {{ .Values.airbag.collectMetrics | quote }} + - name: ASPNETCORE_URLS + value: {{ print "http://+:" .Values.airbag.airbagPort }} + {{- if .Values.airbag.authority }} + - name: AUTHORITY + value: {{ .Values.airbag.authority | quote }} + - name: AUDIENCE + value: {{ .Values.airbag.audience | quote }} + - name: ISSUER + value: {{ .Values.airbag.issuer | quote }} + {{- end }} + livenessProbe: + httpGet: + path: {{ .Values.service.isAlivePath }} + port: {{ .Values.service.internalPort }} + readinessProbe: + httpGet: + path: {{ .Values.service.isAlivePath }} + port: {{ .Values.service.internalPort }} + resources: +{{ toYaml .Values.airbag.resources | indent 12 }} + {{- end }} + {{- if .Values.imagePullSecrets }} + imagePullSecrets: + - name: {{ toYaml .Values.imagePullSecrets }} + {{- end }} + volumes: + - name: secret-volume + secret: + secretName: {{ template "kamus.name" . }} + {{- if .Values.nodeSelector }} + nodeSelector: +{{ toYaml .Values.nodeSelector | indent 8 }} + {{- end }} diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml new file mode 100644 index 000000000..3ebc6777e --- /dev/null +++ b/chart/templates/ingress.yaml @@ -0,0 +1,40 @@ +{{- if .Values.ingress.enabled -}} +{{- $serviceName := include "kamus.name" . -}} +{{- $servicePort := .Values.service.externalPort -}} +{{- $servicePath := .Values.ingress.path | default "/" -}} +{{- $tlsSecretName := .Values.ingress.tls.secretName -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ template "kamus.name" . }} + namespace: {{ .Values.team }} + labels: + app: {{ template "kamus.name" . }} + component: encryptor + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- if .Values.ingress.annotations }} + annotations: +{{ toYaml .Values.ingress.annotations | indent 4 }} +{{- end }} +spec: + rules: + {{- range $host := .Values.ingress.hosts }} + - host: {{ $host }} + http: + paths: + - path: {{ $servicePath }} + backend: + serviceName: {{ $serviceName }} + servicePort: {{ $servicePort }} + {{- end }} + + tls: + - hosts: + {{- range $host := .Values.ingress.hosts }} + - {{ $host }} + {{- end }} + + secretName: {{ $tlsSecretName }} +{{- end -}} \ No newline at end of file diff --git a/chart/templates/role.yaml b/chart/templates/role.yaml new file mode 100644 index 000000000..cdf8e741f --- /dev/null +++ b/chart/templates/role.yaml @@ -0,0 +1,9 @@ +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + namespace: {{ .Values.team }} + name: {{ template "kamus.name" . }} +rules: +- apiGroups: ["authentication.k8s.io"] + resources: ["tokenreviews"] + verbs: ["create"] \ No newline at end of file diff --git a/chart/templates/rolebinding.yaml b/chart/templates/rolebinding.yaml new file mode 100644 index 000000000..85f47e3c3 --- /dev/null +++ b/chart/templates/rolebinding.yaml @@ -0,0 +1,13 @@ +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ template "kamus.name" . }} + namespace: {{ .Values.team }} +subjects: +- kind: ServiceAccount + name: {{ template "kamus.name" . }} + namespace: {{ .Values.team }} +roleRef: + kind: Role + name: {{ template "kamus.name" . }} + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/chart/templates/secret.yaml b/chart/templates/secret.yaml new file mode 100644 index 000000000..2615d677c --- /dev/null +++ b/chart/templates/secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + namespace: {{ .Values.team }} + name: {{ include "kamus.name" . }} +type: Opaque +data: + appsettings.secret.json: {{ include "appsettings.secret.json" . | b64enc}} \ No newline at end of file diff --git a/chart/templates/service-decryptor.yaml b/chart/templates/service-decryptor.yaml new file mode 100644 index 000000000..e92f503e1 --- /dev/null +++ b/chart/templates/service-decryptor.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "kamus.name" . }}-decryptor + namespace: {{ .Values.team }} +{{- if .Values.service.annotations }} + annotations: +{{ toYaml .Values.service.annotations | indent 4 }} +{{- end }} + labels: + app: {{ template "kamus.name" . }} + component: decryptor + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: 80 + targetPort: 9999 + protocol: TCP + name: {{ .Values.service.name }} + selector: + app: {{ template "kamus.name" . }} + component: decryptor + release: {{ .Release.Name }} diff --git a/chart/templates/service-encryptor.yaml b/chart/templates/service-encryptor.yaml new file mode 100644 index 000000000..bb6e15c0e --- /dev/null +++ b/chart/templates/service-encryptor.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "kamus.name" . }} + namespace: {{ .Values.team }} +{{- if .Values.service.annotations }} + annotations: +{{ toYaml .Values.service.annotations | indent 4 }} +{{- end }} + labels: + app: {{ template "kamus.name" . }}-decryptor + component: encryptor + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: 80 + {{- if .Values.useAirbag }} + targetPort: {{ .Values.airbag.airbagPort }} + {{- else }} + targetPort: 9999 + {{- end }} + protocol: TCP + name: {{ .Values.service.name }} + selector: + app: {{ template "kamus.name" . }} + component: encryptor + release: {{ .Release.Name }} diff --git a/chart/templates/serviceaccount.yaml b/chart/templates/serviceaccount.yaml new file mode 100644 index 000000000..635f1d808 --- /dev/null +++ b/chart/templates/serviceaccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "kamus.name" . }} + namespace: {{ .Values.team }} \ No newline at end of file diff --git a/chart/test.sh b/chart/test.sh new file mode 100755 index 000000000..ebed4986c --- /dev/null +++ b/chart/test.sh @@ -0,0 +1,54 @@ +set -e + +echo "Without airbag" +helm template . -f tests/values-without-airbag.yaml > template.yaml +cat template.yaml | kubetest -t tests/common --verbose +cat template.yaml | kubetest -t tests/without-airbag --verbose + +echo "With airbag" +helm template . -f tests/values-with-airbag.yaml > template.yaml +cat template.yaml | kubetest -t tests/common --verbose +cat template.yaml | kubetest -t tests/with-airbag --verbose + +echo "With annotations" +helm template . -f tests/values-with-annotations.yaml > template.yaml +cat template.yaml | kubetest -t tests/common --verbose +cat template.yaml | kubetest -t tests/with-annotations --verbose + +echo "With command and args" +helm template . -f tests/values-with-command-and-args.yaml > template.yaml +cat template.yaml | kubetest -t tests/common --verbose +cat template.yaml | kubetest -t tests/with-command-and-args --verbose + +echo "With custom probes" +helm template . -f tests/values-with-custom-probes.yaml > template.yaml +cat template.yaml | kubetest -t tests/common --verbose +cat template.yaml | kubetest -t tests/with-custom-probes --verbose + +echo "With role" +helm template . -f tests/values-with-role.yaml > template.yaml +cat template.yaml | kubetest -t tests/common --verbose +cat template.yaml | kubetest -t tests/with-role --verbose + +echo "with ingress path" +helm template . -f tests/values-with-ingress-path.yaml > template.yaml +cat template.yaml | kubetest -t tests/common --verbose +cat template.yaml | kubetest -t tests/with-ingress-path --verbose + +echo "with custom tls cert" +helm template . -f tests/values-with-custom-tls-cert.yaml > template.yaml +cat template.yaml | kubetest -t tests/common --verbose +cat template.yaml | kubetest -t tests/with-custom-tls-cert --verbose + +echo "with encrypted secrets" +helm template . -f tests/values-with-encrypted-secrets.yaml > template.yaml +cat template.yaml | kubetest -t tests/common --verbose +cat template.yaml | kubetest -t tests/with-encrypted-secrets --verbose + +echo "with long nameOverride" +helm template . -f tests/values-with-long-nameOverride.yaml > template.yaml +cat template.yaml | kubetest -t tests/with-long-nameOverride --verbose + +echo "with virtual service" +helm template . -f tests/values-with-virtual-service.yaml > template.yaml +cat template.yaml | kubetest -t tests/with-virtual-service --verbose diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 000000000..3b82f99fa --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,61 @@ +# Default values for kamus. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +replicaCount: 1 +maxUnavailable: 0 +revisionHistoryLimit: 5 +useAirbag: true +airbag: + backendHostName: "localhost" + collectMetrics: "true" + airbagPort: "7000" + resources: + limits: + cpu: 500m + memory: 600Mi + requests: + cpu: 100m + memory: 128Mi +image: + version: 1.0 + repository: soluto + pullPolicy: IfNotPresent +service: + name: nginx + type: NodePort + externalPort: 80 + internalPort: 7000 + isAlivePath: /api/v1/isAlive + annotations: + prometheus.io/scrape: "true" +ingress: + enabled: false + path: / + # Used to create an Ingress record. + hosts: + - chart-example.local + annotations: + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + tls: + # Secrets must be manually created in the namespace. + secretName: <> + # hosts: + # - chart-example.local +resources: + limits: + cpu: 500m + memory: 600Mi + requests: + cpu: 100m + memory: 128Mi +autoscale: + minReplicas: 2 + maxReplicas: 10 + targetCPU: 50 +envFrom: {} +container: {} +keyManagment: + provider: AESKey + AES: + key: rWnWbaFutavdoeqUiVYMNJGvmjQh31qaIej/vAxJ9G0= diff --git a/cli/.gitignore b/cli/.gitignore new file mode 100644 index 000000000..b512c09d4 --- /dev/null +++ b/cli/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/cli/encrypt.js b/cli/encrypt.js new file mode 100644 index 000000000..dbf431e1b --- /dev/null +++ b/cli/encrypt.js @@ -0,0 +1,140 @@ +var bluebird = require('bluebird'); +const opn = require('opn'); +const { AuthenticationContext } = require('adal-node'); +const activeDirectoryEndpoint = "https://login.microsoftonline.com/"; +const isDocker = require('./is-docker'); +const url = require('url') +const request = require('request'); +const {promisify} = require('util'); +var pjson = require('./package.json'); + +let _logger; + +module.exports = async (args, options, logger) => { + _logger = logger; + console.log(`options: ${JSON.stringify(options)}`); + if (useAuth(options)) { + const token = await acquireToken(options); + + await encrypt(args, options, token); + } + else { + await encrypt(args, options) + } +} + +const encrypt = async ({ data, serviceAccount, namespace }, { kamusUrl, allowInsecureUrl, certFingerprint }, token = null) => { + _logger.log('Encryption started...'); + _logger.log('service account:', serviceAccount); + _logger.log('namespace:', namespace); + + if (!allowInsecureUrl && url.parse(kamusUrl).protocol == 'http'){ + _logger.error("Insecure Kamus URL is not allowed"); + process.exit(1); + } + + try { + var response = await performEncryptRequestAsync(data, serviceAccount, namespace, kamusUrl, certFingerprint, token) + if (response.statusCode >= 300) { + _logger.error(`Encrypt request failed due to unexpected error. Status code: ${response.statusCode}`); + process.exit(1); + } + _logger.info(`Successfully encrypted data to ${serviceAccount} service account in ${namespace} namespace`); + _logger.info('Encrypted data:\n' + response.body); + process.exit(0); + } + catch (err) { + _logger.error('Error while trying to encrypt with kamus:', err.message); + process.exit(1); + } +} + +const handleEncryptionError = (response) => { + _logger.error('Error while trying to encrypt with kamus'); + if (response.status == 400) { + _logger.error('Server returned bad request, make sure the service account and namespace exists'); + } + if (response.status == 403) { + _logger.error('Server returned authentication error, make sure your user has access rights to kamus'); + } + process.exit(1); +} + +const acquireToken = async ({ authTenant, authApplication, authResource }) => { + const context = new AuthenticationContext(activeDirectoryEndpoint + authTenant); + bluebird.promisifyAll(context); + refreshToken = await acquireTokenWithDeviceCode(context, authApplication, authResource); + const refreshTokenResponse = + await context.acquireTokenWithRefreshTokenAsync(refreshToken, authApplication, null, authResource); + return refreshTokenResponse.accessToken; +}; + +const acquireTokenWithDeviceCode = async (context, authApplication, authResource) => { + const userCodeResult = await context.acquireUserCodeAsync(authResource, authApplication, 'en'); + await outputUserCodeInstructions(userCodeResult); + const deviceCodeResult = + await context.acquireTokenWithDeviceCodeAsync(authResource, authApplication, userCodeResult); + return deviceCodeResult.refreshToken; +}; + +const outputUserCodeInstructions = async (userCodeResult) => { + if (isDocker()) { + _logger.info(`Login to https://microsoft.com/devicelogin Enter this code to authenticate: ${userCodeResult.userCode}`) + } else { + opn(userCodeResult.verificationUrl); + _logger.info(`Enter this code to authenticate: ${userCodeResult.userCode}`); + } +} + +const useAuth = ({ authTenant, authApplication, authResource }) => { + if (authTenant && authApplication && authResource) { + return true; + } + else { + _logger.warn('Auth options were not provided, will try to encrypt without authentication to kamus'); + return false; + } +} + +//Source: http://hassansin.github.io/certificate-pinning-in-nodejs +const performEncryptRequest = (data, serviceAccount, namespace, kamusUrl, certficateFingerprint, token, cb) => { + + var headers = { + 'User-Agent': `kamus-cli-${pjson.version}`, + 'Content-Type': 'application/json' + }; + + if (token != null) { + headers['Authorization'] = `Bearer ${token}` + } + + var options = { + url: kamusUrl + '/api/v1/encrypt', + headers: headers, + // Certificate validation + strictSSL: true, + method: 'POST', + }; + + var req = request(options, cb); + + req.on('socket', socket => { + socket.on('secureConnect', () => { + var fingerprint = socket.getPeerCertificate().fingerprint; + // Match the fingerprint with our saved fingerprints + if(certficateFingerprint != undefined && certficateFingerprint != fingerprint){ + // Abort request, optionally emit an error event + req.emit('error', new Error(`Server fingerprint ${fingerprint} does not match provided fingerprint ${certficateFingerprint}`)); + return req.abort(); + } + }); + }); + + req.write(JSON.stringify({ + data, + "service-account": serviceAccount, + namespace + })); +} + +performEncryptRequestAsync = promisify(performEncryptRequest); \ No newline at end of file diff --git a/cli/index.js b/cli/index.js new file mode 100644 index 000000000..fac658d37 --- /dev/null +++ b/cli/index.js @@ -0,0 +1,20 @@ +var pjson = require('./package.json'); +const prog = require('caporal'); +const encrypt = require('./encrypt'); +const regexGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +prog + .version(pjson.version) + .command('encrypt', 'Encrypt data') + .argument('','Data to encrypt') + .argument('', 'Deployment service account') + .argument('', 'Deployment namespace') + .action(encrypt) + .option('--auth-tenant ', 'Azure tenant id', regexGuid) + .option('--auth-application ', 'Azure application id', regexGuid) + .option('--auth-resource ', 'Azure resource name', prog.STRING) + .option('--kamus-url ', 'Kamus URL', prog.REQUIRED) + .option('--allow-insecure-url', 'Allow insecure (http) Kamus URL', prog.BOOL) + .option('--cert-fingerprint ', 'Force server certificate to match the given fingerprint', prog.STRING); + +prog.parse(process.argv); \ No newline at end of file diff --git a/cli/is-docker.js b/cli/is-docker.js new file mode 100644 index 000000000..762d603ec --- /dev/null +++ b/cli/is-docker.js @@ -0,0 +1,32 @@ +var fs = require('fs'); + +var isDocker; + +function hasDockerEnv() { + try { + fs.statSync('/.dockerenv'); + return true; + } catch (err) { + return false; + } +} + +function hasDockerCGroup() { + try { + return fs.readFileSync('/proc/self/cgroup', 'utf8').indexOf('docker') !== -1; + } catch (err) { + return false; + } +} + +function check() { + return hasDockerEnv() || hasDockerCGroup(); +} + +module.exports = function () { + if (isDocker === undefined) { + isDocker = check(); + } + + return isDocker; +}; \ No newline at end of file diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 000000000..158daac53 --- /dev/null +++ b/cli/package.json @@ -0,0 +1,31 @@ +{ + "name": "kamus-cli", + "version": "0.0.1", + "description": "CLI Tool to encrypt secrets for kamus", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/soluto/kamus.git" + }, + "keywords": [ + "Secrets", + "Kubernetes", + "Kamus" + ], + "author": "Shai Katz", + "license": "ISC", + "bugs": { + "url": "https://github.com/soluto/kamus/issues" + }, + "homepage": "https://github.com/soluto/kamus#readme", + "dependencies": { + "adal-node": "^0.1.28", + "bluebird": "^3.5.3", + "caporal": "^1.1.0", + "node-fetch": "^2.3.0", + "opn": "^5.4.0" + } +} diff --git a/cli/yarn.lock b/cli/yarn.lock new file mode 100644 index 000000000..f134e4a73 --- /dev/null +++ b/cli/yarn.lock @@ -0,0 +1,946 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@^8.0.47": + version "8.10.39" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.39.tgz#e7e87ad00364dd7bc485c940926345b8ec1a26ca" + integrity sha512-rE7fktr02J8ybFf6eysife+WF+L4sAHWzw09DgdCebEu+qDwMvv4zl6Bc+825ttGZP73kCKxa3dhJOoGJ8+5mA== + +adal-node@^0.1.28: + version "0.1.28" + resolved "https://registry.yarnpkg.com/adal-node/-/adal-node-0.1.28.tgz#468c4bb3ebbd96b1270669f4b9cba4e0065ea485" + integrity sha1-RoxLs+u9lrEnBmn0ucuk4AZepIU= + dependencies: + "@types/node" "^8.0.47" + async ">=0.6.0" + date-utils "*" + jws "3.x.x" + request ">= 2.52.0" + underscore ">= 1.3.1" + uuid "^3.1.0" + xmldom ">= 0.1.x" + xpath.js "~1.1.0" + +ajv@^6.5.5: + version "6.6.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.6.2.tgz#caceccf474bf3fc3ce3b147443711a24063cc30d" + integrity sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +ansi@^0.3.0, ansi@~0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21" + integrity sha1-DELU+xcWDVqa8eSEus4cZpIsGyE= + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +async@>=0.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" + integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== + dependencies: + lodash "^4.17.10" + +async@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" + integrity sha1-+PwEyjoTeErenhZBr5hXjPvWR6k= + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +bluebird@^3.4.7, bluebird@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" + integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +caporal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/caporal/-/caporal-1.1.0.tgz#834fd2348ab6ae79171a023a975ccc6f04620533" + integrity sha512-R5qo2QGoqBM6RvzHonGhUuEJSeqEa4lD1r+cPUEY2+YsXhpQVTS2TvScfIbi6ydFdhzFCNeNUB1v0YrRBvsbdg== + dependencies: + bluebird "^3.4.7" + cli-table3 "^0.5.0" + colorette "1.0.1" + fast-levenshtein "^2.0.6" + lodash.camelcase "^4.3.0" + lodash.kebabcase "^4.1.1" + lodash.merge "^4.6.0" + micromist "1.1.0" + prettyjson "^1.2.1" + tabtab "^2.2.2" + winston "^2.3.1" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +cli-cursor@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc= + dependencies: + restore-cursor "^1.0.1" + +cli-table3@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" + integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== + dependencies: + object-assign "^4.1.0" + string-width "^2.1.1" + optionalDependencies: + colors "^1.1.2" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +colorette@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.0.1.tgz#434bad4bd70969c075162fec86ca55da36bf837c" + integrity sha512-40MnlppkzHhFjRhtXunbpqKUT+eJn0gyVGi8aQlNSG8T2CCy31NdD7yktcS0aizH1VP2OhhQCyGMeTp0a/fvaw== + +colors@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= + +colors@^1.1.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" + integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" + integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== + dependencies: + delayed-stream "~1.0.0" + +concat-stream@^1.4.7: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cycle@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" + integrity sha1-IegLK+hYD5i0aPN5QwZisEbDStI= + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +date-utils@*: + version "1.2.21" + resolved "https://registry.yarnpkg.com/date-utils/-/date-utils-1.2.21.tgz#61fb16cdc1274b3c9acaaffe9fc69df8720a2b64" + integrity sha1-YfsWzcEnSzyayq/+n8ad+HIKK2Q= + +debug@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + integrity sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM= + dependencies: + safe-buffer "^5.0.1" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g= + +extend@^3.0.0, extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +external-editor@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-1.1.1.tgz#12d7b0db850f7ff7e7081baf4005700060c4600b" + integrity sha1-Etew24UPf/fnCBuvQAVwAGDEYAs= + dependencies: + extend "^3.0.0" + spawn-sync "^1.0.15" + tmp "^0.0.29" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +eyes@0.1.x: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A= + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +gauge@~1.2.5: + version "1.2.7" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-1.2.7.tgz#e9cec5483d3d4ee0ef44b60a7d99e4935e136d93" + integrity sha1-6c7FSD09TuDvRLYKfZnkk14TbZM= + dependencies: + ansi "^0.3.0" + has-unicode "^2.0.0" + lodash.pad "^4.1.0" + lodash.padend "^4.1.0" + lodash.padstart "^4.1.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +inherits@^2.0.3, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +inquirer@^1.0.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-1.2.3.tgz#4dec6f32f37ef7bb0b2ed3f1d1a5c3f545074918" + integrity sha1-TexvMvN+97sLLtPx0aXD9UUHSRg= + dependencies: + ansi-escapes "^1.1.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + external-editor "^1.1.0" + figures "^1.3.5" + lodash "^4.3.0" + mute-stream "0.0.6" + pinkie-promise "^2.0.0" + run-async "^2.2.0" + rx "^4.1.0" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isstream@0.1.x, isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jwa@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + integrity sha512-tBO/cf++BUsJkYql/kBbJroKOgHWEigTKBAjjBEmrMGYd1QMBC74Hr4Wo2zCZw6ZrVhlJPvoMrkcOnlWR/DJfw== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.10" + safe-buffer "^5.0.1" + +jws@3.x.x: + version "3.1.5" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + integrity sha512-GsCSexFADNQUr8T5HPJvayTjvPIfoyJPtLQBwn5a4WZQchcrPMPMAWcC1AzJVRDKyD6ZPROPAxgv6rfHViO4uQ== + dependencies: + jwa "^1.1.5" + safe-buffer "^5.0.1" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= + +lodash.difference@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw= + +lodash.kebabcase@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" + integrity sha1-hImxyw0p/4gZXM7KRI/21swpXDY= + +lodash.merge@^4.6.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" + integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ== + +lodash.pad@^4.1.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/lodash.pad/-/lodash.pad-4.5.1.tgz#4330949a833a7c8da22cc20f6a26c4d59debba70" + integrity sha1-QzCUmoM6fI2iLMIPaibE1Z3runA= + +lodash.padend@^4.1.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e" + integrity sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4= + +lodash.padstart@^4.1.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.padstart/-/lodash.padstart-4.6.1.tgz#d2e3eebff0d9d39ad50f5cbd1b52a7bce6bb611b" + integrity sha1-0uPuv/DZ05rVD1y9G1KnvOa7YRs= + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + +lodash@^4.17.10, lodash@^4.3.0: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +micromist@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromist/-/micromist-1.1.0.tgz#a490bcf9a4b918ad9eed8e52d0ec98b9c3b2d3c8" + integrity sha512-+CQ76pabE9egniSEdmDuH+j2cYyIBKP97kujG8ZLZyLCRq5ExwtIy4DPHPFrq4jVbhMRBnyjuH50KU9Ohs8QCg== + dependencies: + lodash.camelcase "^4.3.0" + +mime-db@~1.37.0: + version "1.37.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" + integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.21" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" + integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg== + dependencies: + mime-db "~1.37.0" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +mute-stream@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db" + integrity sha1-SJYrGeFp/R38JAs/HnMXYnu8R9s= + +node-fetch@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5" + integrity sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA== + +npmlog@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-2.0.4.tgz#98b52530f2514ca90d09ec5b22c8846722375692" + integrity sha1-mLUlMPJRTKkNCexbIsiEZyI3VpI= + dependencies: + ansi "~0.3.1" + are-we-there-yet "~1.1.2" + gauge "~1.2.5" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k= + +opn@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035" + integrity sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw== + dependencies: + is-wsl "^1.1.0" + +os-shim@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" + integrity sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc= + +os-tmpdir@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +prettyjson@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prettyjson/-/prettyjson-1.2.1.tgz#fcffab41d19cab4dfae5e575e64246619b12d289" + integrity sha1-/P+rQdGcq0365eV15kJGYZsS0ok= + dependencies: + colors "^1.1.2" + minimist "^1.2.0" + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + +psl@^1.1.24: + version "1.1.31" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184" + integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw== + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +readable-stream@^2.0.6, readable-stream@^2.2.2: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +"request@>= 2.52.0": + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE= + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= + dependencies: + is-promise "^2.1.0" + +rx@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" + integrity sha1-pfE/957zt0D+MKqAP7CfmIBdR4I= + +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +spawn-sync@^1.0.15: + version "1.0.15" + resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476" + integrity sha1-sAeZVX63+wyDdsKdROih6mfldHY= + dependencies: + concat-stream "^1.4.7" + os-shim "^0.1.2" + +sshpk@^1.7.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.15.2.tgz#c946d6bd9b1a39d0e8635763f5242d6ed6dcb629" + integrity sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +tabtab@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tabtab/-/tabtab-2.2.2.tgz#7a047f143b010b4cbd31f857e82961512cbf4e14" + integrity sha1-egR/FDsBC0y9MfhX6ClhUSy/ThQ= + dependencies: + debug "^2.2.0" + inquirer "^1.0.2" + lodash.difference "^4.5.0" + lodash.uniq "^4.5.0" + minimist "^1.2.0" + mkdirp "^0.5.1" + npmlog "^2.0.3" + object-assign "^4.1.0" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +tmp@^0.0.29: + version "0.0.29" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0" + integrity sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA= + dependencies: + os-tmpdir "~1.0.1" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +"underscore@>= 1.3.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" + integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +uuid@^3.1.0, uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +winston@^2.3.1: + version "2.4.4" + resolved "https://registry.yarnpkg.com/winston/-/winston-2.4.4.tgz#a01e4d1d0a103cf4eada6fc1f886b3110d71c34b" + integrity sha512-NBo2Pepn4hK4V01UfcWcDlmiVTs7VTB1h7bgnB0rgP146bYhMxX0ypCz3lBOfNxCO4Zuek7yeT+y/zM1OfMw4Q== + dependencies: + async "~1.0.0" + colors "1.0.x" + cycle "1.0.x" + eyes "0.1.x" + isstream "0.1.x" + stack-trace "0.0.x" + +"xmldom@>= 0.1.x": + version "0.1.27" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" + integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk= + +xpath.js@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1" + integrity sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ== diff --git a/src/Controllers/EncryptController.cs b/src/decrypt-api/Controllers/DecryptController.cs similarity index 59% rename from src/Controllers/EncryptController.cs rename to src/decrypt-api/Controllers/DecryptController.cs index 1f7db99f8..d95007b68 100644 --- a/src/Controllers/EncryptController.cs +++ b/src/decrypt-api/Controllers/DecryptController.cs @@ -14,53 +14,22 @@ namespace Hamuste.Controllers { - public class EncryptController : Controller + public class DecryptController : Controller { private readonly IKubernetes mKubernetes; private readonly IKeyManagement mKeyManagement; - private readonly ILogger mAuditLogger = Log.ForContext().AsAudit(); - private readonly ILogger mLogger = Log.ForContext(); + private readonly ILogger mAuditLogger = Log.ForContext().AsAudit(); + private readonly ILogger mLogger = Log.ForContext(); //see: https://github.com/kubernetes/kubernetes/blob/d5803e596fc8aba17aa8c74a96aff9c73bb0f1da/staging/src/k8s.io/apiserver/pkg/authentication/serviceaccount/util.go#L27 private const string ServiceAccountUsernamePrefix = "system:serviceaccount:"; - public EncryptController(IKubernetes kubernetes, IKeyManagement keyManagement) + public DecryptController(IKubernetes kubernetes, IKeyManagement keyManagement) { mKubernetes = kubernetes; mKeyManagement = keyManagement; } - [HttpPost] - [Route("api/v1/encrypt")] - public async Task Encrypt([FromBody]EncryptRequest body) - { - mAuditLogger.Information("Encryption request started, SourceIP: {sourceIp}, ServiceAccount: {sa}, Namespace: {namespace}", - Request.HttpContext.Connection.RemoteIpAddress, - body.SerivceAccountName, - body.NamesapceName); - - try - { - await mKubernetes.ReadNamespacedServiceAccountAsync(body.SerivceAccountName, body.NamesapceName, true); - } - catch (HttpOperationException e) when (e.Response.StatusCode == HttpStatusCode.NotFound) - { - mLogger.Warning(e, "Service account {serviceAccount} not found in namespace {namespace}", - body.SerivceAccountName, - body.NamesapceName); - return BadRequest(); - } - - var encryptedData = await mKeyManagement.Encrypt(body.Data, $"{body.NamesapceName}:{body.SerivceAccountName}"); - - mAuditLogger.Information("Encryption request succeeded, SourceIP: {sourceIp}, ServiceAccount: {serviceAccount}, Namesacpe: {namespace}", - Request.HttpContext.Connection.RemoteIpAddress, - body.SerivceAccountName, - body.NamesapceName); - - return Content(encryptedData); - } - [HttpPost] [Route("api/v1/decrypt")] [Authorize(AuthenticationSchemes = "kubernetes")] diff --git a/src/Controllers/MonitoringController.cs b/src/decrypt-api/Controllers/MonitoringController.cs similarity index 100% rename from src/Controllers/MonitoringController.cs rename to src/decrypt-api/Controllers/MonitoringController.cs diff --git a/src/ErrorHandlingMiddleware.cs b/src/decrypt-api/ErrorHandlingMiddleware.cs similarity index 100% rename from src/ErrorHandlingMiddleware.cs rename to src/decrypt-api/ErrorHandlingMiddleware.cs diff --git a/src/Extensions/LoggingExtensions.cs b/src/decrypt-api/Extensions/LoggingExtensions.cs similarity index 100% rename from src/Extensions/LoggingExtensions.cs rename to src/decrypt-api/Extensions/LoggingExtensions.cs diff --git a/src/decrypt-api/KubernetesAuthentication/KubernetesAuthenticationHandler.cs b/src/decrypt-api/KubernetesAuthentication/KubernetesAuthenticationHandler.cs new file mode 100644 index 000000000..7170c36d1 --- /dev/null +++ b/src/decrypt-api/KubernetesAuthentication/KubernetesAuthenticationHandler.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using k8s; +using k8s.Models; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Hamuste.KubernetesAuthentication +{ + public class KubernetesAuthenticationHandler : AuthenticationHandler + { + private readonly IKubernetes mKubernetes; + + public KubernetesAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock, + IKubernetes kubernetes) : base(options, logger, encoder, clock) + { + mKubernetes = kubernetes; + } + + protected override async Task HandleAuthenticateAsync() + { + string token = ""; + string authorization = Request.Headers["Authorization"]; + + // If no authorization header found, nothing to process further + if (string.IsNullOrEmpty(authorization)) + { + return AuthenticateResult.NoResult(); + } + + if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + token = authorization.Substring("Bearer ".Length).Trim(); + } + + // If no token found, no further work possible + if (string.IsNullOrEmpty(token)) + { + return AuthenticateResult.NoResult(); + } + + var reviewResult = await mKubernetes.CreateTokenReviewAsync(new V1TokenReview + { + Spec = new V1TokenReviewSpec + { + Token = token + } + }); + + if (!reviewResult.Status.Authenticated.HasValue || !reviewResult.Status.Authenticated.Value) { + //todo: improve logging + return AuthenticateResult.Fail(reviewResult.Status.Error); + } + + + var claims = new List { + new Claim("sub", reviewResult.Status.User.Uid), + new Claim("name", reviewResult.Status.User.Username), + new Claim("Groups", string.Join(",", reviewResult.Status.User.Groups)) + }; + + + return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new List { new ClaimsIdentity(claims, "kubernetes") }), "kubernetes")); + + } + } +} diff --git a/src/KubernetesAuthentication/KubernetesAuthenticationOptions.cs b/src/decrypt-api/KubernetesAuthentication/KubernetesAuthenticationOptions.cs similarity index 100% rename from src/KubernetesAuthentication/KubernetesAuthenticationOptions.cs rename to src/decrypt-api/KubernetesAuthentication/KubernetesAuthenticationOptions.cs diff --git a/src/Models/DecryptRequest.cs b/src/decrypt-api/Models/DecryptRequest.cs similarity index 100% rename from src/Models/DecryptRequest.cs rename to src/decrypt-api/Models/DecryptRequest.cs diff --git a/src/Program.cs b/src/decrypt-api/Program.cs similarity index 100% rename from src/Program.cs rename to src/decrypt-api/Program.cs diff --git a/src/Properties/launchSettings.json b/src/decrypt-api/Properties/launchSettings.json similarity index 100% rename from src/Properties/launchSettings.json rename to src/decrypt-api/Properties/launchSettings.json diff --git a/src/Startup.cs b/src/decrypt-api/Startup.cs similarity index 100% rename from src/Startup.cs rename to src/decrypt-api/Startup.cs diff --git a/src/appsettings.Development.json b/src/decrypt-api/appsettings.Development.json similarity index 100% rename from src/appsettings.Development.json rename to src/decrypt-api/appsettings.Development.json diff --git a/src/appsettings.json b/src/decrypt-api/appsettings.json similarity index 100% rename from src/appsettings.json rename to src/decrypt-api/appsettings.json diff --git a/src/Hamuste.csproj b/src/decrypt-api/decrypt-api.csproj similarity index 93% rename from src/Hamuste.csproj rename to src/decrypt-api/decrypt-api.csproj index a36c3cdca..cabe0a5b7 100644 --- a/src/Hamuste.csproj +++ b/src/decrypt-api/decrypt-api.csproj @@ -6,13 +6,15 @@ + + + - diff --git a/src/encrypt-api/Controllers/EncryptController.cs b/src/encrypt-api/Controllers/EncryptController.cs new file mode 100644 index 000000000..5042eab88 --- /dev/null +++ b/src/encrypt-api/Controllers/EncryptController.cs @@ -0,0 +1,54 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using System.Linq; +using Hamuste.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Rest; +using Hamuste.Extensions; +using Hamuste.KeyManagement; +using Serilog; + +namespace Hamuste.Controllers +{ + + public class EncryptController : Controller + { + private readonly IKeyManagement mKeyManagement; + private readonly ILogger mAuditLogger = Log.ForContext().AsAudit(); + private readonly ILogger mLogger = Log.ForContext(); + + //see: https://github.com/kubernetes/kubernetes/blob/d5803e596fc8aba17aa8c74a96aff9c73bb0f1da/staging/src/k8s.io/apiserver/pkg/authentication/serviceaccount/util.go#L27 + private const string ServiceAccountUsernamePrefix = "system:serviceaccount:"; + + public EncryptController(IKeyManagement keyManagement) + { + mKeyManagement = keyManagement; + } + + [HttpPost] + [Route("api/v1/encrypt")] + public async Task Encrypt([FromBody]EncryptRequest body) + { + mAuditLogger.Information("Encryption request started, SourceIP: {sourceIp}, ServiceAccount: {sa}, Namespace: {namespace}", + Request.HttpContext.Connection.RemoteIpAddress, + body.SerivceAccountName, + body.NamesapceName); + + var encryptedData = await mKeyManagement.Encrypt(body.Data, $"{body.NamesapceName}:{body.SerivceAccountName}"); + + if (body.SerivceAccountName == "default") + { + return BadRequest("You cannot encrypt a secret for the default service account"); + } + + mAuditLogger.Information("Encryption request succeeded, SourceIP: {sourceIp}, ServiceAccount: {serviceAccount}, Namesacpe: {namespace}", + Request.HttpContext.Connection.RemoteIpAddress, + body.SerivceAccountName, + body.NamesapceName); + + return Content(encryptedData); + } + } +} diff --git a/src/encrypt-api/Controllers/MonitoringController.cs b/src/encrypt-api/Controllers/MonitoringController.cs new file mode 100644 index 000000000..4e88aa1cb --- /dev/null +++ b/src/encrypt-api/Controllers/MonitoringController.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Hamuste.Controllers +{ + public class MonitoringController + { + [HttpGet] + [Route("api/v1/isAlive")] + public bool IsAlive() + { + return true; + } + + [HttpGet] + [Route("")] + public string Welcome() + { + return "welcome"; + } + } +} \ No newline at end of file diff --git a/src/encrypt-api/ErrorHandlingMiddleware.cs b/src/encrypt-api/ErrorHandlingMiddleware.cs new file mode 100644 index 000000000..f9ad6668d --- /dev/null +++ b/src/encrypt-api/ErrorHandlingMiddleware.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Hamuste +{ + // You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project + public class ErrorHandlingMiddleware + { + private readonly RequestDelegate mNext; + private readonly ILogger mLogger; + + public ErrorHandlingMiddleware(RequestDelegate next, ILogger logger) + { + mNext = next; + mLogger = logger; + } + + public async Task Invoke(HttpContext httpContext) + { + try + { + await mNext.Invoke(httpContext); + } catch (Exception e) { + mLogger.LogError(e, $"Unhandled exception while processing request"); + httpContext.Response.StatusCode = 500; + await httpContext.Response.WriteAsync("server failed to handle response"); + } + } + } + + // Extension method used to add the middleware to the HTTP request pipeline. + public static class ErrorHandlingMiddlewareExtensions + { + public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/src/encrypt-api/Extensions/LoggingExtensions.cs b/src/encrypt-api/Extensions/LoggingExtensions.cs new file mode 100644 index 000000000..e7ce42dd7 --- /dev/null +++ b/src/encrypt-api/Extensions/LoggingExtensions.cs @@ -0,0 +1,12 @@ +using Serilog; + +namespace Hamuste.Extensions +{ + public static class LoggingExtensions + { + public static ILogger AsAudit(this ILogger logger) + { + return logger.ForContext("log_type", "audit"); + } + } +} \ No newline at end of file diff --git a/src/KubernetesAuthentication/KubernetesAuthenticationHandler.cs b/src/encrypt-api/KubernetesAuthentication/KubernetesAuthenticationHandler.cs similarity index 100% rename from src/KubernetesAuthentication/KubernetesAuthenticationHandler.cs rename to src/encrypt-api/KubernetesAuthentication/KubernetesAuthenticationHandler.cs diff --git a/src/encrypt-api/KubernetesAuthentication/KubernetesAuthenticationOptions.cs b/src/encrypt-api/KubernetesAuthentication/KubernetesAuthenticationOptions.cs new file mode 100644 index 000000000..84e802783 --- /dev/null +++ b/src/encrypt-api/KubernetesAuthentication/KubernetesAuthenticationOptions.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authentication; + +namespace Hamuste.KubernetesAuthentication +{ + public class KubernetesAuthenticationOptions : AuthenticationSchemeOptions + { + } +} diff --git a/src/Models/EncryptRequest.cs b/src/encrypt-api/Models/EncryptRequest.cs similarity index 100% rename from src/Models/EncryptRequest.cs rename to src/encrypt-api/Models/EncryptRequest.cs diff --git a/src/encrypt-api/Program.cs b/src/encrypt-api/Program.cs new file mode 100644 index 000000000..995459671 --- /dev/null +++ b/src/encrypt-api/Program.cs @@ -0,0 +1,28 @@ +using App.Metrics.AspNetCore; +using App.Metrics.Formatters.Prometheus; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Serilog; + +namespace Hamuste +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseMetrics(options => { + options.EndpointOptions = endpointsOptions => { + endpointsOptions.MetricsEndpointOutputFormatter = new MetricsPrometheusTextOutputFormatter(); + }; + }) + + .UseStartup() + .UseSerilog() + .Build(); + } +} diff --git a/src/encrypt-api/Properties/launchSettings.json b/src/encrypt-api/Properties/launchSettings.json new file mode 100644 index 000000000..3155711ba --- /dev/null +++ b/src/encrypt-api/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:49615/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "api": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:49621/" + } + } +} \ No newline at end of file diff --git a/src/encrypt-api/Startup.cs b/src/encrypt-api/Startup.cs new file mode 100644 index 000000000..186a8cae9 --- /dev/null +++ b/src/encrypt-api/Startup.cs @@ -0,0 +1,121 @@ +using System; +using System.Threading.Tasks; +using Hamuste.KeyManagement; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.KeyVault; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Clients.ActiveDirectory; +using Serilog; + +namespace Hamuste +{ + public class Startup { + + public Startup(IHostingEnvironment env) + { + string appsettingsPath = "appsettings.json"; + + if (env.IsDevelopment()) + { + appsettingsPath = "appsettings.Development.json"; + } + + Console.WriteLine($"Root: {env.ContentRootPath}"); + + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile(appsettingsPath, optional: true, reloadOnChange: true) + .AddJsonFile("secrets/appsettings.secrets.json", optional: true) + .AddEnvironmentVariables(); + + Configuration = builder.Build(); + } + + + public IConfiguration Configuration; + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices (IServiceCollection services) { + + services.AddMvc().AddMetrics(); + + services.AddSwaggerGen (swagger => { + swagger.SwaggerDoc ("v1", new Swashbuckle.AspNetCore.Swagger.Info { Title = "Hamuste Swagger" }); + }); + + services.AddScoped(s => + { + var provider = Configuration.GetValue("KeyManagement:Provider"); + switch (provider) + { + case "AzureKeyVault": + return new EnvelopeEncryptionDecorator( + new AzureKeyVaultKeyManagement(s.GetService(), Configuration), + new SymmetricKeyManagement(), + Configuration.GetValue("KeyManagement:KeyVault:MaximumDataLength")); + case "AESKey": + var key = Configuration.GetValue("KeyManagement:AES:Key"); + if (string.IsNullOrEmpty(key)) + { + Log.ForContext().Warning("Random key was created for SymmetricKeyManagement, it might break distributed deployments"); + } + return new SymmetricKeyManagement(key); + default: + throw new InvalidOperationException($"Unsupported provider type: {provider}"); + } + }); + + services.AddSingleton(_ => new KeyVaultClient(GetToken)); + + services.AddSingleton(Configuration); + + services.AddSingleton(); + } + + public async Task GetToken(string authority, string resource, string scope) + { + var clientId = Configuration["ActiveDirectory:ClientId"]; + var clientSecret = Configuration["ActiveDirectory:ClientSecret"]; + + var authContext = new AuthenticationContext(authority); + ClientCredential clientCred = new ClientCredential(clientId, clientSecret); + AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred); + + if (result == null) + throw new InvalidOperationException("Failed to obtain the JWT token"); + + return result.AccessToken; + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure (IApplicationBuilder app, IHostingEnvironment env) { + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionMiddleware(); + } + + Log.Logger = new LoggerConfiguration () + .ReadFrom.Configuration (Configuration) + .CreateLogger (); + + + app.UseSwagger (); + app.UseSwaggerUI (c => { + c.SwaggerEndpoint ("/swagger/v1/swagger.json", "Hamuste Swagger"); + }); + + app.UseAuthentication(); + + app.UseMvc (); + } + } +} \ No newline at end of file diff --git a/src/encrypt-api/appsettings.Development.json b/src/encrypt-api/appsettings.Development.json new file mode 100644 index 000000000..855f56146 --- /dev/null +++ b/src/encrypt-api/appsettings.Development.json @@ -0,0 +1,34 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console" ], + "MinimumLevel": "Information", + "WriteTo": [ + { + "Name": "Console", + "Args": { + "formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog" + } + } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ] + }, + "Kubernetes": { + "ProxyUrl": "http://localhost:8080" + }, + "ActiveDirectory": { + "ClientId": "4ff97fae-1841-4558-863d-f85d16d23229", + "ClientSecret": "D9qHJHL9reeyhqMki3uQNhzr2TP8iyWv0TE6TK4GKJw=" + }, + "KeyManagement": { + "Provider": "AESKey", + "AES": { + "Key": "rWnWbaFutavdoeqUiVYMNJGvmjQh31qaIej/vAxJ9G0=" + }, + "KeyVault": { + "Name": "k8spoc", + "KeyType": "RSA", + "KeyLength": "2048", + "MaximumDataLength": "214" + } + } +} diff --git a/src/encrypt-api/appsettings.json b/src/encrypt-api/appsettings.json new file mode 100644 index 000000000..d1e134edd --- /dev/null +++ b/src/encrypt-api/appsettings.json @@ -0,0 +1,18 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console" ], + "MinimumLevel": "Information", + "WriteTo": [ + { + "Name": "Console", + "Args": { + "formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog" + } + } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ] + }, + "KeyManagement": { + "Provider": "AzureKeyVault" + } +} \ No newline at end of file diff --git a/src/encrypt-api/encrypt-api.csproj b/src/encrypt-api/encrypt-api.csproj new file mode 100644 index 000000000..4efaf0f5a --- /dev/null +++ b/src/encrypt-api/encrypt-api.csproj @@ -0,0 +1,33 @@ + + + netcoreapp2.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/KeyManagement/AzureKeyVaultKeyManagement.cs b/src/key-managment/AzureKeyVaultKeyManagement.cs similarity index 100% rename from src/KeyManagement/AzureKeyVaultKeyManagement.cs rename to src/key-managment/AzureKeyVaultKeyManagement.cs diff --git a/src/KeyManagement/EnvelopeEncryptionDecorator.cs b/src/key-managment/EnvelopeEncryptionDecorator.cs similarity index 100% rename from src/KeyManagement/EnvelopeEncryptionDecorator.cs rename to src/key-managment/EnvelopeEncryptionDecorator.cs diff --git a/src/KeyManagement/IKeyManagement.cs b/src/key-managment/IKeyManagement.cs similarity index 100% rename from src/KeyManagement/IKeyManagement.cs rename to src/key-managment/IKeyManagement.cs diff --git a/src/KeyManagement/SymmetricKeyManagement.cs b/src/key-managment/SymmetricKeyManagement.cs similarity index 98% rename from src/KeyManagement/SymmetricKeyManagement.cs rename to src/key-managment/SymmetricKeyManagement.cs index ae0a313da..93dd38c22 100644 --- a/src/KeyManagement/SymmetricKeyManagement.cs +++ b/src/key-managment/SymmetricKeyManagement.cs @@ -29,7 +29,7 @@ public SymmetricKeyManagement(string key = null) public Task Decrypt(string encryptedData, string serviceAccountId) { - var splitted = encryptedData.Split(":"); + var splitted = encryptedData.Split(':'); if (splitted.Length != 2) { throw new InvalidOperationException("Encrypted data format is invalid"); } diff --git a/src/key-managment/key-managment.csproj b/src/key-managment/key-managment.csproj new file mode 100644 index 000000000..fa96103af --- /dev/null +++ b/src/key-managment/key-managment.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + key_managment + + + + + + + + + + diff --git a/tests/blackbox/EncryptControllerTests.cs b/tests/blackbox/EncryptControllerTests.cs index 4813e3a3f..0df080037 100644 --- a/tests/blackbox/EncryptControllerTests.cs +++ b/tests/blackbox/EncryptControllerTests.cs @@ -8,6 +8,7 @@ using System.Net; using Microsoft.Azure.KeyVault; using Microsoft.IdentityModel.Clients.ActiveDirectory; +using System; namespace blackbox { @@ -67,6 +68,8 @@ public EncryptControllerTests () { } + /* + [Fact] public async Task Encrypt_KeyDoesNotExist_CreateIt() { @@ -93,52 +96,15 @@ public async Task Encrypt_KeyDoesNotExist_CreateIt() var request = new EncryptRequest { - SerivceAccountName = "default", + SerivceAccountName = "dummy", NamesapceName = "default", Data = data }; - var result = await httpClient.PostAsync(ConfigurationProvider.Configuration["API_URL"] + "api/v1/encrypt", new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json")); + var result = await httpClient.PostAsync(ConfigurationProvider.Configuration["ENCRYPTOR"] + "api/v1/encrypt", new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json")); result.EnsureSuccessStatusCode(); - } - - - [Fact] - public async Task Encrypt_SANotExist_ReturnBadRequest() - { - var httpClient = mHttpClientProvider.Provide(); - var data = "test"; - - var request = new EncryptRequest - { - SerivceAccountName = "not-exist", - NamesapceName = "namespace", - Data = data - }; - - var result = await httpClient.PostAsync(ConfigurationProvider.Configuration["API_URL"] + "api/v1/encrypt", new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json")); - - Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); - } - - [Fact] - public async Task Encrypt_NamespaceNotExist_ReturnBadRequest() - { - var httpClient = mHttpClientProvider.Provide(); - var data = "test"; - - var request = new EncryptRequest - { - SerivceAccountName = "default", - NamesapceName = "not-exist", - Data = data - }; - - var result = await httpClient.PostAsync(ConfigurationProvider.Configuration["API_URL"] + "api/v1/encrypt", new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json")); - - Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); - } + }*/ [Fact] public async Task TestFullFlow() @@ -148,12 +114,12 @@ public async Task TestFullFlow() var request = new EncryptRequest { - SerivceAccountName = "default", + SerivceAccountName = "dummy", NamesapceName = "default", Data = data - }; - - var result = await httpClient.PostAsync (ConfigurationProvider.Configuration["API_URL"] + "api/v1/encrypt", new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json")); + }; + + var result = await httpClient.PostAsync (ConfigurationProvider.Configuration["ENCRYPTOR"] + "api/v1/encrypt", new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json")); result.EnsureSuccessStatusCode(); @@ -166,12 +132,12 @@ public async Task TestFullFlow() var decryptRequest = new DecryptRequest { - SerivceAccountName = "default", + SerivceAccountName = "dummy", NamesapceName = "default", EncryptedData = encryptedData }; - result = await httpClient.PostAsync(ConfigurationProvider.Configuration["API_URL"] + "api/v1/decrypt", new StringContent(JsonConvert.SerializeObject(decryptRequest), Encoding.UTF8, "application/json")); + result = await httpClient.PostAsync(ConfigurationProvider.Configuration["DECRYPTOR"] + "api/v1/decrypt", new StringContent(JsonConvert.SerializeObject(decryptRequest), Encoding.UTF8, "application/json")); result.EnsureSuccessStatusCode(); @@ -188,32 +154,14 @@ public async Task AnonymousRequestToDecryptEndpointShouldFail() var decryptRequest = new DecryptRequest { - SerivceAccountName = "default", + SerivceAccountName = "dummy", NamesapceName = "default", EncryptedData = data }; - var result = await httpClient.PostAsync(ConfigurationProvider.Configuration["API_URL"] + "api/v1/decrypt", new StringContent(JsonConvert.SerializeObject(decryptRequest), Encoding.UTF8, "application/json")); + var result = await httpClient.PostAsync(ConfigurationProvider.Configuration["DECRYPTOR"] + "api/v1/decrypt", new StringContent(JsonConvert.SerializeObject(decryptRequest), Encoding.UTF8, "application/json")); Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); } - - [Fact] - public async Task Encrypt_SANotExist_Return400() - { - var httpClient = mHttpClientProvider.Provide(); - var data = "test"; - - var decryptRequest = new DecryptRequest - { - SerivceAccountName = "123456", - NamesapceName = "default", - EncryptedData = data - }; - - var result = await httpClient.PostAsync(ConfigurationProvider.Configuration["API_URL"] + "api/v1/encrypt", new StringContent(JsonConvert.SerializeObject(decryptRequest), Encoding.UTF8, "application/json")); - - Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); - } } } \ No newline at end of file diff --git a/tests/blackbox/Wiremock/mappings/apis_authenticationk8sio_v1_tokenreviews-ff46d3b4-0aad-422d-8215-e99cff8f3188.json b/tests/blackbox/Wiremock/mappings/apis_authenticationk8sio_v1_tokenreviews-ff46d3b4-0aad-422d-8215-e99cff8f3188.json index 7431e130a..a46a41ae5 100644 --- a/tests/blackbox/Wiremock/mappings/apis_authenticationk8sio_v1_tokenreviews-ff46d3b4-0aad-422d-8215-e99cff8f3188.json +++ b/tests/blackbox/Wiremock/mappings/apis_authenticationk8sio_v1_tokenreviews-ff46d3b4-0aad-422d-8215-e99cff8f3188.json @@ -12,7 +12,7 @@ }, "response" : { "status" : 201, - "body" : "{\"kind\":\"TokenReview\",\"apiVersion\":\"authentication.k8s.io/v1\",\"metadata\":{\"creationTimestamp\":null},\"spec\":{\"token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZXYtb3BzIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tbjRtbGgiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6Ijg1OWQ5ZWQ3LTZmMGItMTFlOC1hMzBkLTA4MDAyN2MzNWVjYSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZXYtb3BzOmRlZmF1bHQifQ.F8qlI5u_NbVpUXwGUAc8ko8Xi3h38UHoKU8TPvrzf1WaJJGoyK7qIx8XnR90EjZIPAB_CKVQGYiCT3BR8TNWGvndo_XD6ktMeqjAsqagesTj2zRuP3x53psVeYnukwtxLdw0QwnJpqmx5VdljgzWSBylXzIq9pZlejziQBvFLQ2Ruzph9jWnmydPG7IO9A5XGpJev7wDqHe1KOA7zAVEVDsKlKw5P2pisRFELbmISenHCDwhrwdoixVuvEfznZcPHnHHtcZW1rtsgW-Hyq_a5O_grAgyhCXCVVMIOd7hTZ62C8w7YJQN07AFSsI_96lwZLKoHEK4EBpym4FaiKGkcA\"},\"status\":{\"authenticated\":true,\"user\":{\"username\":\"system:serviceaccount:default:default\",\"uid\":\"859d9ed7-6f0b-11e8-a30d-080027c35eca\",\"groups\":[\"system:serviceaccounts\",\"system:serviceaccounts:default\",\"system:authenticated\"]}}}\n", + "body" : "{\"kind\":\"TokenReview\",\"apiVersion\":\"authentication.k8s.io/v1\",\"metadata\":{\"creationTimestamp\":null},\"spec\":{\"token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZXYtb3BzIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tbjRtbGgiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6Ijg1OWQ5ZWQ3LTZmMGItMTFlOC1hMzBkLTA4MDAyN2MzNWVjYSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZXYtb3BzOmRlZmF1bHQifQ.F8qlI5u_NbVpUXwGUAc8ko8Xi3h38UHoKU8TPvrzf1WaJJGoyK7qIx8XnR90EjZIPAB_CKVQGYiCT3BR8TNWGvndo_XD6ktMeqjAsqagesTj2zRuP3x53psVeYnukwtxLdw0QwnJpqmx5VdljgzWSBylXzIq9pZlejziQBvFLQ2Ruzph9jWnmydPG7IO9A5XGpJev7wDqHe1KOA7zAVEVDsKlKw5P2pisRFELbmISenHCDwhrwdoixVuvEfznZcPHnHHtcZW1rtsgW-Hyq_a5O_grAgyhCXCVVMIOd7hTZ62C8w7YJQN07AFSsI_96lwZLKoHEK4EBpym4FaiKGkcA\"},\"status\":{\"authenticated\":true,\"user\":{\"username\":\"system:serviceaccount:default:default\",\"uid\":\"859d9ed7-6f0b-11e8-a30d-080027c35eca\",\"groups\":[\"system:serviceaccounts\",\"system:serviceaccounts:dummy\",\"system:authenticated\"]}}}\n", "headers" : { "Content-Type" : "application/json", "Date" : "Thu, 12 Jul 2018 05:21:59 GMT" diff --git a/tests/blackbox/compose/docker-compose.local.yaml b/tests/blackbox/compose/docker-compose.local.yaml index 4e2b1924d..bf15c843e 100644 --- a/tests/blackbox/compose/docker-compose.local.yaml +++ b/tests/blackbox/compose/docker-compose.local.yaml @@ -1,5 +1,12 @@ version: '3' services: - api: + encryptor: build: context: ../../../. + args: + PROJECT_NAME: encrypt-api + decryptor: + build: + context: ../../../. + args: + PROJECT_NAME: decrypt-api diff --git a/tests/blackbox/compose/docker-compose.yaml b/tests/blackbox/compose/docker-compose.yaml index 98b6aac23..c2a741ad2 100644 --- a/tests/blackbox/compose/docker-compose.yaml +++ b/tests/blackbox/compose/docker-compose.yaml @@ -1,14 +1,20 @@ version: '3' services: - api: + encryptor: environment: - ASPNETCORE_ENVIRONMENT=Development - - Kubernetes:ProxyUrl=http://wiremock:8080 + + decryptor: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - Kubernetes__ProxyUrl=http://wiremock:8080 + black-box: build: context: ../ environment: - - API_URL=http://api:9999/ + - ENCRYPTOR=http://encryptor:9999/ + - DECRYPTOR=http://decryptor:9999/ - TEAMCITY_PROJECT_NAME=api - PROXY_URL=http://zap:8090 - KUBERNETES_URL=http://wiremock:8080 diff --git a/tests/integration/integration.csproj b/tests/integration/integration.csproj index 423a2f92d..c3d5b126d 100644 --- a/tests/integration/integration.csproj +++ b/tests/integration/integration.csproj @@ -3,6 +3,9 @@ netcoreapp2.1 false + + + @@ -10,9 +13,9 @@ - - - + + + diff --git a/tests/unit/unit.csproj b/tests/unit/unit.csproj index c663d9688..c764ecbe3 100644 --- a/tests/unit/unit.csproj +++ b/tests/unit/unit.csproj @@ -3,15 +3,12 @@ netcoreapp2.1 false + + + - - - - - - \ No newline at end of file