├── website ├── static │ ├── .nojekyll │ └── img │ │ ├── favicon.ico │ │ ├── docusaurus.png │ │ ├── slv-social-card.jpg │ │ ├── docusaurus-social-card.jpg │ │ ├── flexible-deployment.svg │ │ ├── decentralized-secrets.svg │ │ ├── secrets-alongside-code.svg │ │ ├── git-branch.svg │ │ └── code.svg ├── src │ ├── pages │ │ ├── markdown-page.md │ │ └── charts.tsx │ └── components │ │ └── OpenSourceSection │ │ └── githubStats.ts ├── docs │ ├── components │ │ ├── _category_.json │ │ └── vault.md │ ├── extensions │ │ ├── slv-in-kubernetes │ │ │ └── _category_.json │ │ ├── _category_.json │ │ └── slv-in-github-actions.md │ ├── command-reference │ │ ├── system │ │ │ ├── _category_.json │ │ │ └── system.md │ │ ├── _category_.json │ │ ├── profile │ │ │ ├── _category_.json │ │ │ ├── list.md │ │ │ ├── set-active.md │ │ │ ├── sync.md │ │ │ └── del.md │ │ ├── environment │ │ │ ├── _category_.json │ │ │ ├── del.md │ │ │ └── add.md │ │ └── vault │ │ │ ├── _category_.json │ │ │ ├── rm.md │ │ │ ├── update.md │ │ │ ├── run.md │ │ │ ├── ref.md │ │ │ └── access.md │ ├── integration-guide │ │ └── _category_.json │ ├── installation.md │ └── overview.md ├── tsconfig.json ├── .gitignore ├── sidebars.ts └── package.json ├── action ├── .eslintignore ├── .eslintrc.json ├── package.json └── .gitignore ├── internal ├── k8s │ ├── config │ │ ├── prometheus │ │ │ ├── kustomization.yaml │ │ │ └── monitor.yaml │ │ ├── certmanager │ │ │ ├── kustomization.yaml │ │ │ ├── kustomizeconfig.yaml │ │ │ └── certificate.yaml │ │ ├── webhook │ │ │ ├── kustomization.yaml │ │ │ ├── service.yaml │ │ │ ├── manifests.yaml │ │ │ └── kustomizeconfig.yaml │ │ ├── samples │ │ │ └── kustomization.yaml │ │ ├── manager │ │ │ └── kustomization.yaml │ │ ├── default │ │ │ ├── manager_config_patch.yaml │ │ │ ├── manager_webhook_patch.yaml │ │ │ ├── webhookcainjection_patch.yaml │ │ │ └── manager_auth_proxy_patch.yaml │ │ ├── crd │ │ │ ├── patches │ │ │ │ ├── cainjection_in_slvs.yaml │ │ │ │ └── webhook_in_slvs.yaml │ │ │ ├── kustomizeconfig.yaml │ │ │ └── kustomization.yaml │ │ └── rbac │ │ │ ├── service_account.yaml │ │ │ ├── auth_proxy_client_clusterrole.yaml │ │ │ ├── role.yaml │ │ │ ├── role_binding.yaml │ │ │ ├── auth_proxy_role_binding.yaml │ │ │ ├── leader_election_role_binding.yaml │ │ │ ├── auth_proxy_role.yaml │ │ │ ├── auth_proxy_service.yaml │ │ │ ├── slv_viewer_role.yaml │ │ │ ├── slv_editor_role.yaml │ │ │ ├── kustomization.yaml │ │ │ └── leader_election_role.yaml │ ├── charts │ │ ├── slv-job │ │ │ ├── templates │ │ │ │ ├── crds │ │ │ │ │ └── crd.yaml │ │ │ │ ├── NOTES.txt │ │ │ │ ├── clusterrole.yaml │ │ │ │ ├── serviceaccount.yaml │ │ │ │ ├── clusterrolebinding.yaml │ │ │ │ └── job.yaml │ │ │ └── Chart.yaml │ │ ├── slv-operator │ │ │ ├── templates │ │ │ │ ├── crds │ │ │ │ │ └── crd.yaml │ │ │ │ ├── NOTES.txt │ │ │ │ ├── serviceaccount.yaml │ │ │ │ ├── secret.yaml │ │ │ │ ├── service.yaml │ │ │ │ ├── clusterrolebinding.yaml │ │ │ │ ├── validatingwebhookconfig.yaml │ │ │ │ └── clusterrole.yaml │ │ │ └── Chart.yaml │ │ └── slv-lib │ │ │ ├── templates │ │ │ ├── _clusterrolebinding.tpl │ │ │ ├── _serviceaccount.tpl │ │ │ ├── _clusterrole.tpl │ │ │ └── _notes.tpl │ │ │ ├── .helmignore │ │ │ └── Chart.yaml │ ├── cmd │ │ └── main.go │ ├── hack │ │ └── boilerplate.go.txt │ ├── PROJECT │ ├── .golangci.yml │ ├── test │ │ └── e2e │ │ │ └── e2e_suite_test.go │ ├── job │ │ └── job.go │ └── api │ │ └── v1 │ │ ├── slv_webhook_test.go │ │ ├── groupversion_info.go │ │ └── slv_types.go ├── core │ ├── settings │ │ ├── const.go │ │ └── settings.go │ ├── input │ │ ├── const.go │ │ ├── inputs.go │ │ └── password.go │ ├── commons │ │ ├── commons.go │ │ ├── files.go │ │ ├── http.go │ │ ├── compression.go │ │ └── encoding.go │ ├── vaults │ │ ├── cache.go │ │ ├── item.go │ │ └── deref.go │ ├── config │ │ ├── appdata.go │ │ ├── version.go │ │ └── const.go │ ├── crypto │ │ └── const.go │ ├── environments │ │ ├── const.go │ │ └── envproviders │ │ │ └── kms.go │ └── profiles │ │ ├── remote.go │ │ └── const.go ├── cli │ ├── commands │ │ ├── utils │ │ │ ├── flag.go │ │ │ └── commons.go │ │ ├── cmdsystem │ │ │ ├── commons.go │ │ │ ├── system.go │ │ │ └── reset.go │ │ ├── cmdenv │ │ │ ├── env.go │ │ │ ├── set.go │ │ │ ├── add.go │ │ │ ├── delete.go │ │ │ ├── const.go │ │ │ └── list.go │ │ ├── cmdprofile │ │ │ ├── commons.go │ │ │ ├── sync.go │ │ │ ├── list.go │ │ │ ├── profile.go │ │ │ ├── delete.go │ │ │ └── active.go │ │ └── cmdvault │ │ │ ├── delete.go │ │ │ ├── deref.go │ │ │ ├── update.go │ │ │ └── new.go │ ├── version.go │ ├── tui.go │ ├── web.go │ └── slv.go ├── app │ └── main.go ├── tui │ ├── main.go │ ├── pages │ │ ├── vault_view │ │ │ ├── sections.go │ │ │ └── forms.go │ │ ├── help │ │ │ ├── state.go │ │ │ └── help.go │ │ ├── profiles │ │ │ ├── state.go │ │ │ └── profiles.go │ │ ├── environments │ │ │ └── state.go │ │ ├── vault_new │ │ │ ├── state.go │ │ │ └── lists.go │ │ ├── vault_edit │ │ │ ├── state.go │ │ │ ├── lists.go │ │ │ └── styles.go │ │ ├── mainpage │ │ │ └── state.go │ │ ├── environments_new │ │ │ ├── state.go │ │ │ ├── core.go │ │ │ └── navigation.go │ │ └── vault_browse │ │ │ └── sections.go │ ├── core │ │ ├── theme.go │ │ ├── application.go │ │ └── layout.go │ ├── navigation │ │ ├── status.go │ │ ├── input.go │ │ ├── state.go │ │ └── navigation.go │ ├── interfaces │ │ ├── tui.go │ │ ├── router.go │ │ ├── page.go │ │ ├── components.go │ │ └── navigation.go │ ├── components │ │ └── maincontent.go │ └── utils │ │ └── input.go ├── api │ ├── session.go │ └── auth.go ├── helpers │ ├── vault_new.go │ └── vault_list.go └── sharedlib │ ├── main.go │ └── lib.go ├── .gitignore ├── Dockerfile ├── action.yaml ├── LICENSE ├── pages ├── scripts │ ├── install.ps1 │ └── install.sh └── slv │ └── index.html └── slv.go /website/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /action/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /internal/k8s/config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-job/templates/crds/crd.yaml: -------------------------------------------------------------------------------- 1 | {{- include "slvlib.crd" . | nindent 0 }} -------------------------------------------------------------------------------- /internal/k8s/charts/slv-job/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | {{- include "slvlib.notes" . | nindent 0 }} 2 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-operator/templates/crds/crd.yaml: -------------------------------------------------------------------------------- 1 | {{- include "slvlib.crd" . | nindent 0 }} -------------------------------------------------------------------------------- /internal/k8s/charts/slv-operator/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | {{- include "slvlib.notes" . | nindent 0 }} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | local_test 2 | dist/* 3 | .DS_Store 4 | slv-dev 5 | .vscode 6 | internal/k8s/bin/* 7 | .ignore/* -------------------------------------------------------------------------------- /internal/k8s/charts/slv-job/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- include "slvlib.clusterrole" . | nindent 0 }} 2 | -------------------------------------------------------------------------------- /website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amagioss/slv/HEAD/website/static/img/favicon.ico -------------------------------------------------------------------------------- /internal/k8s/charts/slv-job/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- include "slvlib.serviceaccount" . | nindent 0 }} 2 | -------------------------------------------------------------------------------- /website/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amagioss/slv/HEAD/website/static/img/docusaurus.png -------------------------------------------------------------------------------- /internal/k8s/charts/slv-job/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- include "slvlib.clusterrolebinding" . | nindent 0 }} 2 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-operator/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- include "slvlib.serviceaccount" . | nindent 0 }} 2 | -------------------------------------------------------------------------------- /website/static/img/slv-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amagioss/slv/HEAD/website/static/img/slv-social-card.jpg -------------------------------------------------------------------------------- /website/static/img/docusaurus-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amagioss/slv/HEAD/website/static/img/docusaurus-social-card.jpg -------------------------------------------------------------------------------- /internal/k8s/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "slv.sh/slv/internal/k8s/operator" 4 | 5 | func main() { 6 | operator.Run() 7 | } 8 | -------------------------------------------------------------------------------- /internal/k8s/config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /internal/k8s/config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /internal/k8s/config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples of your project ## 2 | resources: 3 | - slv_v1_slv.yaml 4 | #+kubebuilder:scaffold:manifestskustomizesamples 5 | -------------------------------------------------------------------------------- /website/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cgr.dev/chainguard/static:latest 2 | ARG TARGETARCH 3 | COPY ./dist/slv_linux_${TARGETARCH}*/slv /bin/ 4 | WORKDIR /workspace 5 | USER 65532:65532 6 | ENV GODEBUG=madvdontneed=1 7 | ENTRYPOINT ["slv"] -------------------------------------------------------------------------------- /website/docs/components/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Components", 3 | "position": 4, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Understand the key terms used in SLV." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /website/src/pages/charts.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect } from '@docusaurus/router'; 3 | 4 | export default function Charts(): JSX.Element { 5 | return ; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /website/docs/extensions/slv-in-kubernetes/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Kubernetes", 3 | "position": 6, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Use SLV in Kubernetes" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /internal/k8s/config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: ghcr.io/amagioss/slv 8 | newTag: latest 9 | -------------------------------------------------------------------------------- /website/docs/extensions/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Extensions", 3 | "position": 6, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Support for SLV in DevOps and CI/CD pipelines." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /website/docs/command-reference/system/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "System", 3 | "position": 4, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Learn about the commands available in SLV." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /website/docs/command-reference/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Command Reference", 3 | "position": 5, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Learn about the commands available in SLV." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /website/docs/command-reference/profile/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Profile", 3 | "position": 3, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Learn about the commands available in SLV." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /website/docs/command-reference/environment/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Environment", 3 | "position": 1, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Commands Related to Environments in SLV." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /website/docs/integration-guide/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Guide", 3 | "position": 8, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Understand how to integrate SLV with your existing systems." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /internal/k8s/config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | }, 7 | "exclude": [".docusaurus", "build"] 8 | } 9 | -------------------------------------------------------------------------------- /internal/core/settings/const.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | errManifestPathExistsAlready = errors.New("manifest path exists already") 9 | errManifestNotFound = errors.New("manifest not found") 10 | ) 11 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-operator/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | {{- if not (.Values.webhook.disableAutomaticCertManagement) }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ .Values.webhook.certSecretName | default "slv-webhook-server-cert"}} 6 | namespace: {{ .Release.Namespace }} 7 | {{- end}} 8 | -------------------------------------------------------------------------------- /internal/k8s/config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | -------------------------------------------------------------------------------- /internal/cli/commands/utils/flag.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type FlagDef struct { 4 | Name string 5 | Shorthand string 6 | Usage string 7 | } 8 | 9 | var ( 10 | QuantumSafeFlag = FlagDef{ 11 | Name: "quantum-safe", 12 | Shorthand: "q", 13 | Usage: "Use post-quantum cryptography (Kyber1024)", 14 | } 15 | ) 16 | -------------------------------------------------------------------------------- /internal/k8s/config/crd/patches/cainjection_in_slvs.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: slvs.slv.sh 8 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /website/static/img/flexible-deployment.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 🚀 7 | -------------------------------------------------------------------------------- /website/static/img/decentralized-secrets.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 🔐 7 | -------------------------------------------------------------------------------- /website/static/img/secrets-alongside-code.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 🧠 7 | -------------------------------------------------------------------------------- /website/docs/command-reference/vault/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Vault", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": " The vault command always works within the context of a vault file. Therefore, the `--vault` flag will have to be set for all commands. The vault file must end with either `.slv.yaml` or `slv.yml`." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /action/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "jest": true, 6 | "node": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": "latest" 15 | }, 16 | "rules": { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdsystem/commons.go: -------------------------------------------------------------------------------- 1 | package cmdsystem 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "slv.sh/slv/internal/cli/commands/utils" 6 | ) 7 | 8 | var ( 9 | systemCmd *cobra.Command 10 | systemResetCmd *cobra.Command 11 | ) 12 | 13 | var ( 14 | yesFlag = utils.FlagDef{ 15 | Name: "yes", 16 | Shorthand: "y", 17 | Usage: "Confirm action", 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-operator/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: slv-operator 6 | name: {{ .Values.webhook.serviceName | default "slv-webhook-service" }} 7 | namespace: {{ .Release.Namespace }} 8 | spec: 9 | ports: 10 | - port: 443 11 | protocol: TCP 12 | targetPort: 9443 13 | selector: 14 | app: slv-operator 15 | -------------------------------------------------------------------------------- /website/src/components/OpenSourceSection/githubStats.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * GitHub statistics fetched at build time 3 | * These values are automatically updated during the build process 4 | * Last updated: 2025-12-05 15:14:50 UTC 5 | */ 6 | export const GITHUB_STATS = { 7 | stars: 66, // Fetched from GitHub API at build time 8 | contributors: 2, // Fetched from GitHub API at build time 9 | } as const; 10 | -------------------------------------------------------------------------------- /internal/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "slv.sh/slv/internal/cli" 8 | "slv.sh/slv/internal/k8s/job" 9 | "slv.sh/slv/internal/k8s/operator" 10 | ) 11 | 12 | func main() { 13 | switch strings.ToLower(os.Getenv("SLV_MODE")) { 14 | case "k8s_operator", "k8s-operator": 15 | operator.Run() 16 | case "k8s_job", "k8s-job": 17 | job.Run() 18 | default: 19 | cli.Run() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/core/input/const.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import "sync" 4 | 5 | var ( 6 | defaultPwdPolicyMutex sync.Mutex 7 | defaultPwdPolicy *PasswordPolicy 8 | ) 9 | 10 | const ( 11 | pwdSpecialCharset = `!@#$%^&*_-+=?:;,.|\/[](){}<>` 12 | pwdDefaultMinLength = 10 13 | pwdDefaultMinUppercase = 1 14 | pwdDefaultMinLowercase = 1 15 | pwdDefaultMinDigits = 1 16 | pwdDefaultMinSpecialChars = 1 17 | ) 18 | -------------------------------------------------------------------------------- /internal/k8s/config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kubernetes.io/instance: controller-manager-sa 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: operator 9 | app.kubernetes.io/part-of: operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-lib/templates/_clusterrolebinding.tpl: -------------------------------------------------------------------------------- 1 | {{- define "slvlib.clusterrolebinding" -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: slv-rolebinding 6 | subjects: 7 | - kind: ServiceAccount 8 | name: slv-serviceaccount 9 | namespace: {{ .Release.Namespace }} 10 | roleRef: 11 | kind: ClusterRole 12 | name: slv-clusterrole 13 | apiGroup: rbac.authorization.k8s.io 14 | {{- end -}} 15 | -------------------------------------------------------------------------------- /internal/core/commons/commons.go: -------------------------------------------------------------------------------- 1 | package commons 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func FileExists(path string) bool { 8 | f, err := os.Stat(path) 9 | return err == nil && !f.IsDir() 10 | } 11 | 12 | func DirExists(path string) bool { 13 | f, err := os.Stat(path) 14 | return err == nil && f.IsDir() 15 | } 16 | 17 | func StringPtr(s string) *string { 18 | return &s 19 | } 20 | 21 | func ByteSlicePtr(bytes []byte) *[]byte { 22 | return &bytes 23 | } 24 | -------------------------------------------------------------------------------- /internal/tui/main.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "slv.sh/slv/internal/tui/app" 8 | ) 9 | 10 | // RunTUI starts the TUI application 11 | func RunTUI() error { 12 | tui := app.NewTUI() 13 | return tui.Run() 14 | } 15 | 16 | // RunTUIWithErrorHandling runs TUI with error handling 17 | func RunTUIWithErrorHandling() { 18 | if err := RunTUI(); err != nil { 19 | fmt.Fprintf(os.Stderr, "TUI Error: %v\n", err) 20 | os.Exit(1) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/cli/version.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "slv.sh/slv/internal/core/config" 8 | ) 9 | 10 | func versionCommand() *cobra.Command { 11 | if versionCmd == nil { 12 | versionCmd = &cobra.Command{ 13 | Use: "version", 14 | Short: "Show version information", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | fmt.Println(config.VersionInfo()) 17 | }, 18 | } 19 | } 20 | return versionCmd 21 | } 22 | -------------------------------------------------------------------------------- /internal/tui/pages/vault_view/sections.go: -------------------------------------------------------------------------------- 1 | package vault_view 2 | 3 | import "github.com/rivo/tview" 4 | 5 | func (vvp *VaultViewPage) createMainSection() tview.Primitive { 6 | mainFlex := tview.NewFlex().SetDirection(tview.FlexRow) 7 | mainFlex.AddItem(vvp.createVaultDetailsTable(), 0, 30, true) 8 | mainFlex.AddItem(vvp.createAccessorsTable(), 0, 30, false) 9 | mainFlex.AddItem(vvp.createVaultItemsTable(), 0, 40, false) 10 | vvp.mainFlex = mainFlex 11 | return mainFlex 12 | } 13 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-lib/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /internal/cli/tui.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "slv.sh/slv/internal/tui" 6 | ) 7 | 8 | func tuiCommand() *cobra.Command { 9 | if tuiCmd == nil { 10 | tuiCmd = &cobra.Command{ 11 | Use: "tui", 12 | Aliases: []string{"ui", "interactive", "menu", "dashboard"}, 13 | Short: "Starts the SLV TUI", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | tui.RunTUIWithErrorHandling() 16 | }, 17 | } 18 | } 19 | return tuiCmd 20 | } 21 | -------------------------------------------------------------------------------- /internal/k8s/config/crd/patches/webhook_in_slvs.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: slvs.slv.sh 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /internal/cli/commands/utils/commons.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fatih/color" 8 | ) 9 | 10 | func ExitOnError(err error) { 11 | fmt.Fprintln(os.Stderr, color.RedString(err.Error())) 12 | ErroredExit() 13 | } 14 | 15 | func ExitOnErrorWithMessage(errMessage string) { 16 | fmt.Fprintln(os.Stderr, color.RedString(errMessage)) 17 | ErroredExit() 18 | } 19 | 20 | func ErroredExit() { 21 | os.Exit(1) 22 | } 23 | 24 | func SafeExit() { 25 | os.Exit(0) 26 | } 27 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-lib/templates/_serviceaccount.tpl: -------------------------------------------------------------------------------- 1 | {{- define "slvlib.serviceaccount" -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: slv-serviceaccount 6 | namespace: {{ .Release.Namespace }} 7 | {{- with .Values.serviceAccount.labels }} 8 | labels: 9 | {{- toYaml . | nindent 4 }} 10 | {{- end }} 11 | {{- with .Values.serviceAccount.annotations }} 12 | annotations: 13 | {{- toYaml . | nindent 4 }} 14 | {{- end }} 15 | automountServiceAccountToken: true 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /internal/k8s/config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: metrics-reader 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: operator 9 | app.kubernetes.io/part-of: operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-reader 12 | rules: 13 | - nonResourceURLs: 14 | - "/metrics" 15 | verbs: 16 | - get 17 | -------------------------------------------------------------------------------- /internal/k8s/config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - slv.sh 9 | resources: 10 | - slvs 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - slv.sh 21 | resources: 22 | - slvs/finalizers 23 | verbs: 24 | - update 25 | - apiGroups: 26 | - slv.sh 27 | resources: 28 | - slvs/status 29 | verbs: 30 | - get 31 | - patch 32 | - update 33 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-operator/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- include "slvlib.clusterrolebinding" . | nindent 0 }} 2 | 3 | --- 4 | 5 | {{- if not (.Values.webhook.disableAutomaticCertManagement) }} 6 | apiVersion: rbac.authorization.k8s.io/v1 7 | kind: ClusterRoleBinding 8 | metadata: 9 | name: slv-webhook-rolebinding 10 | subjects: 11 | - kind: ServiceAccount 12 | name: slv-serviceaccount 13 | namespace: {{ .Release.Namespace }} 14 | roleRef: 15 | kind: ClusterRole 16 | name: slv-webhook-clusterrole 17 | apiGroup: rbac.authorization.k8s.io 18 | {{- end }} 19 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdsystem/system.go: -------------------------------------------------------------------------------- 1 | package cmdsystem 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func SystemCommand() *cobra.Command { 8 | if systemCmd == nil { 9 | systemCmd = &cobra.Command{ 10 | Use: "system", 11 | Aliases: []string{"systems"}, 12 | Short: "System level commands", 13 | Long: `System level operations can be carried out using this command`, 14 | Run: func(cmd *cobra.Command, args []string) { 15 | cmd.Help() 16 | }, 17 | } 18 | systemCmd.AddCommand(systemResetCommand()) 19 | } 20 | return systemCmd 21 | } 22 | -------------------------------------------------------------------------------- /internal/k8s/config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: service 6 | app.kubernetes.io/instance: webhook-service 7 | app.kubernetes.io/component: webhook 8 | app.kubernetes.io/created-by: operator 9 | app.kubernetes.io/part-of: operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: webhook-service 12 | namespace: system 13 | spec: 14 | ports: 15 | - port: 443 16 | protocol: TCP 17 | targetPort: 9443 18 | selector: 19 | control-plane: controller-manager 20 | -------------------------------------------------------------------------------- /internal/tui/pages/help/state.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | // SaveNavigationState implements the Page interface (empty for help page) 4 | func (hp *HelpPage) SaveNavigationState() { 5 | // Help page doesn't need state management 6 | } 7 | 8 | // RestoreNavigationState implements the Page interface (empty for help page) 9 | func (hp *HelpPage) RestoreNavigationState() { 10 | // Help page doesn't need state management 11 | } 12 | 13 | // ClearNavigationState implements the Page interface (empty for help page) 14 | func (hp *HelpPage) ClearNavigationState() { 15 | // Help page doesn't need state management 16 | } 17 | -------------------------------------------------------------------------------- /internal/k8s/config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /internal/core/vaults/cache.go: -------------------------------------------------------------------------------- 1 | package vaults 2 | 3 | func (vlt *Vault) getFromCache(name string) (item *VaultItem) { 4 | if vlt.Spec.cache != nil { 5 | item = vlt.Spec.cache[name] 6 | } 7 | return 8 | } 9 | 10 | func (vlt *Vault) putToCache(name string, item *VaultItem) { 11 | if vlt.Spec.cache == nil { 12 | vlt.Spec.cache = make(map[string]*VaultItem) 13 | } 14 | vlt.Spec.cache[name] = item 15 | } 16 | 17 | func (vlt *Vault) deleteFromCache(name string) { 18 | if vlt.Spec.cache != nil { 19 | delete(vlt.Spec.cache, name) 20 | } 21 | } 22 | 23 | func (vlt *Vault) clearCache() { 24 | vlt.Spec.cache = nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/k8s/config/webhook/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: ValidatingWebhookConfiguration 4 | metadata: 5 | name: validating-webhook-configuration 6 | webhooks: 7 | - admissionReviewVersions: 8 | - v1 9 | clientConfig: 10 | service: 11 | name: webhook-service 12 | namespace: system 13 | path: /validate 14 | failurePolicy: Fail 15 | name: validate-slv 16 | rules: 17 | - apiGroups: 18 | - slv.sh 19 | apiVersions: 20 | - v1 21 | operations: 22 | - CREATE 23 | - UPDATE 24 | resources: 25 | - slvs 26 | sideEffects: None 27 | -------------------------------------------------------------------------------- /internal/k8s/hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /internal/tui/pages/profiles/state.go: -------------------------------------------------------------------------------- 1 | package profiles 2 | 3 | // SaveNavigationState implements the Page interface (empty for profiles page) 4 | func (pp *ProfilesPage) SaveNavigationState() { 5 | // Profiles page doesn't need state management 6 | } 7 | 8 | // RestoreNavigationState implements the Page interface (empty for profiles page) 9 | func (pp *ProfilesPage) RestoreNavigationState() { 10 | // Profiles page doesn't need state management 11 | } 12 | 13 | // ClearNavigationState implements the Page interface (empty for profiles page) 14 | func (pp *ProfilesPage) ClearNavigationState() { 15 | // Profiles page doesn't need state management 16 | } 17 | -------------------------------------------------------------------------------- /internal/k8s/PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: slv.sh 6 | layout: 7 | - go.kubebuilder.io/v4 8 | projectName: operator 9 | repo: slv.sh/slv 10 | resources: 11 | - api: 12 | crdVersion: v1 13 | namespaced: true 14 | controller: true 15 | domain: slv.sh 16 | group: slv 17 | kind: SLV 18 | path: slv.sh/slv/internal/k8s/api/v1 19 | version: v1 20 | webhooks: 21 | validation: true 22 | webhookVersion: v1 23 | version: "3" 24 | -------------------------------------------------------------------------------- /internal/k8s/config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/operator-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /internal/tui/pages/environments/state.go: -------------------------------------------------------------------------------- 1 | package environments 2 | 3 | // SaveNavigationState saves the current navigation state 4 | func (ep *EnvironmentsPage) SaveNavigationState() { 5 | // Environments page doesn't need to save state yet 6 | } 7 | 8 | // RestoreNavigationState restores the saved navigation state 9 | func (ep *EnvironmentsPage) RestoreNavigationState() { 10 | // Environments page doesn't need to restore state yet 11 | ep.navigation.updateHelpText() 12 | } 13 | 14 | // ClearNavigationState clears the navigation state 15 | func (ep *EnvironmentsPage) ClearNavigationState() { 16 | // Environments page doesn't need to clear state yet 17 | } 18 | -------------------------------------------------------------------------------- /internal/k8s/config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: operator 9 | app.kubernetes.io/part-of: operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /internal/tui/pages/vault_new/state.go: -------------------------------------------------------------------------------- 1 | package vault_new 2 | 3 | // SaveNavigationState implements the Page interface (empty for vault new page) 4 | func (vnp *VaultNewPage) SaveNavigationState() { 5 | // Vault new page doesn't need state management 6 | } 7 | 8 | // RestoreNavigationState implements the Page interface (empty for vault new page) 9 | func (vnp *VaultNewPage) RestoreNavigationState() { 10 | // Vault new page doesn't need state management 11 | } 12 | 13 | // ClearNavigationState implements the Page interface (empty for vault new page) 14 | func (vnp *VaultNewPage) ClearNavigationState() { 15 | // Vault new page doesn't need state management 16 | } 17 | -------------------------------------------------------------------------------- /internal/core/commons/files.go: -------------------------------------------------------------------------------- 1 | package commons 2 | 3 | import ( 4 | "os" 5 | 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | const slvYamlNotice = "# This file is managed by SLV. Please avoid editing this file manually.\n" 10 | 11 | func WriteToYAML(filePath string, data any) error { 12 | bytes, err := yaml.Marshal(data) 13 | if err == nil { 14 | bytes = append([]byte(slvYamlNotice), bytes...) 15 | err = os.WriteFile(filePath, bytes, 0644) 16 | } 17 | return err 18 | } 19 | 20 | func ReadFromYAML(filePath string, out any) error { 21 | bytes, err := os.ReadFile(filePath) 22 | if err == nil { 23 | err = yaml.Unmarshal(bytes, out) 24 | } 25 | return err 26 | } 27 | -------------------------------------------------------------------------------- /internal/tui/pages/vault_edit/state.go: -------------------------------------------------------------------------------- 1 | package vault_edit 2 | 3 | // SaveNavigationState implements the Page interface (empty for vault edit page) 4 | func (vep *VaultEditPage) SaveNavigationState() { 5 | // Vault edit page doesn't need state management 6 | } 7 | 8 | // RestoreNavigationState implements the Page interface (empty for vault edit page) 9 | func (vep *VaultEditPage) RestoreNavigationState() { 10 | // Vault edit page doesn't need state management 11 | } 12 | 13 | // ClearNavigationState implements the Page interface (empty for vault edit page) 14 | func (vep *VaultEditPage) ClearNavigationState() { 15 | // Vault edit page doesn't need state management 16 | } 17 | -------------------------------------------------------------------------------- /internal/k8s/config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: proxy-rolebinding 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: operator 9 | app.kubernetes.io/part-of: operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: proxy-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /internal/k8s/config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: operator 9 | app.kubernetes.io/part-of: operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /website/static/img/git-branch.svg: -------------------------------------------------------------------------------- 1 | 2 | ionicons-v5-d -------------------------------------------------------------------------------- /internal/k8s/config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: proxy-role 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: operator 9 | app.kubernetes.io/part-of: operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-role 12 | rules: 13 | - apiGroups: 14 | - authentication.k8s.io 15 | resources: 16 | - tokenreviews 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - authorization.k8s.io 21 | resources: 22 | - subjectaccessreviews 23 | verbs: 24 | - create 25 | -------------------------------------------------------------------------------- /internal/tui/pages/mainpage/state.go: -------------------------------------------------------------------------------- 1 | package mainpage 2 | 3 | // SaveNavigationState implements the Page interface (empty for main page) 4 | func (mp *MainPage) SaveNavigationState() { 5 | // Main page doesn't need state management 6 | } 7 | 8 | // RestoreNavigationState implements the Page interface (empty for main page) 9 | func (mp *MainPage) RestoreNavigationState() { 10 | // Update help text when navigating back 11 | if mp.navigation != nil { 12 | mp.navigation.updateHelpText() 13 | } 14 | } 15 | 16 | // ClearNavigationState implements the Page interface (empty for main page) 17 | func (mp *MainPage) ClearNavigationState() { 18 | // Main page doesn't need state management 19 | } 20 | -------------------------------------------------------------------------------- /internal/k8s/config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: service 7 | app.kubernetes.io/instance: controller-manager-metrics-service 8 | app.kubernetes.io/component: kube-rbac-proxy 9 | app.kubernetes.io/created-by: operator 10 | app.kubernetes.io/part-of: operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: controller-manager-metrics-service 13 | namespace: system 14 | spec: 15 | ports: 16 | - name: https 17 | port: 8443 18 | protocol: TCP 19 | targetPort: https 20 | selector: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /internal/tui/core/theme.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "github.com/rivo/tview" 6 | ) 7 | 8 | // Theme defines the interface for theme functionality 9 | type Theme interface { 10 | ApplyTheme(app *tview.Application) 11 | GetBackground() tcell.Color 12 | GetAccent() tcell.Color 13 | GetPrimary() tcell.Color 14 | GetSecondary() tcell.Color 15 | GetTextPrimary() tcell.Color 16 | GetTextSecondary() tcell.Color 17 | GetTextMuted() tcell.Color 18 | GetSuccess() tcell.Color 19 | GetWarning() tcell.Color 20 | GetError() tcell.Color 21 | GetInfo() tcell.Color 22 | GetBorder() tcell.Color 23 | GetFocus() tcell.Color 24 | GetSelection() tcell.Color 25 | } 26 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-lib/templates/_clusterrole.tpl: -------------------------------------------------------------------------------- 1 | {{- define "slvlib.clusterrole" -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: slv-clusterrole 6 | rules: 7 | - apiGroups: ["slv.sh"] 8 | resources: ["slvs"] 9 | verbs: 10 | - "get" 11 | - "list" 12 | - "watch" 13 | - "update" 14 | - apiGroups: [""] 15 | resources: ["secrets"] 16 | verbs: 17 | - "create" 18 | - "get" 19 | - "list" 20 | - "update" 21 | - "delete" 22 | - "watch" 23 | - apiGroups: [""] 24 | resources: ["configmaps"] 25 | verbs: 26 | - "get" 27 | - "create" 28 | - "update" 29 | {{- end -}} 30 | -------------------------------------------------------------------------------- /internal/k8s/config/rbac/slv_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view slvs. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: slv-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: operator 10 | app.kubernetes.io/part-of: operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: slv-viewer-role 13 | rules: 14 | - apiGroups: 15 | - slv.sh 16 | resources: 17 | - slvs 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - slv.sh 24 | resources: 25 | - slvs/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdenv/env.go: -------------------------------------------------------------------------------- 1 | package cmdenv 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func EnvCommand() *cobra.Command { 8 | if envCmd == nil { 9 | envCmd = &cobra.Command{ 10 | Use: "env", 11 | Aliases: []string{"envs", "environment", "environments"}, 12 | Short: "Manage SLV environments", 13 | Run: func(cmd *cobra.Command, args []string) { 14 | cmd.Help() 15 | }, 16 | } 17 | envCmd.AddCommand(envNewCommand()) 18 | envCmd.AddCommand(envListCommand()) 19 | envCmd.AddCommand(envDeleteCommand()) 20 | envCmd.AddCommand(envSetSelfCommand()) 21 | envCmd.AddCommand(envShowCommand()) 22 | envCmd.AddCommand(envAddCommand()) 23 | } 24 | return envCmd 25 | } 26 | -------------------------------------------------------------------------------- /internal/k8s/config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # CERTIFICATE_NAMESPACE and CERTIFICATE_NAME will be substituted by kustomize 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: ValidatingWebhookConfiguration 5 | metadata: 6 | labels: 7 | app.kubernetes.io/name: validatingwebhookconfiguration 8 | app.kubernetes.io/instance: validating-webhook-configuration 9 | app.kubernetes.io/component: webhook 10 | app.kubernetes.io/created-by: operator 11 | app.kubernetes.io/part-of: operator 12 | app.kubernetes.io/managed-by: kustomize 13 | name: validating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 16 | -------------------------------------------------------------------------------- /internal/k8s/config/rbac/slv_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit slvs. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: slv-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: operator 10 | app.kubernetes.io/part-of: operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: slv-editor-role 13 | rules: 14 | - apiGroups: 15 | - slv.sh 16 | resources: 17 | - slvs 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - slv.sh 28 | resources: 29 | - slvs/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /internal/cli/web.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "slv.sh/slv/internal/api" 6 | "slv.sh/slv/internal/cli/commands/utils" 7 | ) 8 | 9 | var ( 10 | portFlag = utils.FlagDef{ 11 | Name: "port", 12 | Shorthand: "p", 13 | Usage: "Port to serve the SLV Web Interface", 14 | } 15 | ) 16 | 17 | func webCommand() *cobra.Command { 18 | if webCmd == nil { 19 | webCmd = &cobra.Command{ 20 | Use: "web", 21 | Short: "Starts the SLV Web Interface", 22 | Hidden: true, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | port, _ := cmd.Flags().GetUint16(portFlag.Name) 25 | api.Run(port) 26 | }, 27 | } 28 | webCmd.Flags().Uint16P(portFlag.Name, portFlag.Shorthand, 0, portFlag.Usage) 29 | } 30 | return webCmd 31 | } 32 | -------------------------------------------------------------------------------- /internal/k8s/config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdprofile/commons.go: -------------------------------------------------------------------------------- 1 | package cmdprofile 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "slv.sh/slv/internal/cli/commands/utils" 6 | ) 7 | 8 | var ( 9 | profileCmd *cobra.Command 10 | profileNewCmd *cobra.Command 11 | profileListCmd *cobra.Command 12 | profileSetActiveCmd *cobra.Command 13 | profileDelCmd *cobra.Command 14 | profileSyncCmd *cobra.Command 15 | ) 16 | 17 | var ( 18 | profileNameFlag = utils.FlagDef{ 19 | Name: "name", 20 | Shorthand: "n", 21 | Usage: "Profile name", 22 | } 23 | 24 | profileReadOnlyFlag = utils.FlagDef{ 25 | Name: "read-only", 26 | Usage: "Set profile as read-only", 27 | } 28 | 29 | profileSyncInterval = utils.FlagDef{ 30 | Name: "sync-interval", 31 | Usage: "Profile sync interval", 32 | } 33 | ) 34 | -------------------------------------------------------------------------------- /internal/tui/navigation/status.go: -------------------------------------------------------------------------------- 1 | package navigation 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | ) 6 | 7 | // UpdateStatus updates the status bar 8 | func (n *Navigation) UpdateStatus() { 9 | n.app.GetComponents().UpdateStatus(n.app.GetRouter().GetCurrentPage()) 10 | } 11 | 12 | // GetStatusBar returns the status bar primitive 13 | func (n *Navigation) GetStatusBar() tview.Primitive { 14 | return n.app.GetComponents().GetStatusBar() 15 | } 16 | 17 | // SetCustomHelp sets the custom help text for the current page 18 | func (n *Navigation) SetCustomHelp(helpText string) { 19 | n.customHelp = helpText 20 | n.app.GetComponents().UpdateStatusBar(helpText) 21 | } 22 | 23 | // ClearCustomHelp clears the custom help text 24 | func (n *Navigation) ClearCustomHelp() { 25 | n.customHelp = "" 26 | n.app.GetComponents().ClearStatusBar() 27 | } 28 | -------------------------------------------------------------------------------- /internal/api/session.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "slv.sh/slv/internal/core/session" 8 | ) 9 | 10 | func getSession(context *gin.Context, session *session.Session) { 11 | if session.SecretKey() == nil { 12 | context.AbortWithStatusJSON(http.StatusUnauthorized, apiResponse{Success: false, Error: "Unauthorized"}) 13 | return 14 | } 15 | env, err := session.Env() 16 | if err != nil { 17 | context.AbortWithStatusJSON(http.StatusInternalServerError, apiResponse{Success: false, Error: err.Error()}) 18 | } 19 | data := map[string]any{ 20 | "publicKeys": map[string]string{ 21 | "publicKeyEC": session.PublicKeyEC(), 22 | "publicKeyPQ": session.PublicKeyPQ(), 23 | }, 24 | } 25 | if env != nil { 26 | data["env"] = env 27 | } 28 | context.JSON(http.StatusOK, apiResponse{Success: true, Data: data}) 29 | } 30 | -------------------------------------------------------------------------------- /internal/k8s/config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting nameReference. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | -------------------------------------------------------------------------------- /internal/k8s/.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 5m 3 | allow-parallel-runners: true 4 | 5 | issues: 6 | # don't skip warning about doc comments 7 | # don't exclude the default set of lint 8 | exclude-use-default: false 9 | # restore some of the defaults 10 | # (fill in the rest as needed) 11 | exclude-rules: 12 | - path: "api/*" 13 | linters: 14 | - lll 15 | - path: "internal/*" 16 | linters: 17 | - dupl 18 | - lll 19 | linters: 20 | disable-all: true 21 | enable: 22 | - dupl 23 | - errcheck 24 | - exportloopref 25 | - goconst 26 | - gocyclo 27 | - gofmt 28 | - goimports 29 | - gosimple 30 | - govet 31 | - ineffassign 32 | - lll 33 | - misspell 34 | - nakedret 35 | - prealloc 36 | - staticcheck 37 | - typecheck 38 | - unconvert 39 | - unparam 40 | - unused 41 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-lib/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: slvlib 3 | description: Base Library chart for SLV 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: library 14 | # This is the chart version. This version number should be incremented each time you make changes 15 | # to the chart and its templates, including the app version. 16 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 17 | 18 | version: 0.0.0 -------------------------------------------------------------------------------- /internal/k8s/config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | # Prometheus Monitor Service (Metrics) 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | labels: 6 | control-plane: controller-manager 7 | app.kubernetes.io/name: servicemonitor 8 | app.kubernetes.io/instance: controller-manager-metrics-monitor 9 | app.kubernetes.io/component: metrics 10 | app.kubernetes.io/created-by: operator 11 | app.kubernetes.io/part-of: operator 12 | app.kubernetes.io/managed-by: kustomize 13 | name: controller-manager-metrics-monitor 14 | namespace: system 15 | spec: 16 | endpoints: 17 | - path: /metrics 18 | port: https 19 | scheme: https 20 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 21 | tlsConfig: 22 | insecureSkipVerify: true 23 | selector: 24 | matchLabels: 25 | control-plane: controller-manager 26 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdprofile/sync.go: -------------------------------------------------------------------------------- 1 | package cmdprofile 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | "github.com/spf13/cobra" 8 | "slv.sh/slv/internal/cli/commands/utils" 9 | "slv.sh/slv/internal/core/profiles" 10 | ) 11 | 12 | func profileSyncCommand() *cobra.Command { 13 | if profileSyncCmd == nil { 14 | profileSyncCmd = &cobra.Command{ 15 | Use: "sync", 16 | Aliases: []string{"pull"}, 17 | Short: "Update the active profile from remote", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | profile, err := profiles.GetActiveProfile() 20 | if err != nil { 21 | utils.ExitOnError(err) 22 | } 23 | if err = profile.Pull(); err != nil { 24 | utils.ExitOnError(err) 25 | } 26 | fmt.Printf("Profile %s is updated from remote successfully\n", color.GreenString(profile.Name())) 27 | }, 28 | } 29 | } 30 | return profileSyncCmd 31 | } 32 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-operator/templates/validatingwebhookconfig.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: ValidatingWebhookConfiguration 3 | metadata: 4 | name: {{ .Values.webhook.validatingWebhookConfigName | default "slv-operator-validating-webhook" }} 5 | annotations: 6 | {{- toYaml .Values.webhook.validatingWebhookConfigAnnotations | nindent 4 }} 7 | webhooks: 8 | - admissionReviewVersions: 9 | - v1 10 | clientConfig: 11 | service: 12 | name: {{ .Values.webhook.serviceName | default "slv-webhook-service" }} 13 | namespace: {{ .Release.Namespace }} 14 | path: /validate-slv-sh-v1-slv 15 | failurePolicy: Fail 16 | name: slv-webhook-service.{{ .Release.Namespace }}.svc 17 | rules: 18 | - apiGroups: 19 | - slv.sh 20 | apiVersions: 21 | - v1 22 | operations: 23 | - CREATE 24 | - UPDATE 25 | resources: 26 | - slvs 27 | sideEffects: None 28 | -------------------------------------------------------------------------------- /website/docs/command-reference/profile/list.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # List Profiles 6 | Shows all the SLV profiles available on local 7 | 8 | #### General Usage: 9 | ```bash 10 | slv profile list [flags] 11 | ``` 12 | #### Flags: 13 | | Flag | Arguments | Required | Default | Description | 14 | | -- | -- | -- | -- | -- | 15 | | --help | None | NA | NA|Help text for `slv profile list` | 16 | 17 | #### Usage: 18 | ```bash 19 | slv profile list 20 | ``` 21 | #### Example: 22 | ```bash 23 | $ slv profile list 24 | my_org_slv_profile 25 | my_local_slv_profile 26 | ``` 27 | The active profile is shown in a different color. 28 | 29 | --- 30 | 31 | ## See Also 32 | 33 | - [New Profile](/docs/command-reference/profile/new) - Create a new profile 34 | - [Set Active Profile](/docs/command-reference/profile/set-active) - Switch between profiles 35 | - [Profile Component](/docs/components/profile) - Learn more about profiles 36 | -------------------------------------------------------------------------------- /internal/core/config/appdata.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | 8 | "slv.sh/slv/internal/core/commons" 9 | ) 10 | 11 | func GetAppDataDir() string { 12 | if appDataDir == nil { 13 | slvAppDataRoot := os.Getenv(envar_SLV_APP_DATA_DIR) 14 | var err error 15 | if slvAppDataRoot == "" { 16 | slvAppDataRoot, err = os.UserConfigDir() 17 | if err != nil { 18 | log.Fatalf("Error while getting slv app data path: %v", err) 19 | } 20 | slvAppDataRoot = filepath.Join(slvAppDataRoot, AppNameLowerCase) 21 | } 22 | if !commons.DirExists(slvAppDataRoot) { 23 | err := os.MkdirAll(slvAppDataRoot, 0755) 24 | if err != nil { 25 | log.Fatalf("Error in creating the app data directory: %v", err) 26 | } 27 | } 28 | appDataDir = &slvAppDataRoot 29 | } 30 | return *appDataDir 31 | } 32 | 33 | func ResetAppDataDir() error { 34 | return os.RemoveAll(GetAppDataDir()) 35 | } 36 | -------------------------------------------------------------------------------- /internal/helpers/vault_new.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "slv.sh/slv/internal/core/crypto" 5 | "slv.sh/slv/internal/core/vaults" 6 | ) 7 | 8 | func NewVault(vaultFile, name, k8sNamespace string, enableHash, pq bool, pkStrList []string) (*vaults.Vault, error) { 9 | var pubKeys []*crypto.PublicKey 10 | for _, pkStr := range pkStrList { 11 | pubKey, err := crypto.PublicKeyFromString(pkStr) 12 | if err != nil { 13 | return nil, err 14 | } 15 | pubKeys = append(pubKeys, pubKey) 16 | } 17 | return vaults.New(vaultFile, name, k8sNamespace, enableHash, pq, pubKeys...) 18 | } 19 | 20 | func UpdateVault(vaultFile, name, k8sNamespace, secretType string, k8SecretContent []byte) (*vaults.Vault, error) { 21 | vlt, err := vaults.Get(vaultFile) 22 | if err != nil { 23 | return nil, err 24 | } 25 | if err = vlt.Update(name, k8sNamespace, secretType, k8SecretContent); err != nil { 26 | return nil, err 27 | } 28 | return vlt, nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/k8s/config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: operator 10 | app.kubernetes.io/part-of: operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /internal/tui/pages/vault_new/lists.go: -------------------------------------------------------------------------------- 1 | package vault_new 2 | 3 | import "github.com/rivo/tview" 4 | 5 | func (vnp *VaultNewPage) createSearchResultsList() *tview.List { 6 | searchResults := tview.NewList() 7 | searchResults.SetBorder(true) 8 | searchResults.AddItem("", "", 0, nil) 9 | searchResults.SetTitle("Environment Results From Profile").SetTitleAlign(tview.AlignLeft) 10 | searchResults.SetWrapAround(false) // Disable looping behavior 11 | vnp.searchResults = searchResults 12 | return searchResults 13 | } 14 | 15 | func (vnp *VaultNewPage) createGrantedAccessList() *tview.List { 16 | grantedAccess := tview.NewList() 17 | grantedAccess.SetBorder(true).SetTitle("Environments With Access").SetTitleAlign(tview.AlignLeft) 18 | grantedAccess.AddItem("📝 No access granted yet", "Add public keys or environments to grant access", 0, nil) 19 | grantedAccess.SetWrapAround(false) // Disable looping behavior 20 | vnp.grantedAccess = grantedAccess 21 | return grantedAccess 22 | } 23 | -------------------------------------------------------------------------------- /internal/k8s/test/e2e/e2e_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | ) 26 | 27 | // Run e2e tests using the Ginkgo runner. 28 | func TestE2E(t *testing.T) { 29 | RegisterFailHandler(Fail) 30 | fmt.Fprintf(GinkgoWriter, "Starting operator suite\n") 31 | RunSpecs(t, "e2e suite") 32 | } 33 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdprofile/list.go: -------------------------------------------------------------------------------- 1 | package cmdprofile 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | "github.com/spf13/cobra" 8 | "slv.sh/slv/internal/cli/commands/utils" 9 | "slv.sh/slv/internal/core/profiles" 10 | ) 11 | 12 | func profileListCommand() *cobra.Command { 13 | if profileListCmd == nil { 14 | profileListCmd = &cobra.Command{ 15 | Use: "list", 16 | Aliases: []string{"ls"}, 17 | Short: "Lists all profiles", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | profileNames, err := profiles.List() 20 | if err != nil { 21 | utils.ExitOnError(err) 22 | } else { 23 | activeProfileName, _ := profiles.GetActiveProfileName() 24 | for _, profileName := range profileNames { 25 | if profileName == activeProfileName { 26 | fmt.Println(color.GreenString(profileName)) 27 | } else { 28 | fmt.Println(profileName) 29 | } 30 | } 31 | } 32 | }, 33 | } 34 | } 35 | return profileListCmd 36 | } 37 | -------------------------------------------------------------------------------- /internal/core/commons/http.go: -------------------------------------------------------------------------------- 1 | package commons 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | func GetURLContents(url string, headers map[string]string) ([]byte, error) { 10 | req, err := http.NewRequest("GET", url, nil) 11 | if err != nil { 12 | return nil, fmt.Errorf("failed to create request: %w", err) 13 | } 14 | for key, value := range headers { 15 | req.Header.Set(key, value) 16 | } 17 | client := &http.Client{} 18 | resp, err := client.Do(req) 19 | if err != nil { 20 | return nil, fmt.Errorf("request failed: %w", err) 21 | } 22 | defer resp.Body.Close() 23 | if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound { 24 | return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 25 | } 26 | bodyBytes, err := io.ReadAll(resp.Body) 27 | if err != nil { 28 | return nil, fmt.Errorf("failed to read response body: %w", err) 29 | } 30 | if resp.StatusCode == http.StatusNotFound { 31 | return nil, nil 32 | } 33 | return bodyBytes, nil 34 | } 35 | -------------------------------------------------------------------------------- /website/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 2 | 3 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 4 | 5 | /** 6 | * Creating a sidebar enables you to: 7 | - create an ordered group of docs 8 | - render a sidebar for each doc of that group 9 | - provide next/previous navigation 10 | 11 | The sidebars can be generated from the filesystem, or explicitly defined here. 12 | 13 | Create as many sidebars as you want. 14 | */ 15 | const sidebars: SidebarsConfig = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | documentationSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | export default sidebars; 34 | -------------------------------------------------------------------------------- /website/docs/command-reference/profile/set-active.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Set Active Profile 6 | Set an already added SLV profile as the active profile. 7 | #### General usage: 8 | ```bash 9 | slv profile activate [flags] 10 | ``` 11 | #### Flags: 12 | | Flag | Arguments | Required | Default | Description | 13 | | -- | -- | -- | -- | -- | 14 | | --name | String | True | NA | Name of the profile to set | 15 | | --help | None | NA | NA|Help text for `slv profile activate` | 16 | 17 | #### Usage: 18 | ```bash 19 | slv profile activate --name 20 | ``` 21 | #### Example: 22 | ```bash 23 | $ slv profile activate --name my_other_profile 24 | Successfully set my_other_profile as active profile 25 | ``` 26 | 27 | --- 28 | 29 | ## See Also 30 | 31 | - [List Profiles](/docs/command-reference/profile/list) - View all available profiles 32 | - [New Profile](/docs/command-reference/profile/new) - Create a new profile 33 | - [Profile Component](/docs/components/profile) - Learn more about profiles 34 | -------------------------------------------------------------------------------- /website/docs/command-reference/profile/sync.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | # Sync Profile with Remote 5 | Sync the local cache of your profile with the remote profile (git/http). SLV automatically syncs periodically as specified by the `--sync-interval` flag. You can use this command to manually sync profiles. 6 | #### General Usage: 7 | ```bash 8 | slv profile sync [flags] 9 | ``` 10 | #### Flags: 11 | | Flag | Arguments | Required | Default | Description | 12 | | -- | -- | -- | -- | -- | 13 | | --help | None | NA | NA | Help text for `slv profile sync` | 14 | #### Usage: 15 | ```bash 16 | slv profile sync 17 | ``` 18 | #### Example: 19 | ```bash 20 | $ slv profile sync 21 | Profile test is updated from remote successfully 22 | ``` 23 | 24 | --- 25 | 26 | ## See Also 27 | 28 | - [New Profile](/docs/command-reference/profile/new) - Create a new profile 29 | - [List Profiles](/docs/command-reference/profile/list) - View all available profiles 30 | - [Profile Component](/docs/components/profile) - Learn more about profiles 31 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-operator/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- include "slvlib.clusterrole" . | nindent 0 }} 2 | 3 | --- 4 | {{- if not (.Values.webhook.disableAutomaticCertManagement) }} 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: ClusterRole 7 | metadata: 8 | name: slv-webhook-clusterrole 9 | rules: 10 | - apiGroups: ["admissionregistration.k8s.io"] 11 | resources: ["validatingwebhookconfigurations"] 12 | resourceNames: [{{ .Values.webhook.validatingWebhookConfigName }}] 13 | verbs: ["get", "patch","update"] 14 | - apiGroups: ["admissionregistration.k8s.io"] 15 | resources: ["validatingwebhookconfigurations"] 16 | verbs: ["list","watch"] 17 | - apiGroups: [""] 18 | resources: ["secrets"] 19 | verbs: ["get","create", "update", "delete"] 20 | {{- if gt (int .Values.replicas) 1 }} 21 | - apiGroups: ["coordination.k8s.io"] 22 | resources: ["leases"] 23 | verbs: ["get","create","update"] 24 | - apiGroups: [""] 25 | resources: ["events"] 26 | verbs: ["create"] 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdprofile/profile.go: -------------------------------------------------------------------------------- 1 | package cmdprofile 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "slv.sh/slv/internal/core/profiles" 6 | ) 7 | 8 | func ProfileCommand() *cobra.Command { 9 | if profileCmd == nil { 10 | profileCmd = &cobra.Command{ 11 | Use: "profile", 12 | Aliases: []string{"profiles"}, 13 | Short: "Manage SLV profiles", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | cmd.Help() 16 | }, 17 | } 18 | profileCmd.AddCommand(profileNewCommand()) 19 | profileCmd.AddCommand(profileSetActiveCommand()) 20 | profileCmd.AddCommand(profileListCommand()) 21 | profileCmd.AddCommand(profileDeleteCommand()) 22 | profileCmd.AddCommand(profileSyncCommand()) 23 | } 24 | return profileCmd 25 | } 26 | 27 | func profileNameCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 28 | profileNames, err := profiles.List() 29 | if err != nil { 30 | return nil, cobra.ShellCompDirectiveError 31 | } 32 | return profileNames, cobra.ShellCompDirectiveNoFileComp 33 | } 34 | -------------------------------------------------------------------------------- /internal/k8s/config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/slv.sh_slvs.yaml 6 | #+kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patches: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | - path: patches/webhook_in_slvs.yaml 12 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | - path: patches/cainjection_in_slvs.yaml 17 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # [WEBHOOK] To enable webhook, uncomment the following section 20 | # the following config is for teaching kustomize how to do kustomization for CRDs. 21 | 22 | configurations: 23 | - kustomizeconfig.yaml 24 | -------------------------------------------------------------------------------- /website/docs/command-reference/profile/del.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | # Delete a profile 5 | Deletes an existing profile. Note that you **cannot delete the active profile**. 6 | #### General Usage: 7 | ```bash 8 | slv profile rm [flags] 9 | ``` 10 | #### Flags: 11 | | Flag | Arguments | Required | Default | Description | 12 | | -- | -- | -- | -- | -- | 13 | | --name | String | True | NA | Name of the profile to delete | 14 | | --help | None | NA | NA|Help text for `slv profile delete` | 15 | 16 | #### Usage: 17 | ```bash 18 | slv profile rm --name 19 | ``` 20 | #### Example: 21 | ```bash 22 | $ slv profile rm --name my_org 23 | Deleted profile: my_org 24 | ``` 25 | 26 | --- 27 | 28 | ## See Also 29 | 30 | - [List Profiles](/docs/command-reference/profile/list) - View all available profiles 31 | - [New Profile](/docs/command-reference/profile/new) - Create a new profile 32 | - [Set Active Profile](/docs/command-reference/profile/set-active) - Switch between profiles 33 | - [Profile Component](/docs/components/profile) - Learn more about profiles 34 | -------------------------------------------------------------------------------- /internal/k8s/job/job.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "log" 5 | 6 | "k8s.io/client-go/kubernetes" 7 | "k8s.io/client-go/tools/clientcmd" 8 | "slv.sh/slv/internal/core/config" 9 | "slv.sh/slv/internal/core/session" 10 | ) 11 | 12 | var logger = log.Default() 13 | 14 | func Run() { 15 | logger.Println("Starting SLV job...") 16 | logger.Println(config.VersionInfo()) 17 | 18 | secretKey, err := session.GetSecretKey() 19 | if err != nil { 20 | logger.Fatal(err) 21 | } 22 | 23 | kubeCfg := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 24 | clientcmd.NewDefaultClientConfigLoadingRules(), 25 | &clientcmd.ConfigOverrides{}, 26 | ) 27 | 28 | config, err := kubeCfg.ClientConfig() 29 | if err != nil { 30 | logger.Fatal(err) 31 | } 32 | 33 | clientset, err := kubernetes.NewForConfig(config) 34 | if err != nil { 35 | logger.Fatal(err) 36 | } 37 | 38 | slvObjs, err := listSLVs(config) 39 | if err != nil { 40 | logger.Fatal(err) 41 | } 42 | 43 | if err = slvsToSecrets(clientset, secretKey, slvObjs); err != nil { 44 | logger.Fatal(err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /action.yaml: -------------------------------------------------------------------------------- 1 | name: Setup SLV 2 | description: Sets up the SLV CLI and injects vault secrets as masked environment variables for workflows. 3 | inputs: 4 | version: 5 | description: SLV version to use (e.g., 'latest' or a specific version) 6 | required: false 7 | default: latest 8 | github-token: 9 | description: GitHub token (e.g., secrets.GITHUB_TOKEN) for downloading SLV to avoid rate limits 10 | required: false 11 | default: ${{ github.token }} 12 | vault: 13 | description: Path to the vault file containing secrets 14 | required: false 15 | env-secret-key: 16 | description: SLV environment secret key (recommended to set via GitHub secrets) 17 | required: false 18 | selective: 19 | description: Comma-separated list of secret names from the vault file to inject as environment variables 20 | required: false 21 | prefix: 22 | description: Prefix to prepend to environment variable names derived from SLV secrets 23 | required: false 24 | branding: 25 | icon: unlock 26 | color: white 27 | runs: 28 | using: node20 29 | main: action/dist/index.js 30 | -------------------------------------------------------------------------------- /internal/sharedlib/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "C" 5 | ) 6 | 7 | // SLVGetSecret retuns the value of a single secret from a given vault for the specified secret name 8 | // 9 | //export SLVGetSecret 10 | func SLVGetSecret(vaultPath *C.char, secretName *C.char, secretValue **C.char, secretLength *C.int, errMessage **C.char, errLength *C.int) { 11 | getSecret(vaultPath, secretName, secretValue, secretLength, errMessage, errLength) 12 | } 13 | 14 | // SLVGetAllSecrets returns all the secrets from a given vault as a JSON string 15 | // 16 | //export SLVGetAllSecrets 17 | func SLVGetAllSecrets(vaultPath *C.char, secretsJson **C.char, secretsJsonLength *C.int, errMessage **C.char, errLength *C.int) { 18 | getAllSecrets(vaultPath, secretsJson, secretsJsonLength, errMessage, errLength) 19 | } 20 | 21 | // SLVPutSecret writes a secret to the vault 22 | // 23 | //export SLVPutSecret 24 | func SLVPutSecret(vaultPath *C.char, secretName *C.char, secretValue *C.char, errMessage **C.char, errLength *C.int) { 25 | putSecret(vaultPath, secretName, secretValue, errMessage, errLength) 26 | } 27 | 28 | func main() {} 29 | -------------------------------------------------------------------------------- /action/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setup-slv", 3 | "description": "This action downloads and sets up the SLV CLI and helps in setting vault secrets as masked environment variables for workflows to consume", 4 | "main": "index.js", 5 | "scripts": { 6 | "lint": "eslint .", 7 | "prepare": "ncc build index.js -o dist --source-map --license licenses.txt", 8 | "test": "jest", 9 | "all": "npm run lint && npm run prepare && npm run test" 10 | }, 11 | "keywords": [ 12 | "actions", 13 | "slv", 14 | "cli", 15 | "secret-manager", 16 | "setup" 17 | ], 18 | "homepage": "https://github.com/amagioss/slv", 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/amagioss/slv.git" 23 | }, 24 | "dependencies": { 25 | "@actions/core": "^1.11.1", 26 | "@actions/exec": "^1.1.1", 27 | "@actions/tool-cache": "^2.0.2", 28 | "@octokit/auth-token": "^6.0.0", 29 | "@octokit/rest": "^22.0.1" 30 | }, 31 | "devDependencies": { 32 | "@vercel/ncc": "^0.38.4", 33 | "eslint": "^9.39.1", 34 | "jest": "^30.2.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/k8s/api/v1/slv_webhook_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1 18 | 19 | import ( 20 | . "github.com/onsi/ginkgo/v2" 21 | ) 22 | 23 | var _ = Describe("SLV Webhook", func() { 24 | 25 | Context("When creating SLV under Validating Webhook", func() { 26 | It("Should deny if a required field is empty", func() { 27 | 28 | // TODO(user): Add your logic here 29 | 30 | }) 31 | 32 | It("Should admit if all required fields are provided", func() { 33 | 34 | // TODO(user): Add your logic here 35 | 36 | }) 37 | }) 38 | 39 | }) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 The SLV Authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/tui/navigation/input.go: -------------------------------------------------------------------------------- 1 | package navigation 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | ) 6 | 7 | // HandleEscape handles the escape key based on current context 8 | func (n *Navigation) HandleEscape() *tcell.EventKey { 9 | switch n.app.GetRouter().GetCurrentPage() { 10 | case "main": 11 | // On main page, escape quits the app 12 | n.app.Quit() 13 | return nil 14 | case "help": 15 | // On help page, escape goes back 16 | n.GoBack() 17 | return nil 18 | default: 19 | // On other pages, escape goes back 20 | n.GoBack() 21 | return nil 22 | } 23 | } 24 | 25 | // setupInputHandling sets up global input handling for the navigation 26 | func (n *Navigation) setupInputHandling() { 27 | n.app.GetApplication().SetInputCapture(n.handleGlobalInput) 28 | } 29 | 30 | // handleGlobalInput handles global input events 31 | func (n *Navigation) handleGlobalInput(event *tcell.EventKey) *tcell.EventKey { 32 | switch event.Key() { 33 | case tcell.KeyCtrlC: 34 | n.app.Quit() 35 | return nil 36 | case tcell.KeyF1: 37 | n.ShowHelp(false) 38 | return nil 39 | case tcell.KeyEsc: 40 | return n.HandleEscape() 41 | } 42 | return event 43 | } 44 | -------------------------------------------------------------------------------- /internal/tui/pages/environments_new/state.go: -------------------------------------------------------------------------------- 1 | package environments_new 2 | 3 | // SaveNavigationState implements the Page interface 4 | func (nep *NewEnvironmentPage) SaveNavigationState() { 5 | // State is preserved in the page struct 6 | } 7 | 8 | // RestoreNavigationState implements the Page interface 9 | func (nep *NewEnvironmentPage) RestoreNavigationState() { 10 | // Update help text when navigating back 11 | if nep.navigation != nil { 12 | nep.navigation.updateHelpText() 13 | } 14 | 15 | // Set focus to current component 16 | if component := nep.GetCurrentComponent(); component != nil { 17 | nep.GetTUI().GetApplication().SetFocus(component) 18 | } 19 | } 20 | 21 | // ClearNavigationState implements the Page interface 22 | func (nep *NewEnvironmentPage) ClearNavigationState() { 23 | // Reset to initial state when clearing 24 | nep.currentStep = StepProviderSelection 25 | nep.selectedType = "" 26 | nep.envName = "" 27 | nep.envEmail = "" 28 | nep.envTags = nil 29 | nep.quantumSafe = false 30 | nep.selectedProvider = "" 31 | nep.providerInputs = make(map[string]string) 32 | nep.addToProfile = false 33 | nep.createdEnv = nil 34 | nep.secretKey = "" 35 | } 36 | -------------------------------------------------------------------------------- /internal/core/settings/settings.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "slv.sh/slv/internal/core/commons" 5 | ) 6 | 7 | type Settings struct { 8 | path *string 9 | AllowChanges bool `json:"allowChanges" yaml:"allowChanges"` 10 | AllowCreateEnv bool `json:"allowCreateEnv" yaml:"allowCreateEnv"` 11 | AllowCreateGroup bool `json:"allowCreateGroup" yaml:"allowCreateGroup"` 12 | SyncInterval int `json:"syncInterval" yaml:"syncInterval"` 13 | AllowGroups bool `json:"allowGroups" yaml:"allowGroups"` 14 | AllowVaultSharing bool `json:"allowVaultSharing" yaml:"allowVaultSharing"` 15 | } 16 | 17 | func NewManifest(path string) (settings *Settings, err error) { 18 | if commons.FileExists(path) { 19 | return nil, errManifestPathExistsAlready 20 | } 21 | settings = &Settings{ 22 | path: &path, 23 | } 24 | return 25 | } 26 | 27 | func GetManifest(path string) (settings *Settings, err error) { 28 | if !commons.FileExists(path) { 29 | return nil, errManifestNotFound 30 | } 31 | settings = &Settings{} 32 | if err = commons.ReadFromYAML(path, settings); err != nil { 33 | return nil, err 34 | } 35 | settings.path = &path 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdprofile/delete.go: -------------------------------------------------------------------------------- 1 | package cmdprofile 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | "github.com/spf13/cobra" 8 | "slv.sh/slv/internal/cli/commands/utils" 9 | "slv.sh/slv/internal/core/profiles" 10 | ) 11 | 12 | func profileDeleteCommand() *cobra.Command { 13 | if profileDelCmd == nil { 14 | profileDelCmd = &cobra.Command{ 15 | Use: "rm", 16 | Aliases: []string{"remove", "del", "delete"}, 17 | Short: "Removes a profile", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | name, _ := cmd.Flags().GetString(profileNameFlag.Name) 20 | if err := profiles.Delete(name); err != nil { 21 | utils.ExitOnError(err) 22 | } else { 23 | fmt.Println("Deleted profile: ", color.GreenString(name)) 24 | utils.SafeExit() 25 | } 26 | }, 27 | } 28 | profileDelCmd.Flags().StringP(profileNameFlag.Name, profileNameFlag.Shorthand, "", profileNameFlag.Usage) 29 | profileDelCmd.MarkFlagRequired(profileNameFlag.Name) 30 | if err := profileDelCmd.RegisterFlagCompletionFunc(profileNameFlag.Name, profileNameCompletion); err != nil { 31 | utils.ExitOnError(err) 32 | } 33 | } 34 | return profileDelCmd 35 | } 36 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-lib/templates/_notes.tpl: -------------------------------------------------------------------------------- 1 | {{- define "slvlib.notes" -}} 2 | SLV Install Successful. 3 | 4 | {{- if and (empty .Values.k8sSecret) (empty .Values.secretBinding) }} 5 | WARNING: You have not set the value for ".Values.secretBinding" or "Values.slvEnvironment.k8sSecret". 6 | SLV will now look for a secret named "slv" in the "{{ .Release.Namespace }}" namespace. 7 | If a secret is not found, SLV will not run as expected and return an error. 8 | 9 | Ensure that you have set atleast one of the following 10 | - secret key for the environment (under key "SecretKey") 11 | - secret binding for the environment (under key "SecretBinding"), 12 | under the secret name "slv" 13 | in namespace "{{ .Release.Namespace }}" 14 | {{- end -}} 15 | 16 | {{- if ne .Values.k8sSecret "" }} 17 | SLV will get the environment secret key/binding from a preloaded kubernetes secret. 18 | 19 | Ensure that you have set atleast one of the following 20 | - secret binding for the environment (under key "SecretBinding"), 21 | - secret key for the environment (under key "SecretKey") 22 | under the secret name "{{ .Values.k8sSecret }}" 23 | in namespace "{{ .Release.Namespace }}" 24 | {{- end }} 25 | 26 | {{- end }} 27 | -------------------------------------------------------------------------------- /website/docs/command-reference/vault/rm.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | # Remove a Secret 5 | Delete an item from a vault. 6 | #### General Usage: 7 | ```bash 8 | slv vault --vault rm [flags] 9 | ``` 10 | #### Flags: 11 | | Flag | Arguments | Required | Default | Description | 12 | | -- | -- | -- | -- | -- | 13 | | --name | String | False | None | Name of the item (key) to delete | 14 | | --vault | String | True | NA | Path to the SLV Vault file | 15 | | --help | None | NA | NA | Help text for `slv vault rm` | 16 | 17 | #### Usage: 18 | ```bash 19 | slv vault --vault rm --name 20 | ``` 21 | #### Example: 22 | ```bash 23 | $ slv vault --vault test.slv.yaml rm --name my_secret 24 | Successfully deleted the secrets: [my_secret] from the vault: test.slv.yaml 25 | ``` 26 | 27 | --- 28 | 29 | ## See Also 30 | 31 | - [Put a Secret](/docs/command-reference/vault/put) - Add secrets to your vault 32 | - [Get a Secret](/docs/command-reference/vault/get) - Retrieve secrets from your vault 33 | - [Update Vault Attributes](/docs/command-reference/vault/update) - Update vault metadata 34 | - [Vault Component](/docs/components/vault) - Learn more about vaults 35 | -------------------------------------------------------------------------------- /internal/core/crypto/const.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "errors" 5 | 6 | "slv.sh/slv/internal/core/config" 7 | ) 8 | 9 | const ( 10 | publicKeyAbbrev = "PK" // PK = Public Key 11 | secretKeyAbbrev = "SK" // SK = Secret Key 12 | wrappedKeyAbbrev = "WK" // WK = Wrapped Key 13 | sealedSecretAbbrev = "SS" // SS = Sealed Secret 14 | slvPrefix = config.AppNameUpperCase 15 | cryptoVersion uint8 = 1 16 | 17 | hashMaxLength = 4 18 | ) 19 | 20 | var ( 21 | errUnsupportedCryptoVersion = errors.New("unsupported cryptography version") 22 | errGeneratingSecretKey = errors.New("error generating a new secret key") 23 | errDerivingPublicKey = errors.New("error deriving public key from the secret key") 24 | errInvalidPublicKeyFormat = errors.New("invalid public key format") 25 | errInvalidSecretKeyFormat = errors.New("invalid secret key format") 26 | errEncryptionFailed = errors.New("encryption failed") 27 | errDecryptionFailed = errors.New("decryption failed") 28 | errSecretKeyMismatch = errors.New("given secret key cannot decrypt the data") 29 | errInvalidCiphertextFormat = errors.New("invalid ciphertext format") 30 | ) 31 | -------------------------------------------------------------------------------- /website/docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | # Installation 5 | 6 | ## Installation 7 | 8 | ### MacOS (Homebrew) 9 | You can install SLV in mac using [homebrew](https://brew.sh/), the package manager for MacOS. 10 | ```bash 11 | brew install amagioss/slv/slv 12 | ``` 13 | 14 | ### Linux 15 | You can use the official installation script to install SLV on linux. (This will work on MacOS too.) 16 | ```bash 17 | curl -fsSL https://slv.sh/scripts/install.sh | sh 18 | ``` 19 | 20 | ### Windows 21 | You can use the powershell script to install SLV on windows 22 | ```powershell 23 | irm https://slv.sh/scripts/install.ps1 | iex 24 | ``` 25 | 26 | --- 27 | 28 | ## Install a specific version 29 | You can use the same install script to install specific versions of SLV 30 | ### MacOS/Linux 31 | ```bash 32 | curl -fsSL https://slv.sh/scripts/install.sh | sh -s v0.16.3 33 | ``` 34 | ### Windows (Powershell) 35 | ```powershell 36 | $v="v0.16.3"; irm https://slv.sh/scripts/install.ps1 | iex 37 | ``` 38 | 39 | --- 40 | 41 | ## Verify Installation 42 | Once installed, you can verify if SLV was installed correctly by entering 43 | ```bash 44 | slv --version 45 | ``` 46 | You should see the SLV version and build information in the console. 47 | 48 | --- 49 | -------------------------------------------------------------------------------- /internal/core/commons/compression.go: -------------------------------------------------------------------------------- 1 | package commons 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "io" 7 | ) 8 | 9 | func zCompress(data []byte) (compressedData []byte, err error) { 10 | var buf bytes.Buffer 11 | writer := zlib.NewWriter(&buf) 12 | if _, err = writer.Write(data); err == nil { 13 | if writer.Close() == nil { 14 | return buf.Bytes(), nil 15 | } 16 | } 17 | return 18 | } 19 | 20 | func zDecompress(compressedData []byte) (data []byte, err error) { 21 | reader, err := zlib.NewReader(bytes.NewReader(compressedData)) 22 | if err == nil { 23 | defer reader.Close() 24 | return io.ReadAll(reader) 25 | } 26 | return 27 | } 28 | 29 | func Compress(data []byte) (compressedBytes []byte, err error) { 30 | dataSize := len(data) 31 | compressedData, err := zCompress(data) 32 | if err == nil { 33 | if dataSize <= len(compressedData) { 34 | compressedBytes = append([]byte{0}, data...) 35 | } else { 36 | compressedBytes = append([]byte{1}, compressedData...) 37 | } 38 | } 39 | return compressedBytes, err 40 | } 41 | 42 | func Decompress(compressedBytes []byte) (data []byte, err error) { 43 | preProcessedBytes := compressedBytes[1:] 44 | if compressedBytes[0] == 1 { 45 | return zDecompress(preProcessedBytes) 46 | } 47 | return preProcessedBytes, nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/k8s/config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | securityContext: 14 | allowPrivilegeEscalation: false 15 | capabilities: 16 | drop: 17 | - "ALL" 18 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0 19 | args: 20 | - "--secure-listen-address=0.0.0.0:8443" 21 | - "--upstream=http://127.0.0.1:8080/" 22 | - "--logtostderr=true" 23 | - "--v=0" 24 | ports: 25 | - containerPort: 8443 26 | protocol: TCP 27 | name: https 28 | resources: 29 | limits: 30 | cpu: 500m 31 | memory: 128Mi 32 | requests: 33 | cpu: 5m 34 | memory: 64Mi 35 | - name: manager 36 | args: 37 | - "--health-probe-bind-address=:8081" 38 | - "--metrics-bind-address=127.0.0.1:8080" 39 | - "--leader-elect" 40 | -------------------------------------------------------------------------------- /pages/scripts/install.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | $ErrorActionPreference = 'Stop' 4 | 5 | if ($v) { 6 | $Version = "v${v}" 7 | } 8 | if ($Args.Length -eq 1) { 9 | $Version = $Args.Get(0) 10 | } 11 | 12 | $SLVInstall = $env:SLV_INSTALL 13 | $BinDir = if ($SLVInstall) { 14 | "${SLVInstall}\bin" 15 | } else { 16 | "${Home}\.slv\bin" 17 | } 18 | 19 | $SLVZip = "$BinDir\slv.zip" 20 | $SLVExe = "$BinDir\slv.exe" 21 | $Target = 'windows_amd64' 22 | 23 | $DownloadUrl = if (!$Version) { 24 | "https://github.com/amagioss/slv/releases/latest/download/slv_${Target}.zip" 25 | } else { 26 | "https://github.com/amagioss/slv/releases/download/${Version}/slv_${Target}.zip" 27 | } 28 | 29 | if (!(Test-Path $BinDir)) { 30 | New-Item $BinDir -ItemType Directory | Out-Null 31 | } 32 | 33 | curl.exe -Lo $SLVZip $DownloadUrl 34 | 35 | tar.exe xf $SLVZip -C $BinDir 36 | 37 | Remove-Item $SLVZip 38 | 39 | $User = [System.EnvironmentVariableTarget]::User 40 | $Path = [System.Environment]::GetEnvironmentVariable('Path', $User) 41 | if (!(";${Path};".ToLower() -like "*;${BinDir};*".ToLower())) { 42 | [System.Environment]::SetEnvironmentVariable('Path', "${Path};${BinDir}", $User) 43 | $Env:Path += ";${BinDir}" 44 | } 45 | 46 | Write-Output "SLV was installed successfully to ${SLVExe}" 47 | Write-Output "Run 'slv --help' to get started" 48 | -------------------------------------------------------------------------------- /internal/tui/interfaces/tui.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | "slv.sh/slv/internal/tui/theme" 6 | ) 7 | 8 | // TUIInterface defines the interface that pages can use to interact with the TUI 9 | type TUIInterface interface { 10 | // Navigation methods 11 | GetNavigation() NavigationInterface 12 | 13 | // UI components 14 | GetInfoBar() tview.Primitive 15 | UpdateStatusBar(text string) 16 | ClearStatusBar() 17 | 18 | // Page layout method 19 | CreatePageLayout(title string, content tview.Primitive) tview.Primitive 20 | 21 | // Modal methods 22 | ShowError(message string) 23 | ShowInfo(message string) 24 | ShowConfirmation(message string, onConfirm func(), onCancel func()) 25 | ShowConfirmationWithFocus(message string, confirmButtonText string, cancelButtonText string, onConfirm func(), onCancel func(), restoreFocus func()) 26 | ShowModalForm(title string, form *tview.Form, confirmButtonText string, cancelButtonText string, onConfirm func(), onCancel func(), restoreFocus func()) 27 | ShowModal(title string, content tview.Primitive, restoreFocus func()) 28 | 29 | // Application control 30 | Quit() 31 | GetApplication() *tview.Application 32 | 33 | // Theme access 34 | GetTheme() *theme.Theme 35 | 36 | // Core components access 37 | GetComponents() ComponentManagerInterface 38 | GetRouter() RouterInterface 39 | } 40 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-job/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: slv-job 3 | description: Job/CronJob to reconcile SLV CRs to Secrets 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.0.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: 0.0.0 25 | 26 | dependencies: 27 | - name: slvlib 28 | version: 0.0.0 29 | repository: file://../slv-lib 30 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-operator/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: slv-operator 3 | description: Controller that reconciles SLV CRs to Secrets 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.0.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: 0.0.0 25 | 26 | dependencies: 27 | - name: slvlib 28 | version: 0.0.0 29 | repository: file://../slv-lib 30 | -------------------------------------------------------------------------------- /internal/tui/interfaces/router.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | ) 6 | 7 | // RouterInterface defines the interface for router functionality 8 | type RouterInterface interface { 9 | // Basic tview.Pages operations 10 | AddPage(name string, page tview.Primitive, resize, visible bool) 11 | RemovePage(name string) 12 | HasPage(name string) bool 13 | GetPages() *tview.Pages 14 | 15 | // Page stack management 16 | PushPage(page string) 17 | PopPage() string 18 | ClearStack() 19 | GetCurrentPage() string 20 | SetCurrentPage(page string) 21 | GetPageStack() []string 22 | 23 | // Page interface support (legacy - for backward compatibility) 24 | RegisterPage(name string, page Page) 25 | GetRegisteredPage(name string) Page 26 | HasRegisteredPage(name string) bool 27 | GetRegisteredPageNames() []string 28 | 29 | // Page factory support 30 | RegisterPageFactory(name string, factory PageFactory) 31 | CreatePage(tui TUIInterface, name string, params ...interface{}) Page 32 | HasPageFactory(name string) bool 33 | GetPageFactoryNames() []string 34 | 35 | // Infrastructure methods (to avoid duplication in Navigation) 36 | AddPageToMainComponent(name string, page tview.Primitive, components ComponentManagerInterface) 37 | NavigateToPage(name string, components ComponentManagerInterface, replace bool) 38 | GoBackWithComponents(components ComponentManagerInterface) error 39 | } 40 | -------------------------------------------------------------------------------- /internal/core/commons/encoding.go: -------------------------------------------------------------------------------- 1 | package commons 2 | 3 | import ( 4 | "encoding/base32" 5 | "encoding/json" 6 | ) 7 | 8 | var ( 9 | base32Encoding = base32.StdEncoding.WithPadding(base32.NoPadding) 10 | ) 11 | 12 | func jsonSerialize(data any) (dataBytes []byte, err error) { 13 | dataBytes, err = json.Marshal(data) 14 | if err != nil { 15 | return nil, err 16 | } 17 | return dataBytes, nil 18 | } 19 | 20 | func jsonDeserialize(dataBytes []byte, data any) (err error) { 21 | err = json.Unmarshal(dataBytes, &data) 22 | if err != nil { 23 | return err 24 | } 25 | return nil 26 | } 27 | 28 | func Serialize(data any) (string, error) { 29 | serialized, err := jsonSerialize(data) 30 | if err != nil { 31 | return "", err 32 | } 33 | serialized, err = Compress(serialized) 34 | if err != nil { 35 | return "", err 36 | } 37 | return Encode(serialized), nil 38 | } 39 | 40 | func Deserialize(serialized string, data any) (err error) { 41 | serializedBytes, err := Decode(serialized) 42 | if err != nil { 43 | return err 44 | } 45 | serializedBytes, err = Decompress(serializedBytes) 46 | if err != nil { 47 | return err 48 | } 49 | return jsonDeserialize(serializedBytes, &data) 50 | } 51 | 52 | func Encode(data []byte) (encoded string) { 53 | return base32Encoding.EncodeToString(data) 54 | } 55 | 56 | func Decode(encoded string) (data []byte, err error) { 57 | return base32Encoding.DecodeString(encoded) 58 | } 59 | -------------------------------------------------------------------------------- /internal/core/environments/const.go: -------------------------------------------------------------------------------- 1 | package environments 2 | 3 | import ( 4 | "errors" 5 | 6 | "slv.sh/slv/internal/core/config" 7 | "slv.sh/slv/internal/core/crypto" 8 | ) 9 | 10 | const ( 11 | envDefStringAbbrev = "EDS" // Environment Definition String 12 | EnvironmentKey crypto.KeyType = 'E' 13 | USER EnvType = "user" 14 | SERVICE EnvType = "service" 15 | slvPrefix = config.AppNameUpperCase 16 | selfEnvFileName = ".self" 17 | ) 18 | 19 | var ( 20 | errInvalidEnvDef = errors.New("invalid environment definition string") 21 | errInvalidEnvironmentType = errors.New("invalid environment type") 22 | errEnvironmentPublicKeyNotFound = errors.New("environment public key not found") 23 | errManifestPathExistsAlready = errors.New("manifest path exists already") 24 | errManifestNotFound = errors.New("manifest not found") 25 | errWritingManifest = errors.New("error in writing manifest") 26 | errRootExistsAlready = errors.New("root environment exists already") 27 | errMarkingSelfEnvBindingNotFound = errors.New("error in marking environment as self - env secret binding not found") 28 | errMarkingSelfNonUserEnv = errors.New("error in marking environment as self - non user environment") 29 | errEnvNotFound = errors.New("environment not found") 30 | ) 31 | -------------------------------------------------------------------------------- /internal/core/config/version.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | func VersionInfo() string { 11 | if appInfo == nil { 12 | appInfo = new(string) 13 | var committedAt string 14 | if builtAtTime, err := time.Parse(time.RFC3339, commitDate); err == nil { 15 | builtAtLocalTime := builtAtTime.Local() 16 | committedAt = builtAtLocalTime.Format("02 Jan 2006 03:04:05 PM MST") 17 | } 18 | appInfoBuilder := strings.Builder{} 19 | appInfoBuilder.WriteString(ColorizedArt()) 20 | appInfoBuilder.WriteString("\n") 21 | appInfoBuilder.WriteString(description) 22 | appInfoBuilder.WriteString("\n") 23 | appInfoBuilder.WriteString("-------------------------------------------------") 24 | appInfoBuilder.WriteString("\n") 25 | appInfoBuilder.WriteString(fmt.Sprintf("SLV Version : %s\n", Version)) 26 | appInfoBuilder.WriteString(fmt.Sprintf("Built At : %s\n", committedAt)) 27 | appInfoBuilder.WriteString(fmt.Sprintf("Release : %s\n", releaseURL)) 28 | appInfoBuilder.WriteString(fmt.Sprintf("Git Commit : %s\n", fullCommit)) 29 | appInfoBuilder.WriteString(fmt.Sprintf("Web : %s\n", website)) 30 | appInfoBuilder.WriteString(fmt.Sprintf("Platform : %s\n", runtime.GOOS+"/"+runtime.GOARCH)) 31 | appInfoBuilder.WriteString(fmt.Sprintf("Go Version : %s", runtime.Version())) 32 | *appInfo = appInfoBuilder.String() 33 | } 34 | return *appInfo 35 | } 36 | -------------------------------------------------------------------------------- /internal/tui/pages/vault_edit/lists.go: -------------------------------------------------------------------------------- 1 | package vault_edit 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | ) 6 | 7 | func (vep *VaultEditPage) createSearchResultsList() *tview.List { 8 | searchResults := tview.NewList() 9 | searchResults.SetBorder(true) 10 | searchResults.AddItem("", "", 0, nil) 11 | 12 | // Set title based on vault access state 13 | title := "Environment Results From Profile" 14 | if !vep.IsVaultUnlocked() { 15 | title = "Environment Results (Locked - No Access)" 16 | // Apply disabled styling when vault is locked 17 | vep.applyDisabledStyling(searchResults) 18 | } 19 | 20 | searchResults.SetTitle(title).SetTitleAlign(tview.AlignLeft) 21 | searchResults.SetWrapAround(false) // Disable looping behavior 22 | vep.searchResults = searchResults 23 | return searchResults 24 | } 25 | 26 | func (vep *VaultEditPage) createGrantedAccessList() *tview.List { 27 | grantedAccess := tview.NewList() 28 | grantedAccess.SetBorder(true) 29 | 30 | // Set title based on vault access state 31 | title := "Environments With Access" 32 | if !vep.IsVaultUnlocked() { 33 | title = "Environments With Access (Locked - No Access)" 34 | // Apply disabled styling when vault is locked 35 | vep.applyDisabledStyling(grantedAccess) 36 | } 37 | 38 | grantedAccess.SetTitle(title).SetTitleAlign(tview.AlignLeft) 39 | grantedAccess.SetWrapAround(false) // Disable looping behavior 40 | vep.grantedAccess = grantedAccess 41 | return grantedAccess 42 | } 43 | -------------------------------------------------------------------------------- /internal/tui/interfaces/page.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "github.com/rivo/tview" 6 | ) 7 | 8 | // Page defines the interface that all pages must implement 9 | type Page interface { 10 | // Create returns the tview.Primitive for this page 11 | Create() tview.Primitive 12 | 13 | // Refresh refreshes the page content (useful for dynamic content) 14 | Refresh() 15 | 16 | // HandleInput handles input events specific to this page 17 | HandleInput(event *tcell.EventKey) *tcell.EventKey 18 | 19 | // GetTitle returns the page title 20 | GetTitle() string 21 | 22 | // SaveNavigationState saves the current navigation state for restoration 23 | SaveNavigationState() 24 | 25 | // RestoreNavigationState restores the saved navigation state 26 | RestoreNavigationState() 27 | 28 | // ClearNavigationState clears the saved navigation state 29 | ClearNavigationState() 30 | } 31 | 32 | // PageFactory defines the interface for creating page instances 33 | type PageFactory interface { 34 | // CreatePage creates a new page instance with the given parameters 35 | CreatePage(params ...interface{}) Page 36 | } 37 | 38 | // PageFactoryFunc is a function type that implements PageFactory 39 | type PageFactoryFunc func(params ...interface{}) Page 40 | 41 | // CreatePage implements the PageFactory interface for PageFactoryFunc 42 | func (f PageFactoryFunc) CreatePage(params ...interface{}) Page { 43 | return f(params...) 44 | } 45 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slv-docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.9.2", 19 | "@docusaurus/preset-classic": "3.9.2", 20 | "@docusaurus/theme-mermaid": "^3.9.2", 21 | "@easyops-cn/docusaurus-search-local": "^0.52.1", 22 | "@mdx-js/react": "^3.1.1", 23 | "clsx": "^2.1.1", 24 | "prism-react-renderer": "^2.4.1", 25 | "react": "^19.2.0", 26 | "react-dom": "^19.2.0" 27 | }, 28 | "devDependencies": { 29 | "@docusaurus/module-type-aliases": "3.9.2", 30 | "@docusaurus/tsconfig": "3.9.2", 31 | "@docusaurus/types": "3.9.2", 32 | "typescript": "~5.9.3" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.5%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 3 chrome version", 42 | "last 3 firefox version", 43 | "last 5 safari version" 44 | ] 45 | }, 46 | "engines": { 47 | "node": ">=18.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /website/docs/command-reference/vault/update.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 10 3 | --- 4 | 5 | # Update Vault Attributes 6 | Update the metadata of a vault 7 | #### General Usage: 8 | ```bash 9 | slv vault --vault update [flags] 10 | ``` 11 | #### Flags: 12 | | Flag | Arguments | Required | Default | Description | 13 | | -- | -- | -- | -- | -- | 14 | | --name | String | False | None | The name to update the vault name with | 15 | | --k8s-namespace | String | False | None | Namespace for the K8S Custom Resource | 16 | | --vault | String | True | NA | Path to the SLV Vault file | 17 | | --help | None | NA | NA | Help text for `slv vault update` | 18 | 19 | When none of the flags above are given, the command would update the vault structure into a K8S compatible resource YAML. 20 | 21 | #### Usage 22 | ```bash 23 | slv vault --vault test.slv.yaml update --name --k8s-namespace 24 | ``` 25 | 26 | #### Example: 27 | ```bash 28 | $ slv vault --vault test.slv.yaml update --name new_vault --k8s-namespace slv 29 | Vault test.slv.yaml transformed to K8s resource new_vault 30 | ``` 31 | 32 | --- 33 | 34 | ## See Also 35 | 36 | - [Create a New Vault](/docs/command-reference/vault/new) - Create a new vault 37 | - [Put a Secret](/docs/command-reference/vault/put) - Add secrets to your vault 38 | - [Kubernetes Operator](/docs/extensions/slv-in-kubernetes/operator) - Use SLV with Kubernetes 39 | - [Vault Component](/docs/components/vault) - Learn more about vaults 40 | -------------------------------------------------------------------------------- /internal/tui/pages/vault_edit/styles.go: -------------------------------------------------------------------------------- 1 | package vault_edit 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | "slv.sh/slv/internal/tui/theme" 6 | ) 7 | 8 | func (vep *VaultEditPage) applyDisabledStyling(component tview.Primitive) { 9 | colors := theme.GetCurrentPalette() 10 | switch c := component.(type) { 11 | case *tview.Form: 12 | // Use very muted colors for disabled appearance 13 | c.SetLabelColor(colors.FormDisabledText). 14 | SetTitleColor(colors.FormDisabledText) 15 | case *tview.List: 16 | // Apply muted colors to lists 17 | c.SetSelectedTextColor(colors.FormDisabledText). 18 | SetSelectedBackgroundColor(colors.FormDisabledBg). 19 | SetSecondaryTextColor(colors.FormDisabledText). 20 | SetMainTextColor(colors.FormDisabledText). 21 | SetBorderColor(colors.FormDisabledBorder). 22 | SetTitleColor(colors.FormDisabledText) 23 | case *tview.InputField: 24 | // Apply muted colors to input fields 25 | c.SetFieldTextColor(colors.FormDisabledText). 26 | SetLabelColor(colors.FormDisabledText). 27 | SetBorderColor(colors.FormDisabledBorder). 28 | SetTitleColor(colors.FormDisabledText) 29 | case *tview.Table: 30 | // Apply muted colors to tables 31 | c.SetBorderColor(colors.FormDisabledBorder). 32 | SetTitleColor(colors.FormDisabledText) 33 | // Note: Table doesn't have SetSelectedTextColor/SetSelectedBackgroundColor methods 34 | case *tview.Flex: 35 | c.SetBorderColor(colors.FormDisabledBorder). 36 | SetTitleColor(colors.FormDisabledText) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdprofile/active.go: -------------------------------------------------------------------------------- 1 | package cmdprofile 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | "github.com/spf13/cobra" 8 | "slv.sh/slv/internal/cli/commands/utils" 9 | "slv.sh/slv/internal/core/profiles" 10 | ) 11 | 12 | func profileSetActiveCommand() *cobra.Command { 13 | if profileSetActiveCmd == nil { 14 | profileSetActiveCmd = &cobra.Command{ 15 | Use: "activate", 16 | Aliases: []string{"active", "set-active", "current", "default", "set"}, 17 | Short: "Set a profile as the active profile", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | profileNames, err := profiles.List() 20 | if err != nil { 21 | utils.ExitOnError(err) 22 | } 23 | name, _ := cmd.Flags().GetString(profileNameFlag.Name) 24 | for _, profileName := range profileNames { 25 | if profileName == name { 26 | profiles.SetActiveProfile(name) 27 | fmt.Printf("Successfully set %s as the active profile\n", color.GreenString(name)) 28 | utils.SafeExit() 29 | } 30 | } 31 | utils.ExitOnError(fmt.Errorf("profile %s not found", name)) 32 | }, 33 | } 34 | profileSetActiveCmd.Flags().StringP(profileNameFlag.Name, profileNameFlag.Shorthand, "", profileNameFlag.Usage) 35 | profileSetActiveCmd.MarkFlagRequired(profileNameFlag.Name) 36 | if err := profileSetActiveCmd.RegisterFlagCompletionFunc(profileNameFlag.Name, profileNameCompletion); err != nil { 37 | utils.ExitOnError(err) 38 | } 39 | } 40 | return profileSetActiveCmd 41 | } 42 | -------------------------------------------------------------------------------- /internal/k8s/api/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1 contains API Schema definitions for the slv v1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=slv.sh 20 | package v1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | "slv.sh/slv/internal/core/config" 26 | ) 27 | 28 | const ( 29 | Group = config.K8SLVGroup 30 | Version = config.K8SLVVersion 31 | Kind = config.K8SLVKind 32 | ) 33 | 34 | var ( 35 | // GroupVersion is group version used to register these objects 36 | GroupVersion = schema.GroupVersion{Group: Group, Version: Version} 37 | 38 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 39 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 40 | 41 | // AddToScheme adds the types in this group-version to the given scheme. 42 | AddToScheme = SchemeBuilder.AddToScheme 43 | ) 44 | -------------------------------------------------------------------------------- /internal/helpers/vault_list.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "slv.sh/slv/internal/core/config" 10 | ) 11 | 12 | const ( 13 | vaultFileNameExt = config.AppNameLowerCase 14 | ) 15 | 16 | func ListVaultFiles(dir string, recursive bool) (vaultFiles []string, err error) { 17 | if dir == "" { 18 | dir, err = os.Getwd() 19 | if err != nil { 20 | return nil, err 21 | } 22 | } 23 | if recursive { 24 | err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { 25 | if d.IsDir() { 26 | return nil 27 | } 28 | if strings.HasSuffix(d.Name(), "."+vaultFileNameExt) || 29 | strings.HasSuffix(d.Name(), vaultFileNameExt+".yaml") || 30 | strings.HasSuffix(d.Name(), vaultFileNameExt+".yml") { 31 | if relPath, err := filepath.Rel(dir, path); err == nil { 32 | vaultFiles = append(vaultFiles, relPath) 33 | } else { 34 | vaultFiles = append(vaultFiles, path) 35 | } 36 | } 37 | return nil 38 | }) 39 | } else { 40 | files, err := os.ReadDir(dir) 41 | if err != nil { 42 | return nil, err 43 | } 44 | for _, file := range files { 45 | if file.IsDir() { 46 | continue 47 | } 48 | if strings.HasSuffix(file.Name(), "."+vaultFileNameExt) || 49 | strings.HasSuffix(file.Name(), vaultFileNameExt+".yaml") || 50 | strings.HasSuffix(file.Name(), vaultFileNameExt+".yml") { 51 | vaultFiles = append(vaultFiles, file.Name()) 52 | } 53 | } 54 | } 55 | return vaultFiles, err 56 | } 57 | -------------------------------------------------------------------------------- /internal/core/vaults/item.go: -------------------------------------------------------------------------------- 1 | package vaults 2 | 3 | import ( 4 | "time" 5 | 6 | "slv.sh/slv/internal/core/crypto" 7 | ) 8 | 9 | type VaultItem struct { 10 | value []byte `json:"-"` 11 | rawValue string `json:"-"` 12 | plaintext bool `json:"-"` 13 | encryptedAt *time.Time `json:"-"` 14 | hash string `json:"-"` 15 | vlt *Vault `json:"-"` 16 | } 17 | 18 | func (vi *VaultItem) Vault() *Vault { 19 | return vi.vlt 20 | } 21 | 22 | func (vi *VaultItem) Value() (value []byte, err error) { 23 | if vi.value == nil { 24 | if !vi.IsPlaintext() { 25 | if vi.vlt.IsLocked() { 26 | return nil, errVaultLocked 27 | } 28 | sealedSecret := &crypto.SealedSecret{} 29 | if err = sealedSecret.FromString(vi.rawValue); err == nil { 30 | vi.value, err = vi.vlt.Spec.secretKey.DecryptSecret(*sealedSecret) 31 | } 32 | if err != nil { 33 | return nil, err 34 | } 35 | } else { 36 | vi.value = []byte(vi.rawValue) 37 | } 38 | } 39 | return vi.value, err 40 | } 41 | 42 | func (vi *VaultItem) ValueString() (value string, err error) { 43 | var valueBytes []byte 44 | if valueBytes, err = vi.Value(); err == nil { 45 | value = string(valueBytes) 46 | } 47 | return 48 | } 49 | 50 | func (vi *VaultItem) IsPlaintext() bool { 51 | return vi.plaintext 52 | } 53 | 54 | func (vi *VaultItem) EncryptedAt() *time.Time { 55 | return vi.encryptedAt 56 | } 57 | 58 | func (vi *VaultItem) Hash() string { 59 | return vi.hash 60 | } 61 | 62 | func (vi *VaultItem) String() string { 63 | return vi.rawValue 64 | } 65 | -------------------------------------------------------------------------------- /website/docs/command-reference/vault/run.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 9 3 | --- 4 | 5 | # Load Vault to Environment Variables 6 | Launch a shell or run a command with the secrets loaded as Environment Variables in it. 7 | #### General Usage: 8 | ```bash 9 | slv vault --vault run [flags] 10 | ``` 11 | #### Flags: 12 | | Flag | Arguments | Required | Default | Description | 13 | | -- | -- | -- | -- | -- | 14 | | --command | String | False | $SHELL | The command to be run | 15 | | --prefix | String | False | None | Prefix to add to the ENVAR | 16 | | --vault | String | True | NA | Path to the SLV Vault file or Vault URL | 17 | | --help | None | NA | NA | Help text for `slv vault run` | 18 | 19 | #### Usage: 20 | ```bash 21 | slv vault --vault run -c 22 | ``` 23 | 24 | #### Example: 25 | ```bash 26 | $ slv vault --vault test.slv.yaml run -c bash --prefix SLV_ENV_VAR_ 27 | Running command [bash] with secrets loaded into environment variables from the vault test.slv.yaml... 28 | Please note that the secret names are prefixed with SLV_ENV_VAR_ 29 | 30 | $ env | grep SLV_ENV_VAR 31 | SLV_ENV_VAR_username=johndoe 32 | SLV_ENV_VAR_password=super_secret_password 33 | 34 | $ exit 35 | exit 36 | Command [bash] ended successfully 37 | ``` 38 | 39 | --- 40 | 41 | ## See Also 42 | 43 | - [Get a Secret](/docs/command-reference/vault/get) - Retrieve secrets from your vault 44 | - [Vault Component](/docs/components/vault) - Learn more about vaults 45 | - [GitHub Actions Integration](/docs/extensions/slv-in-github-actions) - Use SLV in CI/CD pipelines 46 | -------------------------------------------------------------------------------- /website/docs/extensions/slv-in-github-actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 7 3 | --- 4 | 5 | # Github Actions 6 | 7 | ## Inputs 8 | - `version`: The version of slv to install, defaulting to `latest` 9 | - `vault`: Path to the vault file 10 | - `env-secret-key`: The SLV environment secret (key/binding) to use for the action 11 | - `prefix`: Prefix to use for the environment variable names along with the SLV secret name 12 | 13 | ## Use Cases 14 | ### Set Up SLV CLI 15 | You can use the action to set up SLV CLI on the runner. 16 | ```yaml 17 | steps: 18 | - name: Setup SLV 19 | uses: amagioss/slv@v 20 | ``` 21 | 22 | #### Install a Specific Version 23 | ```yaml 24 | steps: 25 | - name: Setup SLV 26 | uses: amagioss/slv@v 27 | with: 28 | version: 0.16.3 29 | ``` 30 | 31 | ### Load SLV Secrets Into Environment Variables 32 | You can use the action to load secrets from a vault into environment variables that can further be consumed by other actions or programs. 33 | ```yaml 34 | steps: 35 | - name: Load SLV Secrets 36 | uses: amagioss/slv@v 37 | with: 38 | vault: creds.slv.yaml 39 | env-secret-key: ${{ secrets.SLV_ENV_SECRET_KEY }} 40 | ``` 41 | 42 | #### Set a Prefix for Variables 43 | If you'd like to set a prefix across all the environment variables created by SLV, you can do so by specifying the `prefix` parameter. 44 | ```yaml 45 | steps: 46 | - name: Load SLV Secrets - PROD 47 | uses: amagioss/slv@v 48 | with: 49 | vault: creds.slv.yaml 50 | env-secret-key: ${{ secrets.SLV_ENV_SECRET_KEY }} 51 | prefix: "PROD_" 52 | ``` 53 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdenv/set.go: -------------------------------------------------------------------------------- 1 | package cmdenv 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | "github.com/spf13/cobra" 8 | "slv.sh/slv/internal/cli/commands/utils" 9 | "slv.sh/slv/internal/core/environments" 10 | "slv.sh/slv/internal/core/input" 11 | ) 12 | 13 | func envSetSelfCommand() *cobra.Command { 14 | if envSetSelfSCmd == nil { 15 | envSetSelfSCmd = &cobra.Command{ 16 | Use: "set-self", 17 | Aliases: []string{"self-set", "register-self", "self-register", "register"}, 18 | Short: "Registers an environment in the current host as self", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | envDef := cmd.Flag(envDefFlag.Name).Value.String() 21 | env, err := environments.FromDefStr(envDef) 22 | if err != nil { 23 | utils.ExitOnError(err) 24 | } 25 | if env.EnvType != environments.USER { 26 | utils.ExitOnError(fmt.Errorf("only user environments can be registered as self")) 27 | } 28 | if env.SecretBinding == "" { 29 | secretBinding, err := input.GetVisibleInput("Enter the secret binding: ") 30 | if err != nil { 31 | utils.ExitOnError(err) 32 | } 33 | env.SecretBinding = secretBinding 34 | } 35 | if err = env.SetAsSelf(); err != nil { 36 | utils.ExitOnError(err) 37 | } 38 | ShowEnv(*env, true, true) 39 | fmt.Println(color.GreenString("Successfully registered self environment")) 40 | }, 41 | } 42 | envSetSelfSCmd.Flags().StringP(envDefFlag.Name, envDefFlag.Shorthand, "", envDefFlag.Usage) 43 | envSetSelfSCmd.MarkFlagRequired(envDefFlag.Name) 44 | } 45 | return envSetSelfSCmd 46 | } 47 | -------------------------------------------------------------------------------- /internal/core/environments/envproviders/kms.go: -------------------------------------------------------------------------------- 1 | package envproviders 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/sha256" 7 | "crypto/x509" 8 | "encoding/pem" 9 | "errors" 10 | "os" 11 | 12 | "slv.sh/slv/internal/core/commons" 13 | ) 14 | 15 | const ( 16 | sealedSecretKeyRefName = "ssk" 17 | rsaPubKeyRefName = "rsa-pubkey" 18 | ) 19 | 20 | var ( 21 | errInvalidRSAPublicKey = errors.New("invalid RSA public key") 22 | errSealedSecretKeyRef = errors.New("invalid sealed secret key from provider binding") 23 | 24 | rsaArg = arg{ 25 | id: rsaPubKeyRefName, 26 | name: "RSA public key", 27 | required: false, 28 | description: "RSA public key (file path or PEM encoded content) for offline binding [Only RSA OAEP SHA-256 will be supported - recommended 4096 bits key]", 29 | } 30 | ) 31 | 32 | func rsaEncrypt(plain, rsaPublicKey []byte) (encrypted []byte, err error) { 33 | if commons.FileExists(string(rsaPublicKey)) { 34 | keyFilePath := string(rsaPublicKey) 35 | if rsaPublicKey, err = os.ReadFile(keyFilePath); err != nil || len(rsaPublicKey) == 0 { 36 | return nil, errors.New("failed to read RSA public key from file: " + keyFilePath) 37 | } 38 | } 39 | block, _ := pem.Decode(rsaPublicKey) 40 | if block == nil { 41 | return nil, errInvalidRSAPublicKey 42 | } 43 | pub, err := x509.ParsePKIXPublicKey(block.Bytes) 44 | if err != nil { 45 | return nil, err 46 | } 47 | rsaPub, ok := pub.(*rsa.PublicKey) 48 | if !ok { 49 | return nil, errInvalidRSAPublicKey 50 | } 51 | return rsa.EncryptOAEP(sha256.New(), rand.Reader, rsaPub, plain, []byte("")) 52 | } 53 | -------------------------------------------------------------------------------- /internal/k8s/config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. 4 | apiVersion: cert-manager.io/v1 5 | kind: Issuer 6 | metadata: 7 | labels: 8 | app.kubernetes.io/name: certificate 9 | app.kubernetes.io/instance: serving-cert 10 | app.kubernetes.io/component: certificate 11 | app.kubernetes.io/created-by: operator 12 | app.kubernetes.io/part-of: operator 13 | app.kubernetes.io/managed-by: kustomize 14 | name: selfsigned-issuer 15 | namespace: system 16 | spec: 17 | selfSigned: {} 18 | --- 19 | apiVersion: cert-manager.io/v1 20 | kind: Certificate 21 | metadata: 22 | labels: 23 | app.kubernetes.io/name: certificate 24 | app.kubernetes.io/instance: serving-cert 25 | app.kubernetes.io/component: certificate 26 | app.kubernetes.io/created-by: operator 27 | app.kubernetes.io/part-of: operator 28 | app.kubernetes.io/managed-by: kustomize 29 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 30 | namespace: system 31 | spec: 32 | # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize 33 | dnsNames: 34 | - SERVICE_NAME.SERVICE_NAMESPACE.svc 35 | - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local 36 | issuerRef: 37 | kind: Issuer 38 | name: selfsigned-issuer 39 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 40 | -------------------------------------------------------------------------------- /pages/slv/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | SLV 10 | 47 | 48 | 49 |
50 |

SLV

51 |

Secure Local Vault

52 |

If you are not redirected, click here.

53 |
54 | 55 | 56 | -------------------------------------------------------------------------------- /internal/cli/slv.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "slv.sh/slv/internal/cli/commands/cmdenv" 8 | "slv.sh/slv/internal/cli/commands/cmdprofile" 9 | "slv.sh/slv/internal/cli/commands/cmdsystem" 10 | "slv.sh/slv/internal/cli/commands/cmdvault" 11 | "slv.sh/slv/internal/cli/commands/utils" 12 | "slv.sh/slv/internal/core/config" 13 | ) 14 | 15 | var ( 16 | slvCmd *cobra.Command 17 | versionCmd *cobra.Command 18 | webCmd *cobra.Command 19 | tuiCmd *cobra.Command 20 | 21 | versionFlag = utils.FlagDef{ 22 | Name: "version", 23 | Shorthand: "v", 24 | Usage: "Shows version info", 25 | } 26 | ) 27 | 28 | func slvCommand() *cobra.Command { 29 | if slvCmd == nil { 30 | slvCmd = &cobra.Command{ 31 | Use: "slv", 32 | Short: "SLV is a tool to encrypt secrets locally", 33 | Run: func(cmd *cobra.Command, args []string) { 34 | version, _ := cmd.Flags().GetBool(versionFlag.Name) 35 | if version { 36 | fmt.Println(config.VersionInfo()) 37 | } else { 38 | cmd.Help() 39 | } 40 | }, 41 | } 42 | slvCmd.Flags().BoolP(versionFlag.Name, versionFlag.Shorthand, false, versionFlag.Usage) 43 | slvCmd.AddCommand(versionCommand()) 44 | slvCmd.AddCommand(cmdsystem.SystemCommand()) 45 | slvCmd.AddCommand(cmdenv.EnvCommand()) 46 | slvCmd.AddCommand(cmdprofile.ProfileCommand()) 47 | slvCmd.AddCommand(cmdvault.VaultCommand()) 48 | slvCmd.AddCommand(webCommand()) 49 | slvCmd.AddCommand(tuiCommand()) 50 | } 51 | return slvCmd 52 | } 53 | 54 | func Run() { 55 | if err := slvCommand().Execute(); err != nil { 56 | utils.ExitOnError(err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/tui/components/maincontent.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | "slv.sh/slv/internal/tui/interfaces" 6 | ) 7 | 8 | // MainContent represents the main content area component 9 | type MainContent struct { 10 | tui interfaces.TUIInterface 11 | primitive *tview.Pages 12 | content tview.Primitive 13 | } 14 | 15 | // NewMainContent creates a new MainContent component 16 | func NewMainContent(tui interfaces.TUIInterface) *MainContent { 17 | mc := &MainContent{ 18 | tui: tui, 19 | primitive: tview.NewPages(), 20 | } 21 | return mc 22 | } 23 | 24 | // Render returns the primitive for this component 25 | func (mc *MainContent) Render() tview.Primitive { 26 | return mc.primitive 27 | } 28 | 29 | // Refresh refreshes the component with current data 30 | func (mc *MainContent) Refresh() { 31 | // Main content refreshes are handled by the router 32 | // This method is here for interface compliance 33 | } 34 | 35 | // SetFocus sets focus on the component 36 | func (mc *MainContent) SetFocus(focus bool) { 37 | // Pages component doesn't have SetFocus method 38 | // Focus is handled by the application 39 | } 40 | 41 | // SetContent sets the content for the main area 42 | func (mc *MainContent) SetContent(content tview.Primitive) { 43 | mc.content = content 44 | mc.primitive.AddPage("main", content, true, true) 45 | } 46 | 47 | // GetContent returns the current content 48 | func (mc *MainContent) GetContent() tview.Primitive { 49 | return mc.content 50 | } 51 | 52 | // GetPages returns the pages container for direct manipulation 53 | func (mc *MainContent) GetPages() *tview.Pages { 54 | return mc.primitive 55 | } 56 | -------------------------------------------------------------------------------- /pages/scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if ! command -v unzip >/dev/null && ! command -v 7z >/dev/null; then 6 | echo "Error: either unzip or 7z is required to install SLV" 1>&2 7 | exit 1 8 | fi 9 | 10 | if [ "$OS" = "Windows_NT" ]; then 11 | target="windows-amd64" 12 | else 13 | case $(uname -sm) in 14 | "Darwin x86_64") target="darwin_amd64" ;; 15 | "Darwin arm64") target="darwin_arm64" ;; 16 | "Linux aarch64") target="linux_arm64" ;; 17 | *) target="linux_amd64" ;; 18 | esac 19 | fi 20 | 21 | if [ $# -eq 0 ]; then 22 | slv_uri="https://github.com/amagioss/slv/releases/latest/download/slv_${target}.zip" 23 | else 24 | slv_uri="https://github.com/amagioss/slv/releases/download/${1}/slv_${target}.zip" 25 | fi 26 | 27 | slv_install="${SLV_INSTALL:-$HOME/.slv}" 28 | bin_dir="$slv_install/bin" 29 | exe="$bin_dir/slv" 30 | 31 | if [ ! -d "$bin_dir" ]; then 32 | mkdir -p "$bin_dir" 33 | fi 34 | 35 | curl --fail --location --progress-bar --output "$exe.zip" "$slv_uri" 36 | if command -v unzip >/dev/null; then 37 | unzip -d "$bin_dir" -o "$exe.zip" 38 | else 39 | 7z x -o"$bin_dir" -y "$exe.zip" 40 | fi 41 | chmod +x "$exe" 42 | rm "$exe.zip" 43 | 44 | echo "SLV was installed successfully to $exe" 45 | if command -v slv >/dev/null; then 46 | echo "Run 'slv --help' to get started" 47 | else 48 | case $SHELL in 49 | /bin/zsh) shell_profile=".zshrc" ;; 50 | *) shell_profile=".bashrc" ;; 51 | esac 52 | echo "Manually add the directory to your \$HOME/$shell_profile (or similar)" 53 | echo " export SLV_INSTALL=\"$slv_install\"" 54 | echo " export PATH=\"\$SLV_INSTALL/bin:\$PATH\"" 55 | echo "Run '$exe --help' to get started" 56 | fi 57 | echo 58 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdvault/delete.go: -------------------------------------------------------------------------------- 1 | package cmdvault 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | "github.com/spf13/cobra" 8 | "slv.sh/slv/internal/cli/commands/utils" 9 | "slv.sh/slv/internal/core/vaults" 10 | ) 11 | 12 | func vaultDeleteCommand() *cobra.Command { 13 | if vaultDeleteCmd == nil { 14 | vaultDeleteCmd = &cobra.Command{ 15 | Use: "rm", 16 | Aliases: []string{"del", "remove", "delete", "destroy", "erase"}, 17 | Short: "Removes an item from the vault", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | vaultFile := cmd.Flag(vaultFileFlag.Name).Value.String() 20 | vault, err := vaults.Get(vaultFile) 21 | if err != nil { 22 | utils.ExitOnError(err) 23 | } 24 | secretNames, err := cmd.Flags().GetStringSlice(itemNameFlag.Name) 25 | if err != nil { 26 | utils.ExitOnError(err) 27 | } 28 | if len(secretNames) == 0 { 29 | if err = vault.Delete(); err != nil { 30 | utils.ExitOnError(err) 31 | } 32 | fmt.Printf(color.GreenString("Successfully deleted the vault: %s\n"), vaultFile) 33 | } else { 34 | if err = vault.DeleteItems(secretNames); err != nil { 35 | utils.ExitOnError(err) 36 | } 37 | fmt.Printf(color.GreenString("Successfully deleted the secrets: %v from the vault: %s\n"), secretNames, vaultFile) 38 | } 39 | }, 40 | } 41 | vaultDeleteCmd.Flags().StringSliceP(itemNameFlag.Name, itemNameFlag.Shorthand, []string{}, itemNameFlag.Usage) 42 | if err := vaultDeleteCmd.RegisterFlagCompletionFunc(itemNameFlag.Name, vaultItemNameCompletion); err != nil { 43 | utils.ExitOnError(err) 44 | } 45 | } 46 | return vaultDeleteCmd 47 | } 48 | -------------------------------------------------------------------------------- /internal/api/auth.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/golang-jwt/jwt/v5" 11 | ) 12 | 13 | const ( 14 | jwtExpiry = 10 * time.Second 15 | ) 16 | 17 | func validateJWT(tokenString string, jwtSecret []byte) (jwt.MapClaims, error) { 18 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) { 19 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 20 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 21 | } 22 | return jwtSecret, nil 23 | }) 24 | if err != nil { 25 | return nil, fmt.Errorf("failed to parse or validate token: %v", err) 26 | } 27 | claims, ok := token.Claims.(jwt.MapClaims) 28 | if !ok || !token.Valid { 29 | return nil, fmt.Errorf("invalid token") 30 | } 31 | if claims["iat"] == nil || time.Unix(int64(claims["iat"].(float64)), 0).Add(jwtExpiry).Before(time.Now()) { 32 | return nil, fmt.Errorf("invalid token") 33 | } 34 | return claims, nil 35 | } 36 | 37 | func authMiddleware(jwtSecret []byte) gin.HandlerFunc { 38 | return func(context *gin.Context) { 39 | token := context.GetHeader("Authorization") 40 | if token == "" { 41 | context.JSON(http.StatusUnauthorized, apiResponse{Success: false, Error: "Unauthorized"}) 42 | context.Abort() 43 | return 44 | } 45 | token = strings.TrimPrefix(token, "Bearer ") 46 | claims, err := validateJWT(token, jwtSecret) 47 | if err != nil { 48 | context.JSON(http.StatusUnauthorized, apiResponse{Success: false, Error: err.Error()}) 49 | context.Abort() 50 | return 51 | } 52 | context.Set("claims", claims) 53 | context.Next() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/tui/pages/profiles/profiles.go: -------------------------------------------------------------------------------- 1 | package profiles 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "github.com/rivo/tview" 6 | "slv.sh/slv/internal/tui/interfaces" 7 | "slv.sh/slv/internal/tui/pages" 8 | "slv.sh/slv/internal/tui/theme" 9 | ) 10 | 11 | // ProfilesPage handles the profiles page functionality 12 | type ProfilesPage struct { 13 | pages.BasePage 14 | } 15 | 16 | // NewProfilesPage creates a new ProfilesPage instance 17 | func NewProfilesPage(tui interfaces.TUIInterface) *ProfilesPage { 18 | return &ProfilesPage{ 19 | BasePage: *pages.NewBasePage(tui, "Profiles"), 20 | } 21 | } 22 | 23 | // Create implements the Page interface 24 | func (pp *ProfilesPage) Create() tview.Primitive { 25 | // Create content 26 | text := tview.NewTextView(). 27 | SetText("Profiles Page\n\nThis page will show profile management options."). 28 | SetTextAlign(tview.AlignCenter). 29 | SetDynamicColors(true) 30 | 31 | // Style the text 32 | colors := theme.GetCurrentPalette() 33 | text.SetTextColor(colors.TextPrimary) 34 | 35 | // Update status bar 36 | pp.UpdateStatus("Profiles management - Coming soon") 37 | 38 | // Create layout using BasePage method 39 | return pp.CreateLayout(text) 40 | } 41 | 42 | // Refresh implements the Page interface 43 | func (pp *ProfilesPage) Refresh() { 44 | // Profiles page doesn't need refresh logic yet 45 | } 46 | 47 | // HandleInput implements the Page interface 48 | func (pp *ProfilesPage) HandleInput(event *tcell.EventKey) *tcell.EventKey { 49 | // Profiles page doesn't handle specific input yet 50 | return event 51 | } 52 | 53 | // GetTitle implements the Page interface 54 | func (pp *ProfilesPage) GetTitle() string { 55 | return pp.BasePage.GetTitle() 56 | } 57 | -------------------------------------------------------------------------------- /website/docs/command-reference/environment/del.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | # Delete an Environment 5 | 6 | Delete one or more environments based on search parameters such as `name`, `email`, `tags`. 7 | 8 | #### General Usage: 9 | ```bash 10 | slv env rm [flags] 11 | ``` 12 | 13 | #### Flags: 14 | | Flag | Arguments | Required | Default | Description | 15 | | -- | -- | -- | -- | -- | 16 | | --env-search | String(s) | False | None | Search for environments based on `tag`/`email`/`name` to delete | 17 | | --help | None | NA | NA| Help text for `slv env del` | 18 | 19 | #### Usage: 20 | ```bash 21 | slv env rm --env-search 22 | ``` 23 | #### Example: 24 | ```bash 25 | $ slv env rm --env-search alice 26 | Public Key: SLV_EPK_AEAUKAAAABUEMSPQ4BJIIWMSAKFUUXUV4THOP3ERH25CY4HR54W25HUJQR6XK 27 | Name: alice 28 | Email: alice@example.com 29 | Tags: [example_env] 30 | Secret Binding: SLV_ESB_AF4JYBGA2FXKUMAYADQHP6LPPMJM6JQDILREKS3KIHUQJWRZZKO5FQKCM4FHIRGR7DXPWHRQIACMHSNZVOOTJ7EDBGRAOODHEABHYIYY5O4Q23WD7TOZWSKIF66XVPZPLNXNTHJVNLVG3DH4N3237LZ7QMOJLGKSHHS6F7JQKOWCW3QLCYXLDYBBZFJ5ZRGVCEXXZVZYLR2ER33X3JLNJNHYICODVMPQ5VREN5GDSLSDLENJ6PUMFXKHZ5EHGOIGIT4TEW6LOYW6XMYR452BRPZSKKXLM5ZT7KHAVL64LPKNK45R3HAPH6IXAAAP772O3VBWC 31 | 32 | Are you sure you wish to delete the above environment(s) [yes/no]: yes 33 | Environment alice deleted successfully 34 | ``` 35 | 36 | --- 37 | 38 | ## See Also 39 | 40 | - [List Environments](/docs/command-reference/environment/list) - View all available environments 41 | - [Create a New Environment](/docs/command-reference/environment/new) - Create a new environment 42 | - [Environment Component](/docs/components/environment) - Learn more about environments 43 | -------------------------------------------------------------------------------- /internal/tui/core/application.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rivo/tview" 7 | ) 8 | 9 | // Application represents the main TUI application 10 | type Application struct { 11 | app *tview.Application 12 | router *Router 13 | theme Theme 14 | layout *LayoutManager 15 | ctx context.Context 16 | cancel context.CancelFunc 17 | } 18 | 19 | // NewApplication creates a new Application instance 20 | func NewApplication(theme Theme) *Application { 21 | ctx, cancel := context.WithCancel(context.Background()) 22 | 23 | return &Application{ 24 | app: tview.NewApplication(), 25 | router: NewRouter(), 26 | theme: theme, 27 | layout: NewLayoutManager(), 28 | ctx: ctx, 29 | cancel: cancel, 30 | } 31 | } 32 | 33 | // GetApplication returns the tview application 34 | func (a *Application) GetApplication() *tview.Application { 35 | return a.app 36 | } 37 | 38 | // GetRouter returns the router 39 | func (a *Application) GetRouter() *Router { 40 | return a.router 41 | } 42 | 43 | // GetTheme returns the theme 44 | func (a *Application) GetTheme() Theme { 45 | return a.theme 46 | } 47 | 48 | // GetLayoutManager returns the layout manager 49 | func (a *Application) GetLayoutManager() *LayoutManager { 50 | return a.layout 51 | } 52 | 53 | // GetContext returns the context 54 | func (a *Application) GetContext() context.Context { 55 | return a.ctx 56 | } 57 | 58 | // Cancel cancels the context 59 | func (a *Application) Cancel() { 60 | a.cancel() 61 | } 62 | 63 | // Run starts the application 64 | func (a *Application) Run() error { 65 | defer a.cancel() 66 | return a.app.Run() 67 | } 68 | 69 | // Stop stops the application 70 | func (a *Application) Stop() { 71 | a.cancel() 72 | a.app.Stop() 73 | } 74 | -------------------------------------------------------------------------------- /website/docs/components/vault.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Vault 6 | 7 | ## What is a vault? 8 | 9 | A **vault** in SLV is a container for storing a secret. To create a vault, see [Create a New Vault](/docs/command-reference/vault/new). 10 | 11 | As the name suggests, a vault represents a secure space where sensitive data — such as tokens, credentials, or API keys — can be stored and shared with specific environments. It holds the secret value, as well as a list of environments it is shared with. 12 | 13 | Each vault can be shared with multiple environments. These environments are identified by their public keys, which are used to determine who can access the secret. 14 | 15 | From a user’s perspective, a vault is: 16 | - A **named container** for storing multiple secrets. 17 | - A **sharing mechanism** for distributing secrets to specific environments. 18 | 19 | --- 20 | 21 | ## Write Without Access 22 | 23 | One of the defining features of SLV is that **you do not need access to a vault to put a secret into it**. 24 | This means: 25 | - Anyone can write to a vault, even if they cannot read from it. 26 | - Access to read secrets is strictly limited to environments the vault is shared with. 27 | 28 | This makes SLV particularly powerful in collaborative workflows. 29 | 30 | --- 31 | 32 | ## Related Topics 33 | 34 | - [List Vaults](/docs/command-reference/vault/list) - Discover and list all vaults in your project 35 | - [Create a New Vault](/docs/command-reference/vault/new) - Learn how to create vaults 36 | - [Put a Secret](/docs/command-reference/vault/put) - Add secrets to your vault 37 | - [Get a Secret](/docs/command-reference/vault/get) - Retrieve secrets from your vault 38 | - [Quick Start Guide](/docs/quick-start) - Get started with SLV 39 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdsystem/reset.go: -------------------------------------------------------------------------------- 1 | package cmdsystem 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | "github.com/spf13/cobra" 8 | "slv.sh/slv/internal/cli/commands/cmdenv" 9 | "slv.sh/slv/internal/cli/commands/utils" 10 | "slv.sh/slv/internal/core/config" 11 | "slv.sh/slv/internal/core/environments" 12 | "slv.sh/slv/internal/core/input" 13 | ) 14 | 15 | func systemResetCommand() *cobra.Command { 16 | if systemResetCmd == nil { 17 | systemResetCmd = &cobra.Command{ 18 | Use: "reset", 19 | Aliases: []string{"reset", "pruge", "prune", "clean", "clear"}, 20 | Short: "Reset the system", 21 | Long: `Cleans all existing profiles and any other data`, 22 | Run: func(cmd *cobra.Command, args []string) { 23 | selfEnv := environments.GetSelf() 24 | confirm, _ := cmd.Flags().GetBool(yesFlag.Name) 25 | if !confirm || selfEnv != nil { 26 | if selfEnv != nil { 27 | fmt.Println(color.YellowString("You have a configured environment which you might have to consider backing up:")) 28 | cmdenv.ShowEnv(*selfEnv, true, true) 29 | } 30 | var err error 31 | if confirm, err = input.GetConfirmation("Are you sure you wish to proceed? (yes/no): ", "yes"); err != nil { 32 | utils.ExitOnError(err) 33 | } 34 | } 35 | if confirm { 36 | err := config.ResetAppDataDir() 37 | if err == nil { 38 | fmt.Println(color.GreenString("System reset successful")) 39 | } else { 40 | utils.ExitOnError(err) 41 | } 42 | } else { 43 | fmt.Println(color.YellowString("System reset aborted")) 44 | } 45 | utils.SafeExit() 46 | }, 47 | } 48 | systemResetCmd.Flags().BoolP(yesFlag.Name, yesFlag.Shorthand, false, yesFlag.Usage) 49 | } 50 | return systemResetCmd 51 | } 52 | -------------------------------------------------------------------------------- /internal/core/profiles/remote.go: -------------------------------------------------------------------------------- 1 | package profiles 2 | 3 | import "sync" 4 | 5 | var ( 6 | remotes = make(map[string]*remote) 7 | remoteInitializer sync.Once 8 | ) 9 | 10 | func RegisterRemote(name string, setup setup, pull pull, push push, args []arg) { 11 | remotes[name] = &remote{ 12 | name: name, 13 | setup: setup, 14 | pull: pull, 15 | push: push, 16 | args: args, 17 | } 18 | } 19 | 20 | func registerDefaultRemotes() { 21 | remoteInitializer.Do(func() { 22 | RegisterRemote("git", gitSetup, gitPull, gitPush, gitArgs) 23 | RegisterRemote("http", httpSetup, httpPull, nil, httpArgs) 24 | }) 25 | } 26 | 27 | func ListRemoteNames() []string { 28 | registerDefaultRemotes() 29 | remoteNames := make([]string, 0, len(remotes)) 30 | for name := range remotes { 31 | remoteNames = append(remoteNames, name) 32 | } 33 | return remoteNames 34 | } 35 | 36 | func GetRemoteTypeArgs(name string) []arg { 37 | if remote, ok := remotes[name]; ok { 38 | return remote.args 39 | } 40 | return nil 41 | } 42 | 43 | type setup func(dir string, config map[string]string) error 44 | type pull func(dir string, config map[string]string) error 45 | type push func(dir string, config map[string]string, note string) error 46 | 47 | type remote struct { 48 | name string 49 | setup setup 50 | pull pull 51 | push push 52 | args []arg 53 | } 54 | 55 | type arg struct { 56 | name string 57 | required bool 58 | sensitive bool 59 | description string 60 | } 61 | 62 | func (a *arg) Name() string { 63 | return a.name 64 | } 65 | 66 | func (a *arg) Required() bool { 67 | return a.required 68 | } 69 | 70 | func (a *arg) Sensitive() bool { 71 | return a.sensitive 72 | } 73 | 74 | func (a *arg) Description() string { 75 | return a.description 76 | } 77 | -------------------------------------------------------------------------------- /website/docs/command-reference/environment/add.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Add an existing Environment 6 | 7 | Used to add an environment that is created elsewhere to the existing machine. The Environment Definition String (`EDS`) can be used to do the same. 8 | > **Note:** This is not available when using **read-only** profiles. 9 | 10 | #### General usage: 11 | ```bash 12 | slv env add [flags] 13 | ``` 14 | 15 | #### Flags: 16 | | Flag | Arguments | Required | Default | Description | 17 | | -- | -- | -- | -- | -- | 18 | | --env-def | String(s) | True | NA | EDS for the environment to be added | 19 | | --root | None | NA | NA| Set the environment as root environment for the active profile | 20 | | --help | None | NA | NA| Help text for `slv env add` | 21 | 22 | #### Usage: 23 | ```bash 24 | slv env add --env-def 25 | ``` 26 | 27 | #### Example: 28 | ```bash 29 | $ slv env add --env-def SLV_EDS_AF4JYRGM35FMGMA4YXYXOOOXYGFS2U6ISUEWMZUJWMS52H4HJCE7DRYIGS23IWRM4K5YWUGZ5X4RZPW7FA7V7GYUDVGRBKA6B22S4XJNWXODWKNFXTN24NXFU3YEPO7AYXETY4K33ENX7LP4WZMKZNBR67NLXNK5F22XE7D5HTWLSUJHGA6IMTAQUCXZBO4G5KA7UMKFAKB44IJVCCMJPW7ZOEK57447W3RW52XI4JQNRBPTADY7ZX6CBNBULMNHB6K5VN6UTYQYBH67AAAAB777TR3T5CA 30 | Successfully added 1 environments to profile my_org 31 | ``` 32 | 33 | --- 34 | 35 | ## See Also 36 | 37 | - [Create a New Environment](/docs/command-reference/environment/new) - Create a new environment 38 | - [List Environments](/docs/command-reference/environment/list) - View all available environments 39 | - [Show Environment](/docs/command-reference/environment/show) - View environment details 40 | - [Profile Component](/docs/components/profile) - Learn about profiles 41 | - [Environment Component](/docs/components/environment) - Learn more about environments 42 | -------------------------------------------------------------------------------- /action/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Stores VSCode versions used for testing VSCode extensions 80 | .vscode-test 81 | 82 | # yarn v2 83 | .yarn/cache 84 | .yarn/unplugged 85 | .yarn/build-state.yml 86 | .yarn/install-state.gz 87 | .pnp.* 88 | -------------------------------------------------------------------------------- /website/docs/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Overview 6 | 7 | # What is SLV? 8 | 9 | **SLV (Secure Local Vault)** is a free and open-source software designed for securely storing, sharing, and accessing secrets directly within your codebase. It ensures secrets remain encrypted at rest and in transit, allowing access only within explicitly authorized environments, thus simplifying secure collaboration and enhancing overall security. 10 | 11 | --- 12 | 13 | ## What problem does SLV solve? 14 | 15 | Secrets such as API keys and tokens are often scattered across Git repositories, CI/CD systems, and cloud configurations, increasing the risk of leaks. SLV addresses this challenge by providing a simple, secure, and decentralized solution for managing secrets without relying on a centralized vault. This approach enables seamless cross-collaboration across hybrid cloud environments and even supports air-gapped systems. 16 | 17 | --- 18 | 19 | ## Core Principles of SLV 20 | 21 | - **Secrets can be added or updated by anyone**, but only authorized environments are permitted to decrypt them. 22 | - **Each environment is treated as a distinct entity**, with the ability to access secrets distributed across multiple vaults. 23 | 24 | This approach ensures that SLV remains secure, auditable, and easy to use. 25 | 26 | --- 27 | 28 | ## Next Steps 29 | 30 | - **[Quick Start Guide](/docs/quick-start)** - Get up and running with SLV in minutes 31 | - **[Installation](/docs/installation)** - Install SLV on your system 32 | - **[Components](/docs/components/vault)** - Learn about vaults, profiles, and environments 33 | - **[Command Reference](/docs/command-reference/vault/get)** - Explore all available commands 34 | - **[Contributing](/docs/contributing)** - Contribute to SLV development 35 | -------------------------------------------------------------------------------- /internal/k8s/api/v1/slv_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | "slv.sh/slv/internal/core/vaults" 22 | ) 23 | 24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 | 27 | // SLVStatus defines the state of SLV vault 28 | type SLVStatus struct { 29 | Error string `json:"error,omitempty"` 30 | } 31 | 32 | //+kubebuilder:object:root=true 33 | //+kubebuilder:subresource:status 34 | 35 | // SLV is the Schema for the SLV Vault 36 | type SLV struct { 37 | metav1.TypeMeta `json:",inline"` 38 | metav1.ObjectMeta `json:"metadata,omitempty"` 39 | 40 | vaults.Vault `json:",inline"` 41 | Type string `json:"type,omitempty"` 42 | Status SLVStatus `json:"status,omitempty"` 43 | } 44 | 45 | //+kubebuilder:object:root=true 46 | 47 | // SLVList contains a list of SLV Vaults 48 | type SLVList struct { 49 | metav1.TypeMeta `json:",inline"` 50 | metav1.ListMeta `json:"metadata,omitempty"` 51 | Items []SLV `json:"items"` 52 | } 53 | 54 | func init() { 55 | SchemeBuilder.Register(&SLV{}, &SLVList{}) 56 | } 57 | -------------------------------------------------------------------------------- /internal/tui/pages/vault_browse/sections.go: -------------------------------------------------------------------------------- 1 | package vault_browse 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rivo/tview" 7 | "slv.sh/slv/internal/tui/theme" 8 | ) 9 | 10 | func (vbp *VaultBrowsePage) createMainSection() *tview.Grid { 11 | colors := theme.GetCurrentPalette() 12 | 13 | // Create the directory list (left column) 14 | vbp.directoryList = tview.NewList() 15 | vbp.directoryList.SetTitle(fmt.Sprintf("Directories (%s)", vbp.currentDir)).SetTitleAlign(tview.AlignLeft) 16 | vbp.directoryList.SetBorder(true) 17 | 18 | // Style the directory list 19 | vbp.directoryList.SetSelectedTextColor(colors.ListSelectedText). 20 | SetSelectedBackgroundColor(colors.ListSelectedBg). 21 | SetSecondaryTextColor(colors.ListSecondaryText). 22 | SetMainTextColor(colors.ListMainText). 23 | SetWrapAround(false) // Disable looping behavior 24 | 25 | // Create the file list (right column) 26 | vbp.fileList = tview.NewList() 27 | vbp.fileList.SetTitle("Vault Files").SetTitleAlign(tview.AlignLeft) 28 | vbp.fileList.SetBorder(true) 29 | 30 | // Style the file list 31 | vbp.fileList.SetSelectedTextColor(colors.ListSelectedText). 32 | SetSelectedBackgroundColor(colors.ListSelectedBg). 33 | SetSecondaryTextColor(colors.ListSecondaryText). 34 | SetMainTextColor(colors.ListMainText). 35 | SetWrapAround(false) // Disable looping behavior 36 | 37 | // Create a two-column layout using grid 38 | mainContent := tview.NewGrid(). 39 | SetRows(0). // Single row taking full height 40 | SetColumns(0, 0). // Two equal columns 41 | SetBorders(false) 42 | 43 | // Add the directory list (left column) 44 | mainContent.AddItem(vbp.directoryList, 0, 0, 1, 1, 0, 0, true) 45 | 46 | // Add the file list (right column) 47 | mainContent.AddItem(vbp.fileList, 0, 1, 1, 1, 0, 0, false) 48 | 49 | return mainContent 50 | } 51 | -------------------------------------------------------------------------------- /internal/tui/navigation/state.go: -------------------------------------------------------------------------------- 1 | package navigation 2 | 3 | // SavePageState saves a state value for a specific page and key 4 | func (n *Navigation) SavePageState(pageName, stateKey string, stateValue interface{}) { 5 | if n.pageStates[pageName] == nil { 6 | n.pageStates[pageName] = make(map[string]interface{}) 7 | } 8 | n.pageStates[pageName][stateKey] = stateValue 9 | } 10 | 11 | // GetPageState retrieves a state value for a specific page and key 12 | func (n *Navigation) GetPageState(pageName, stateKey string) (interface{}, bool) { 13 | if pageState, exists := n.pageStates[pageName]; exists { 14 | value, exists := pageState[stateKey] 15 | return value, exists 16 | } 17 | return nil, false 18 | } 19 | 20 | // ClearPageState clears all state for a specific page 21 | func (n *Navigation) ClearPageState(pageName string) { 22 | delete(n.pageStates, pageName) 23 | } 24 | 25 | // ClearPageStateKey clears a specific state key for a page 26 | func (n *Navigation) ClearPageStateKey(pageName, stateKey string) { 27 | if pageState, exists := n.pageStates[pageName]; exists { 28 | delete(pageState, stateKey) 29 | } 30 | } 31 | 32 | // HasPageState checks if a page has any saved state 33 | func (n *Navigation) HasPageState(pageName string) bool { 34 | pageState, exists := n.pageStates[pageName] 35 | return exists && len(pageState) > 0 36 | } 37 | 38 | // saveCurrentPageState saves the state of the currently active page 39 | func (n *Navigation) saveCurrentPageState() { 40 | currentPage := n.GetCurrentPage() 41 | if page, exists := n.pageInstances[currentPage]; exists && page != nil { 42 | // Check if the page has a SaveNavigationState method 43 | // We'll use type assertion to check for pages that support state saving 44 | switch p := page.(type) { 45 | case interface{ SaveNavigationState() }: 46 | p.SaveNavigationState() 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/tui/interfaces/components.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | ) 6 | 7 | // Component defines the interface for all UI components 8 | type Component interface { 9 | Render() tview.Primitive 10 | Refresh() 11 | } 12 | 13 | // InfoBarComponent defines the interface for the info bar 14 | type InfoBarComponent interface { 15 | Component 16 | UpdateProfile(profileName string) 17 | UpdateEnvironment(envName, envEmail, envType, publicKey string) 18 | ShowNoEnvironment() 19 | } 20 | 21 | // StatusBarComponent defines the interface for the status bar 22 | type StatusBarComponent interface { 23 | Component 24 | UpdateStatus(pageName string) 25 | SetCustomHelp(helpText string) 26 | ClearCustomHelp() 27 | SetPageName(pageName string) 28 | } 29 | 30 | // MainContentComponent defines the interface for the main content area 31 | type MainContentComponent interface { 32 | Component 33 | SetContent(content tview.Primitive) 34 | GetContent() tview.Primitive 35 | } 36 | 37 | // ComponentManagerInterface defines the interface for component management 38 | type ComponentManagerInterface interface { 39 | InitializeComponents() 40 | GetInfoBar() tview.Primitive 41 | GetStatusBar() tview.Primitive 42 | GetMainContent() tview.Primitive 43 | GetMainContentPages() *tview.Pages 44 | UpdateStatus(page string) 45 | UpdateStatusBar(text string) 46 | ClearStatusBar() 47 | RefreshInfoBar() 48 | } 49 | 50 | // // RouterInterface defines the interface for router functionality 51 | // type RouterInterface interface { 52 | // AddPage(name string, page tview.Primitive, resize, visible bool) 53 | // RemovePage(name string) 54 | // SwitchToPage(name string) 55 | // PushPage(page string) 56 | // PopPage() string 57 | // ClearStack() 58 | // GetCurrentPage() string 59 | // SetCurrentPage(page string) 60 | // GetPageStack() []string 61 | // } 62 | -------------------------------------------------------------------------------- /website/docs/command-reference/vault/ref.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 7 3 | --- 4 | 5 | # Referencing 6 | This is particularly useful when you have files with secrets. The command would replace the actual secrets with references to SLV secrets and add the SLV secrets to an SLV vault. 7 | #### General Usage: 8 | ```bash 9 | slv vault --vault ref [flags] 10 | ``` 11 | #### Flags: 12 | | Flag | Arguments | Required | Default | Description | 13 | | -- | -- | -- | -- | -- | 14 | | --file | String | True | NA | The reference YAML/JSON/BLOB file (Needs to be flat) | 15 | | --name | String | False | None | Name of the item (key) to reference. References all keys if not provided | 16 | | --force | None | NA | NA | Overwrite the item if it already exists | 17 | | --preview | None | NA | NA | Dry Run - Show what the referenced file would look like after referencing | 18 | | --vault | String | True | NA | Path to the SLV Vault file | 19 | | --help | None | NA | NA | Help text for `slv vault ref` | 20 | 21 | #### Usage: 22 | ```bash 23 | slv vault --vault ref --file 24 | ``` 25 | #### Example: 26 | ```bash 27 | $ cat secrets.yaml 28 | username: johndoe 29 | password: super_secret_password 30 | 31 | $ slv vault --vault test.slv.yaml ref --file secrets.yaml 32 | Auto referenced secrets.yaml (YAML) with vault test.slv.yaml 33 | 34 | $ cat secrets.yaml 35 | password: '{{SLV.test.password}}' 36 | username: '{{SLV.test.username}}' 37 | ``` 38 | 39 | --- 40 | 41 | ## See Also 42 | 43 | - [Dereferencing](/docs/command-reference/vault/deref) - Replace references with actual secrets 44 | - [Put a Secret](/docs/command-reference/vault/put) - Add secrets to your vault 45 | - [Get a Secret](/docs/command-reference/vault/get) - Retrieve secrets from your vault 46 | - [Vault Component](/docs/components/vault) - Learn more about vaults 47 | -------------------------------------------------------------------------------- /slv.go: -------------------------------------------------------------------------------- 1 | package slv 2 | 3 | import ( 4 | "slv.sh/slv/internal/core/crypto" 5 | "slv.sh/slv/internal/core/session" 6 | "slv.sh/slv/internal/core/vaults" 7 | ) 8 | 9 | func unlockVault(vaultFileOrURL string) (*vaults.Vault, error) { 10 | secretKey, err := session.GetSecretKey() 11 | if err != nil { 12 | return nil, err 13 | } 14 | vault, err := vaults.Get(vaultFileOrURL) 15 | if err != nil { 16 | return nil, err 17 | } 18 | if err = vault.Unlock(secretKey); err != nil { 19 | return nil, err 20 | } 21 | return vault, nil 22 | } 23 | 24 | // GetAllVaultItems returns all secrets from the vault 25 | func GetAllVaultItems(vaultFileOrURL string) (map[string]*vaults.VaultItem, error) { 26 | vault, err := unlockVault(vaultFileOrURL) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return vault.GetAllItems() 31 | } 32 | 33 | // GetVaultItem returns a named secret from the vault 34 | func GetVaultItem(vaultFileOrURL, name string) (vaultItem *vaults.VaultItem, err error) { 35 | if vault, err := unlockVault(vaultFileOrURL); err != nil { 36 | return nil, err 37 | } else { 38 | return vault.Get(name) 39 | } 40 | } 41 | 42 | // PutVaultItem writes a secret to the vault 43 | func PutVaultItem(vaultFile, secretName string, secretValue []byte, encrypt bool) error { 44 | vault, err := vaults.Get(vaultFile) 45 | if err != nil { 46 | return err 47 | } 48 | return vault.Put(secretName, secretValue, encrypt) 49 | } 50 | 51 | func NewVault(vaultFile, name, k8sNamespace string, enableHash, pq bool, pkStrList []string) (*vaults.Vault, error) { 52 | var pubKeys []*crypto.PublicKey 53 | for _, pkStr := range pkStrList { 54 | pubKey, err := crypto.PublicKeyFromString(pkStr) 55 | if err != nil { 56 | return nil, err 57 | } 58 | pubKeys = append(pubKeys, pubKey) 59 | } 60 | return vaults.New(vaultFile, name, k8sNamespace, enableHash, pq, pubKeys...) 61 | } 62 | -------------------------------------------------------------------------------- /internal/tui/interfaces/navigation.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "github.com/rivo/tview" 6 | "slv.sh/slv/internal/core/vaults" 7 | ) 8 | 9 | // NavigationInterface defines the interface for navigation functionality 10 | type NavigationInterface interface { 11 | // Page navigation 12 | ShowMainMenu(replace bool) 13 | ShowVaults(replace bool) 14 | ShowProfiles(replace bool) 15 | ShowEnvironments(replace bool) 16 | ShowNewEnvironment(replace bool) 17 | ShowHelp(replace bool) 18 | ShowVaultDetails(replace bool) 19 | ShowNewVault(replace bool) 20 | 21 | // Parameterized page navigation 22 | ShowVaultsWithDir(dir string, replace bool) 23 | ShowVaultDetailsWithVault(vault *vaults.Vault, filePath string, replace bool) 24 | ShowVaultEditWithVault(vault *vaults.Vault, filePath string, replace bool) 25 | ShowNewVaultWithDir(dir string, replace bool) 26 | 27 | // Routing and stack management 28 | GoBack() 29 | NavigateTo(pageName string, replace bool) 30 | GetCurrentPage() string 31 | GetPageStack() []string 32 | ClearStack() 33 | 34 | // Input handling 35 | HandleEscape() *tcell.EventKey 36 | 37 | // Status and help 38 | UpdateStatus() 39 | GetStatusBar() tview.Primitive 40 | SetCustomHelp(helpText string) 41 | ClearCustomHelp() 42 | 43 | // Vault directory management 44 | GetVaultDir() string 45 | SetVaultDir(dir string) 46 | GetPageInstance(pageName string) (Page, bool) 47 | StorePageInstance(pageName string, page Page) 48 | RemovePageInstance(pageName string) 49 | 50 | // App access 51 | GetApp() TUIInterface 52 | 53 | // Page state management 54 | SavePageState(pageName, stateKey string, stateValue interface{}) 55 | GetPageState(pageName, stateKey string) (interface{}, bool) 56 | ClearPageState(pageName string) 57 | ClearPageStateKey(pageName, stateKey string) 58 | HasPageState(pageName string) bool 59 | } 60 | -------------------------------------------------------------------------------- /internal/core/input/inputs.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "syscall" 8 | 9 | "golang.org/x/term" 10 | ) 11 | 12 | func GetHiddenInput(prompt string) ([]byte, error) { 13 | fmt.Print(prompt) 14 | input, err := term.ReadPassword(int(syscall.Stdin)) 15 | fmt.Println() 16 | return input, err 17 | } 18 | 19 | func GetMultiLineHiddenInput(prompt string) (input []byte, err error) { 20 | if prompt != "" { 21 | fmt.Println(prompt) 22 | } 23 | fmt.Println("Press enter/return twice after finishing your input to submit.") 24 | var line []byte 25 | emptyLines := 0 26 | for { 27 | if line, err = term.ReadPassword(int(os.Stdin.Fd())); err != nil { 28 | return nil, err 29 | } 30 | if len(line) == 0 { 31 | emptyLines++ 32 | if emptyLines == 2 { 33 | break 34 | } 35 | } else { 36 | for i := 0; i < emptyLines; i++ { 37 | input = append(input, '\n') 38 | } 39 | if len(input) > 0 { 40 | input = append(input, '\n') 41 | } 42 | input = append(input, line...) 43 | emptyLines = 0 44 | } 45 | } 46 | return input, err 47 | } 48 | 49 | func GetVisibleInput(prompt string) (string, error) { 50 | var input string 51 | if prompt != "" { 52 | fmt.Print(prompt) 53 | } 54 | _, err := fmt.Scanln(&input) 55 | return input, err 56 | } 57 | 58 | func ReadBufferFromStdin(prompt string) ([]byte, error) { 59 | var input []byte 60 | buffer := make([]byte, 1024) 61 | if prompt != "" { 62 | fmt.Println(prompt) 63 | } 64 | for { 65 | n, err := os.Stdin.Read(buffer) 66 | if err != nil || n == 0 { 67 | break 68 | } 69 | input = append(input, buffer[:n]...) 70 | } 71 | return input, nil 72 | } 73 | 74 | func GetConfirmation(prompt, allowFor string) (bool, error) { 75 | fmt.Print(prompt) 76 | var input string 77 | _, err := fmt.Scanln(&input) 78 | return strings.EqualFold(input, allowFor), err 79 | } 80 | 81 | func IsInteractive() bool { 82 | return term.IsTerminal(int(syscall.Stdin)) 83 | } 84 | -------------------------------------------------------------------------------- /internal/tui/utils/input.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "github.com/rivo/tview" 6 | "golang.design/x/clipboard" 7 | ) 8 | 9 | // AttachPasteHandler attaches a Ctrl+V paste handler to a tview component (InputField or TextArea) 10 | // bypassing tcell's event queue to avoid truncation issues 11 | func AttachPasteHandler(component interface{}) { 12 | if component == nil { 13 | return 14 | } 15 | 16 | switch c := component.(type) { 17 | case *tview.InputField: 18 | existingCapture := c.GetInputCapture() 19 | c.SetInputCapture(createPasteHandler( 20 | func() string { return c.GetText() }, 21 | func(text string) { c.SetText(text) }, 22 | existingCapture, 23 | )) 24 | case *tview.TextArea: 25 | existingCapture := c.GetInputCapture() 26 | c.SetInputCapture(createPasteHandler( 27 | func() string { return c.GetText() }, 28 | func(text string) { c.SetText(text, false) }, 29 | existingCapture, 30 | )) 31 | } 32 | } 33 | 34 | // createPasteHandler creates a reusable input capture function for paste handling 35 | func createPasteHandler(getText func() string, setText func(string), existingCapture func(*tcell.EventKey) *tcell.EventKey) func(*tcell.EventKey) *tcell.EventKey { 36 | return func(event *tcell.EventKey) *tcell.EventKey { 37 | // Handle Ctrl+V (or Cmd+V on Mac) for manual paste 38 | if event.Key() == tcell.KeyCtrlV || (event.Key() == tcell.KeyRune && event.Rune() == 'v' && event.Modifiers()&tcell.ModCtrl != 0) { 39 | // Read from clipboard 40 | clipboardBytes := clipboard.Read(clipboard.FmtText) 41 | if len(clipboardBytes) > 0 { 42 | clipboardText := string(clipboardBytes) 43 | // Get current text 44 | currentText := getText() 45 | // Append clipboard text 46 | setText(currentText + clipboardText) 47 | } 48 | return nil 49 | } 50 | 51 | // Chain to existing handler if present 52 | if existingCapture != nil { 53 | return existingCapture(event) 54 | } 55 | return event 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /internal/tui/pages/vault_view/forms.go: -------------------------------------------------------------------------------- 1 | package vault_view 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | "slv.sh/slv/internal/tui/utils" 6 | ) 7 | 8 | func (vvp *VaultViewPage) createAddItemForm() *tview.Form { 9 | form := tview.NewForm(). 10 | AddInputField("Name", "", 40, nil, nil) 11 | 12 | // Add manual paste handler to Name field 13 | nameField := form.GetFormItem(0).(*tview.InputField) 14 | utils.AttachPasteHandler(nameField) 15 | 16 | // Add TextArea for Value field to support unlimited text length 17 | valueTextArea := tview.NewTextArea() 18 | valueTextArea.SetLabel("Value: "). 19 | SetLabelWidth(10). 20 | SetText("", false). 21 | SetBorder(false) 22 | 23 | // Add manual paste handler to bypass tcell paste event limitations 24 | utils.AttachPasteHandler(valueTextArea) 25 | 26 | form.AddFormItem(valueTextArea) 27 | 28 | form.AddCheckbox("Encrypted", true, nil) 29 | 30 | form.GetFormItem(2).(*tview.Checkbox).SetCheckedString("✓") 31 | 32 | // Don't set border/title here - will be handled by ShowModalForm 33 | return form 34 | } 35 | 36 | func (vvp *VaultViewPage) createEditItemForm(itemKey string, itemValue string, isEncrypted bool) *tview.Form { 37 | form := tview.NewForm(). 38 | AddInputField("Name", itemKey, 40, nil, nil) 39 | 40 | // Disable the name field to prevent changes 41 | form.GetFormItem(0).(*tview.InputField).SetDisabled(true) 42 | 43 | // Add TextArea for Value field to support unlimited text length 44 | valueTextArea := tview.NewTextArea() 45 | valueTextArea.SetLabel("Value: "). 46 | SetLabelWidth(10). 47 | SetText(itemValue, false). 48 | SetBorder(false) 49 | 50 | // Add manual paste handler to bypass tcell paste event limitations 51 | utils.AttachPasteHandler(valueTextArea) 52 | 53 | form.AddFormItem(valueTextArea) 54 | 55 | form.AddCheckbox("Encrypted", isEncrypted, nil) 56 | 57 | form.GetFormItem(2).(*tview.Checkbox).SetCheckedString("✓") 58 | 59 | // Don't set border/title here - will be handled by ShowModalForm 60 | return form 61 | } 62 | -------------------------------------------------------------------------------- /internal/sharedlib/lib.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "C" 5 | 6 | "slv.sh/slv" 7 | ) 8 | import ( 9 | "encoding/json" 10 | "unsafe" 11 | 12 | "slv.sh/slv/internal/core/vaults" 13 | ) 14 | 15 | func getSecret(vaultPath, secretName *C.char, secretValue **C.char, secretLength *C.int, errMessage **C.char, errLength *C.int) { 16 | vaultFile := C.GoString(vaultPath) 17 | name := C.GoString(secretName) 18 | var err error 19 | var vaultItem *vaults.VaultItem 20 | if vaultItem, err = slv.GetVaultItem(vaultFile, name); err == nil { 21 | var valueBytes []byte 22 | if valueBytes, err = vaultItem.Value(); err == nil { 23 | *secretValue = (*C.char)(C.CBytes(valueBytes)) 24 | *secretLength = C.int(len(valueBytes)) 25 | *errMessage = nil 26 | *errLength = 0 27 | return 28 | } 29 | } 30 | *secretValue = nil 31 | *secretLength = 0 32 | *errMessage = C.CString(err.Error()) 33 | *errLength = C.int(len(err.Error())) 34 | } 35 | 36 | func getAllSecrets(vaultPath *C.char, secretsJson **C.char, secretsJsonLength *C.int, errMessage **C.char, errLength *C.int) { 37 | vaultFile := C.GoString(vaultPath) 38 | secrets, err := slv.GetAllVaultItems(vaultFile) 39 | if err == nil { 40 | var jsonBytes []byte 41 | if jsonBytes, err = json.Marshal(secrets); err == nil { 42 | *secretsJson = (*C.char)(C.CBytes(jsonBytes)) 43 | *secretsJsonLength = C.int(len(jsonBytes)) 44 | *errMessage = nil 45 | *errLength = 0 46 | return 47 | } 48 | } 49 | *secretsJson = nil 50 | *secretsJsonLength = 0 51 | *errMessage = C.CString(err.Error()) 52 | *errLength = C.int(len(err.Error())) 53 | } 54 | 55 | func putSecret(vaultPath, secretName, secretValue *C.char, errMessage **C.char, errLength *C.int) { 56 | vaultFile := C.GoString(vaultPath) 57 | name := C.GoString(secretName) 58 | value := C.GoBytes(unsafe.Pointer(secretValue), C.int(len(C.GoString(secretValue)))) 59 | if err := slv.PutVaultItem(vaultFile, name, value, true); err != nil { 60 | *errMessage = C.CString(err.Error()) 61 | *errLength = C.int(len(err.Error())) 62 | } else { 63 | *errMessage = nil 64 | *errLength = 0 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /website/static/img/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ic_fluent_code_24_regular 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /internal/tui/pages/help/help.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "github.com/rivo/tview" 6 | "slv.sh/slv/internal/tui/interfaces" 7 | "slv.sh/slv/internal/tui/pages" 8 | "slv.sh/slv/internal/tui/theme" 9 | ) 10 | 11 | // HelpPage handles the help page functionality 12 | type HelpPage struct { 13 | pages.BasePage 14 | } 15 | 16 | // NewHelpPage creates a new HelpPage instance 17 | func NewHelpPage(tui interfaces.TUIInterface) *HelpPage { 18 | return &HelpPage{ 19 | BasePage: *pages.NewBasePage(tui, "Help"), 20 | } 21 | } 22 | 23 | // Create implements the Page interface 24 | func (hp *HelpPage) Create() tview.Primitive { 25 | // Create help content 26 | helpText := `SLV TUI Help 27 | 28 | [yellow]Navigation:[white] 29 | - Arrow keys: Navigate 30 | - Enter: Select 31 | - Esc: Back 32 | - Ctrl+C: Quit 33 | 34 | [yellow]Shortcuts:[white] 35 | - v: Vaults 36 | - p: Profiles 37 | - e: Environments 38 | - h: Help 39 | 40 | [yellow]Features:[white] 41 | - Secure vault management 42 | - Profile-based configuration 43 | - Environment-specific settings 44 | - Terminal-based interface 45 | 46 | [yellow]For more information:[white] 47 | Visit the documentation or use the CLI commands.` 48 | 49 | text := tview.NewTextView(). 50 | SetText(helpText). 51 | SetDynamicColors(true). 52 | SetTextAlign(tview.AlignLeft) 53 | 54 | // Style the text 55 | colors := theme.GetCurrentPalette() 56 | text.SetTextColor(colors.TextPrimary) 57 | 58 | // Update status bar 59 | hp.UpdateStatus("Help - Press ESC to go back") 60 | 61 | // Create layout using BasePage method 62 | return hp.CreateLayout(text) 63 | } 64 | 65 | // Refresh implements the Page interface 66 | func (hp *HelpPage) Refresh() { 67 | // Help page doesn't need refresh logic 68 | } 69 | 70 | // HandleInput implements the Page interface 71 | func (hp *HelpPage) HandleInput(event *tcell.EventKey) *tcell.EventKey { 72 | // Help page doesn't handle specific input yet 73 | return event 74 | } 75 | 76 | // GetTitle implements the Page interface 77 | func (hp *HelpPage) GetTitle() string { 78 | return hp.BasePage.GetTitle() 79 | } 80 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdenv/add.go: -------------------------------------------------------------------------------- 1 | package cmdenv 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | "github.com/spf13/cobra" 8 | "slv.sh/slv/internal/cli/commands/utils" 9 | "slv.sh/slv/internal/core/environments" 10 | "slv.sh/slv/internal/core/profiles" 11 | ) 12 | 13 | func envAddCommand() *cobra.Command { 14 | if envAddCmd == nil { 15 | envAddCmd = &cobra.Command{ 16 | Use: "add", 17 | Aliases: []string{"put"}, 18 | Short: "Adds/updates an environment into the active profile", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | profile, err := profiles.GetActiveProfile() 21 | if err != nil { 22 | utils.ExitOnError(err) 23 | } 24 | if !profile.IsPushSupported() { 25 | utils.ExitOnError(fmt.Errorf("profile (%s) does not support adding environments", profile.Name())) 26 | } 27 | envdef, _ := cmd.Flags().GetString(envDefFlag.Name) 28 | setAsRoot, _ := cmd.Flags().GetBool(envSetRootFlag.Name) 29 | if setAsRoot && len(envdef) > 1 { 30 | utils.ExitOnError(fmt.Errorf("cannot set more than one environment as root")) 31 | } 32 | var successMessage string 33 | var env *environments.Environment 34 | if env, err = environments.FromDefStr(envdef); err == nil && env != nil { 35 | if setAsRoot { 36 | err = profile.SetRoot(env) 37 | successMessage = fmt.Sprintf("Successfully set %s as root environment for profile %s", color.GreenString(env.Name), color.GreenString(profile.Name())) 38 | } else { 39 | err = profile.PutEnv(env) 40 | } 41 | } 42 | if err != nil { 43 | utils.ExitOnError(err) 44 | } 45 | if successMessage == "" { 46 | successMessage = fmt.Sprintf("Successfully added environment to profile (%s)", color.GreenString(profile.Name())) 47 | } 48 | fmt.Println(successMessage) 49 | utils.SafeExit() 50 | }, 51 | } 52 | envAddCmd.Flags().StringP(envDefFlag.Name, envDefFlag.Shorthand, "", envDefFlag.Usage) 53 | envAddCmd.Flags().Bool(envSetRootFlag.Name, false, envSetRootFlag.Usage) 54 | envAddCmd.MarkFlagRequired(envDefFlag.Name) 55 | } 56 | return envAddCmd 57 | } 58 | -------------------------------------------------------------------------------- /website/docs/command-reference/system/system.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Reset SLV 6 | Wipe clean of all profile configurations and delete environments in your local SLV installation. This does not touch remote profile configurations. 7 | 8 | #### General Usage: 9 | ```bash 10 | slv system reset [flags] 11 | ``` 12 | 13 | #### Flags: 14 | | Flag | Arguments | Required | Default | Description | 15 | | -- | -- | -- | -- | -- | 16 | | --yes | None | NA | NA | Confirm action without prompt | 17 | | --help | None | NA | NA | Help text for `slv system reset` | 18 | 19 | #### Usage: 20 | ```bash 21 | slv system reset 22 | ``` 23 | 24 | #### Example: 25 | ```bash 26 | $ slv system reset 27 | You have a configured environment which you might have to consider backing up: 28 | Public Key: SLV_EPK_AEAUKAAAABUEMSPQ4BJIIWMSAKFUUXUV4THOP3ERH25CY4HR54W25HUJQR6XK 29 | Name: alice 30 | Email: alice@example.com 31 | Tags: [example_env] 32 | Secret Binding: SLV_ESB_AF4JYBGA2FXKUMAYADQHP6LPPMJM6JQDILREKS3KIHUQJWRZZKO5FQKCM4FHIRGR7DXPWHRQIACMHSNZVOOTJ7EDBGRAOODHEABHYIYY5O4Q23WD7TOZWSKIF66XVPZPLNXNTHJVNLVG3DH4N3237LZ7QMOJLGKSHHS6F7JQKOWCW3QLCYXLDYBBZFJ5ZRGVCEXXZVZYLR2ER33X3JLNJNHYICODVMPQ5VREN5GDSLSDLENJ6PUMFXKHZ5EHGOIGIT4TEW6LOYW6XMYR452BRPZSKKXLM5ZT7KHAVL64LPKNK45R3HAPH6IXAAAP772O3VBWC 33 | ------------------------------------------------------------ 34 | Env Definition: SLV_EDS_AF4JYNGKIFF4GMAYQ7Y674R7A4HTH5MQSOUUJMWFNHLDJW2EUTCPQMQCJEOJWKYW6G5UWQMPB7H66G7W6KLFGNBUIHAD23AHMXGSBFPUIZFCSW5P23HG46F3LIXHO2ZHZW67O6574W6X5MWXVLJYXZXOTHV25YN3IWR722WT3XWA2GA6IMQQRBE4EKAUDFMQ6J757USXFDDUYV7RUPGK7DX5OSOPLZKME4YPJYLQQZ4MCHY3VCHHQZLQCRH7JWNG7KPOUAIC7D4Q2AAA77745LZ2FQ 35 | Are you sure you wish to proceed? (yes/no): yes 36 | System reset successful 37 | ``` 38 | 39 | --- 40 | 41 | ## See Also 42 | 43 | - [Interactive Mode (TUI)](/docs/interactive-mode) - Use the interactive TUI to manage SLV 44 | - [Create a New Environment](/docs/command-reference/environment/new) - Create a new environment after reset 45 | - [Quick Start Guide](/docs/quick-start) - Get started with SLV 46 | - [Overview](/docs/overview) - Learn about SLV 47 | -------------------------------------------------------------------------------- /internal/core/vaults/deref.go: -------------------------------------------------------------------------------- 1 | package vaults 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | "strings" 8 | 9 | "slv.sh/slv/internal/core/commons" 10 | ) 11 | 12 | func (vlt *Vault) getDataByReference(secretRef string) ([]byte, error) { 13 | sliced := strings.Split(secretRef, "."+vlt.Name+".") 14 | if len(sliced) != 2 { 15 | return nil, errInvalidReferenceFormat 16 | } 17 | secretName := strings.Trim(sliced[1], " }") 18 | if item, err := vlt.Get(secretName); err != nil { 19 | return nil, err 20 | } else if itemValue, err := item.Value(); err != nil { 21 | return nil, err 22 | } else { 23 | return itemValue, nil 24 | } 25 | } 26 | 27 | func (vlt *Vault) getVaultSecretRefRegex() *regexp.Regexp { 28 | if vlt.Spec.vaultSecretRefRegex == nil { 29 | vlt.Spec.vaultSecretRefRegex = regexp.MustCompile(strings.ReplaceAll(secretRefPatternBase, vaultNamePatternPlaceholder, vlt.Name)) 30 | } 31 | return vlt.Spec.vaultSecretRefRegex 32 | } 33 | 34 | func (vlt *Vault) deRefContent(content string) ([]byte, error) { 35 | vaultSecretRefRegex := vlt.getVaultSecretRefRegex() 36 | secretRefs := vaultSecretRefRegex.FindAllString(content, -1) 37 | if len(secretRefs) == 1 && len(content) == len(secretRefs[0]) { 38 | return vlt.getDataByReference(secretRefs[0]) 39 | } else { 40 | for _, secretRef := range secretRefs { 41 | decrypted, err := vlt.getDataByReference(secretRef) 42 | if err != nil { 43 | return nil, err 44 | } 45 | content = strings.Replace(content, secretRef, string(decrypted), -1) 46 | } 47 | } 48 | return []byte(content), nil 49 | } 50 | 51 | func (vlt *Vault) DeRef(file string, previewOnlyMode bool) (string, error) { 52 | if vlt.IsLocked() { 53 | return "", errVaultLocked 54 | } 55 | if !commons.FileExists(file) { 56 | return "", fmt.Errorf("file does not exist") 57 | } 58 | data, err := os.ReadFile(file) 59 | if err != nil { 60 | return "", err 61 | } 62 | derefedBytes, err := vlt.deRefContent(string(data)) 63 | if err != nil { 64 | return "", err 65 | } 66 | if previewOnlyMode { 67 | return string(derefedBytes), nil 68 | } 69 | return "", os.WriteFile(file, derefedBytes, 0644) 70 | } 71 | -------------------------------------------------------------------------------- /internal/tui/core/layout.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | ) 6 | 7 | // LayoutManager handles the layout of UI components 8 | type LayoutManager struct { 9 | infoBar tview.Primitive 10 | statusBar tview.Primitive 11 | content tview.Primitive 12 | root tview.Primitive 13 | } 14 | 15 | // NewLayoutManager creates a new LayoutManager instance 16 | func NewLayoutManager() *LayoutManager { 17 | return &LayoutManager{} 18 | } 19 | 20 | // SetInfoBar sets the info bar primitive 21 | func (lm *LayoutManager) SetInfoBar(infoBar tview.Primitive) { 22 | lm.infoBar = infoBar 23 | } 24 | 25 | // SetStatusBar sets the status bar primitive 26 | func (lm *LayoutManager) SetStatusBar(statusBar tview.Primitive) { 27 | lm.statusBar = statusBar 28 | } 29 | 30 | // SetContent sets the content primitive 31 | func (lm *LayoutManager) SetContent(content tview.Primitive) { 32 | lm.content = content 33 | } 34 | 35 | // GetInfoBar returns the info bar primitive 36 | func (lm *LayoutManager) GetInfoBar() tview.Primitive { 37 | return lm.infoBar 38 | } 39 | 40 | // GetStatusBar returns the status bar primitive 41 | func (lm *LayoutManager) GetStatusBar() tview.Primitive { 42 | return lm.statusBar 43 | } 44 | 45 | // GetContent returns the content primitive 46 | func (lm *LayoutManager) GetContent() tview.Primitive { 47 | return lm.content 48 | } 49 | 50 | // GetRoot returns the root layout primitive 51 | func (lm *LayoutManager) GetRoot() tview.Primitive { 52 | return lm.root 53 | } 54 | 55 | // BuildLayout builds the complete layout 56 | func (lm *LayoutManager) BuildLayout() tview.Primitive { 57 | flex := tview.NewFlex(). 58 | SetDirection(tview.FlexRow). 59 | AddItem(lm.infoBar, 8, 1, false). // Info bar 60 | AddItem(lm.content, 0, 1, true). // Content (pages) 61 | AddItem(lm.statusBar, 3, 1, false) // Status bar 62 | 63 | lm.root = flex 64 | return lm.root 65 | } 66 | 67 | // UpdateLayout updates the layout with new components 68 | func (lm *LayoutManager) UpdateLayout(infoBar, content, statusBar tview.Primitive) { 69 | lm.infoBar = infoBar 70 | lm.content = content 71 | lm.statusBar = statusBar 72 | lm.BuildLayout() 73 | } 74 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdvault/deref.go: -------------------------------------------------------------------------------- 1 | package cmdvault 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | "github.com/spf13/cobra" 8 | "slv.sh/slv/internal/cli/commands/utils" 9 | "slv.sh/slv/internal/core/session" 10 | "slv.sh/slv/internal/core/vaults" 11 | ) 12 | 13 | func vaultDerefCommand() *cobra.Command { 14 | if vaultDerefCmd == nil { 15 | vaultDerefCmd = &cobra.Command{ 16 | Use: "deref", 17 | Short: "Dereferences and updates values from a vault to a given file with vault references", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | envSecretKey, err := session.GetSecretKey() 20 | if err != nil { 21 | utils.ExitOnError(err) 22 | } 23 | vaultFile, err := cmd.Flags().GetString(vaultFileFlag.Name) 24 | if err != nil { 25 | utils.ExitOnError(err) 26 | } 27 | file, err := cmd.Flags().GetString(vaultRefFileFlag.Name) 28 | if err != nil { 29 | utils.ExitOnError(err) 30 | } 31 | previewOnlyMode, _ := cmd.Flags().GetBool(secretSubstitutionPreviewOnlyFlag.Name) 32 | vault, err := vaults.Get(vaultFile) 33 | if err != nil { 34 | utils.ExitOnError(err) 35 | } 36 | err = vault.Unlock(envSecretKey) 37 | if err != nil { 38 | utils.ExitOnError(err) 39 | } 40 | result, err := vault.DeRef(file, previewOnlyMode) 41 | if err != nil { 42 | utils.ExitOnError(err) 43 | } 44 | if previewOnlyMode { 45 | if len(result) > 0 && result[len(result)-1] == '\n' { 46 | fmt.Print(result) 47 | } else { 48 | fmt.Println(result) 49 | } 50 | } else { 51 | fmt.Println("Dereferenced", color.GreenString(file), "with the vault", color.GreenString(vaultFile)) 52 | } 53 | utils.SafeExit() 54 | }, 55 | } 56 | vaultDerefCmd.Flags().StringP(vaultFileFlag.Name, vaultFileFlag.Shorthand, "", vaultFileFlag.Usage) 57 | vaultDerefCmd.Flags().StringP(vaultRefFileFlag.Name, vaultRefFileFlag.Shorthand, "", vaultRefFileFlag.Usage) 58 | vaultDerefCmd.Flags().BoolP(secretSubstitutionPreviewOnlyFlag.Name, secretSubstitutionPreviewOnlyFlag.Shorthand, false, secretSubstitutionPreviewOnlyFlag.Usage) 59 | vaultDerefCmd.MarkFlagRequired(vaultRefFileFlag.Name) 60 | } 61 | return vaultDerefCmd 62 | } 63 | -------------------------------------------------------------------------------- /internal/core/profiles/const.go: -------------------------------------------------------------------------------- 1 | package profiles 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | 8 | "xipher.org/xipher" 9 | ) 10 | 11 | // Errors and constants used by profiles 12 | const ( 13 | // Profile Manager constants 14 | profilesDirName = "profiles" 15 | profileMgrConfigFileName = ".profiles.yaml" 16 | 17 | // Profile constants 18 | profileConfigFileName = ".profile.yaml" 19 | profileDataDirName = "data" 20 | defaultSyncInterval = time.Hour 21 | 22 | defaultEnvManifestFileName = "environments.yaml" 23 | defaultSettingsFileName = "settings.yaml" 24 | 25 | profileCryptoKeyName = "slv_profiles" 26 | ) 27 | 28 | var ( 29 | profileMgrMutex sync.Mutex 30 | profileMgr *profileManager = nil 31 | profileMap map[string]*Profile = make(map[string]*Profile) 32 | profileSK *xipher.SecretKey 33 | 34 | envManifestFileNames = []string{defaultEnvManifestFileName, "environments.yml"} 35 | settingsFileNames = []string{defaultSettingsFileName, "settings.yml"} 36 | 37 | errProfilePathExistsAlready = errors.New("profile path exists already") 38 | errProfilePathDoesNotExist = errors.New("profile path does not exist") 39 | errCreatingProfilesHomeDir = errors.New("error creating profiles home dir inside app data dir") 40 | errInitializingProfileManagementDir = errors.New("error initializing profile management dir") 41 | errOpeningProfileManagementDir = errors.New("error opening profile management dir") 42 | errProfileNotFound = errors.New("profile not found") 43 | errProfileExistsAlready = errors.New("profile exists already") 44 | errInvalidProfileName = errors.New("invalid profile name") 45 | errNoActiveProfileSet = errors.New("active profile not set") 46 | errSettingActiveProfile = errors.New("error setting active profile") 47 | errDeletingActiveProfile = errors.New("error deleting active profile") 48 | errRemoteTypeDoesNotExist = errors.New("remote type does not exist") 49 | errRemoteSetupNotImplemented = errors.New("remote setup not implemented") 50 | errRemotePullNotImplemented = errors.New("remote pull not implemented") 51 | errRemotePushNotSupported = errors.New("remote push not supported") 52 | ) 53 | -------------------------------------------------------------------------------- /internal/core/config/const.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/fatih/color" 7 | ) 8 | 9 | const ( 10 | envar_SLV_APP_DATA_DIR = "SLV_APP_DATA_DIR" 11 | 12 | AppNameLowerCase = "slv" 13 | AppNameUpperCase = "SLV" 14 | description = AppNameUpperCase + " (Secure Local Vault) : " + "Securely store, share, and access secrets alongside the codebase." 15 | website = "https://slv.sh" 16 | art = ` ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 17 | ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 18 | ▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 19 | ▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 20 | ▓▓▓▓▓▓▓▓▓▓▓ ░░░░░▒▒▒▒▒ 21 | ▓▓▓▓▓▓▓▓▓▓ ░░░░░▒▒▒▒ 22 | ▓▓▓▓▓▓▓▓▓▓░░░▒▒▒▒ 23 | ▓▓▓▓▓▓▓▓▓▓▒▒▒▒ 24 | ▓▓▓▓▓▓▓▓▓▓ 25 | ░░░░░▓▓▓▓▓▓▓▓▓ 26 | ░░░░░▒▒▒▓▓▓▓▓▓▓▓ 27 | ░░░░░░▒▒▒ ▓▓▓▓▓▓▓▓▓ 28 | ░░░░░░▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓ 29 | ░░░░░░▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 30 | ░░░░░░▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 31 | ░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 32 | ░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ` 33 | 34 | K8SLVGroup = "slv.sh" 35 | K8SLVVersion = "v1" 36 | K8SLVKind = AppNameUpperCase 37 | K8SLVAnnotationVersionKey = K8SLVGroup + "/version" 38 | K8SLVVaultField = "spec" 39 | ) 40 | 41 | func ColorizedArt() string { 42 | if colorizedArt == nil { 43 | colorizedArt = new(string) 44 | *colorizedArt = strings.ReplaceAll(art, "▓", color.RGB(157, 58, 79).Sprint("▓")) 45 | *colorizedArt = strings.ReplaceAll(*colorizedArt, "░", color.RGB(79, 85, 89).Sprint("░")) 46 | *colorizedArt = strings.ReplaceAll(*colorizedArt, "▒", color.RGB(79, 85, 89).Sprint("▒")) 47 | } 48 | return *colorizedArt 49 | } 50 | 51 | // Art returns the plain ASCII art without color codes 52 | func Art() string { 53 | return art 54 | } 55 | 56 | var ( 57 | Version = "v" + version 58 | version = "777.77.77" 59 | fullCommit = "" 60 | commitDate = "" 61 | releaseURL = "" 62 | appInfo *string 63 | 64 | appDataDir *string 65 | 66 | colorizedArt *string 67 | ) 68 | 69 | func GetVersion() string { 70 | return version 71 | } 72 | 73 | func GetFullCommit() string { 74 | return fullCommit 75 | } 76 | 77 | func GetCommitDate() string { 78 | return commitDate 79 | } 80 | 81 | func GetReleaseURL() string { 82 | return releaseURL 83 | } 84 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdvault/update.go: -------------------------------------------------------------------------------- 1 | package cmdvault 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/fatih/color" 9 | "github.com/spf13/cobra" 10 | "slv.sh/slv/internal/cli/commands/utils" 11 | "slv.sh/slv/internal/core/input" 12 | "slv.sh/slv/internal/core/vaults" 13 | ) 14 | 15 | func vaultUpdateCommand() *cobra.Command { 16 | if vaultUpdateCmd == nil { 17 | vaultUpdateCmd = &cobra.Command{ 18 | Use: "update", 19 | Short: "Update attributes of a vault", 20 | Run: func(cmd *cobra.Command, args []string) { 21 | vaultFilePath := cmd.Flag(vaultFileFlag.Name).Value.String() 22 | name := cmd.Flag(vaultNameFlag.Name).Value.String() 23 | namespace := cmd.Flag(vaultK8sNamespaceFlag.Name).Value.String() 24 | vault, err := vaults.Get(vaultFilePath) 25 | if err != nil { 26 | utils.ExitOnError(err) 27 | } 28 | k8sSecret := cmd.Flag(vaultK8sSecretFlag.Name).Value.String() 29 | var data []byte 30 | if k8sSecret != "" { 31 | if strings.HasSuffix(k8sSecret, ".yaml") || strings.HasSuffix(k8sSecret, ".yml") || 32 | strings.HasSuffix(k8sSecret, ".json") { 33 | data, err = os.ReadFile(k8sSecret) 34 | } else if k8sSecret == "-" { 35 | data, err = input.ReadBufferFromStdin("Input the k8s secret object as yaml/json: ") 36 | } else { 37 | utils.ExitOnErrorWithMessage("invalid k8s secret resource file") 38 | } 39 | if err != nil { 40 | utils.ExitOnError(err) 41 | } 42 | } 43 | secretType := cmd.Flag(vaultK8sSecretTypeFlag.Name).Value.String() 44 | if err = vault.Update(name, namespace, secretType, data); err != nil { 45 | utils.ExitOnError(err) 46 | } 47 | fmt.Printf("Vault %s transformed to K8s resource %s\n", color.GreenString(vaultFilePath), color.GreenString(name)) 48 | }, 49 | } 50 | vaultUpdateCmd.Flags().StringP(vaultNameFlag.Name, vaultNameFlag.Shorthand, "", vaultNameFlag.Usage) 51 | vaultUpdateCmd.Flags().StringP(vaultK8sNamespaceFlag.Name, vaultK8sNamespaceFlag.Shorthand, "", vaultK8sNamespaceFlag.Usage) 52 | vaultUpdateCmd.Flags().StringP(vaultK8sSecretFlag.Name, vaultK8sSecretFlag.Shorthand, "", vaultK8sSecretFlag.Usage) 53 | vaultUpdateCmd.Flags().StringP(vaultK8sSecretTypeFlag.Name, vaultK8sSecretTypeFlag.Shorthand, "", vaultK8sSecretTypeFlag.Usage) 54 | } 55 | return vaultUpdateCmd 56 | } 57 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdenv/delete.go: -------------------------------------------------------------------------------- 1 | package cmdenv 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "slv.sh/slv/internal/cli/commands/utils" 8 | "slv.sh/slv/internal/core/input" 9 | "slv.sh/slv/internal/core/profiles" 10 | ) 11 | 12 | func envDeleteCommand() *cobra.Command { 13 | if envDeleteCmd == nil { 14 | envDeleteCmd = &cobra.Command{ 15 | Use: "rm", 16 | Aliases: []string{"remove", "delete", "del"}, 17 | Short: "Removes an environment from active profile", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | profile, err := profiles.GetActiveProfile() 20 | if err != nil { 21 | utils.ExitOnError(err) 22 | } 23 | if !profile.IsPushSupported() { 24 | utils.ExitOnError(fmt.Errorf("profile (%s) does not support deleting environments", profile.Name())) 25 | } 26 | queries, err := cmd.Flags().GetStringSlice(EnvSearchFlag.Name) 27 | if err != nil { 28 | utils.ExitOnError(err) 29 | } 30 | envs, err := profile.SearchEnvs(queries) 31 | if err != nil { 32 | utils.ExitOnError(err) 33 | } 34 | if envs != nil { 35 | for _, env := range envs { 36 | ShowEnv(*env, false, false) 37 | fmt.Println() 38 | } 39 | confirm, err := input.GetConfirmation("Are you sure you wish to delete the above environment(s) [yes/no]: ", "yes") 40 | if err != nil { 41 | utils.ExitOnError(err) 42 | } 43 | if confirm { 44 | for _, env := range envs { 45 | if profile.DeleteEnv(env.PublicKey); err != nil { 46 | utils.ExitOnError(err) 47 | } 48 | fmt.Printf("Environment %s deleted successfully\n", env.Name) 49 | } 50 | } 51 | } 52 | utils.SafeExit() 53 | }, 54 | } 55 | envDeleteCmd.Flags().StringSliceP(EnvSearchFlag.Name, EnvSearchFlag.Shorthand, []string{}, EnvSearchFlag.Usage) 56 | if err := envDeleteCmd.RegisterFlagCompletionFunc(EnvSearchFlag.Name, EnvSearchCompletion); err != nil { 57 | utils.ExitOnError(err) 58 | } 59 | envDeleteCmd.Flags().StringSliceP(EnvPublicKeysFlag.Name, EnvPublicKeysFlag.Shorthand, []string{}, EnvPublicKeysFlag.Usage) 60 | envDeleteCmd.Flags().BoolP(EnvSelfFlag.Name, EnvSelfFlag.Shorthand, false, EnvSelfFlag.Usage) 61 | envDeleteCmd.Flags().BoolP(EnvK8sFlag.Name, EnvK8sFlag.Shorthand, false, EnvK8sFlag.Usage) 62 | } 63 | return envDeleteCmd 64 | } 65 | -------------------------------------------------------------------------------- /internal/k8s/charts/slv-job/templates/job.yaml: -------------------------------------------------------------------------------- 1 | {{- if not .Values.schedule -}} 2 | apiVersion: batch/v1 3 | kind: Job 4 | metadata: 5 | name: {{.Values.jobName | default "slv-job"}} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | app: slv-job 9 | {{- with .Values.labels }} 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | ttlSecondsAfterFinished: {{ .Values.ttlSecondsAfterFinished | default 3600 }} 14 | template: 15 | metadata: 16 | name: slv-job 17 | namespace: {{ .Release.Namespace }} 18 | labels: 19 | app: slv-job 20 | {{- with .Values.podLabels }} 21 | {{- toYaml . | nindent 8 }} 22 | {{- end }} 23 | {{- with .Values.podAnnotations }} 24 | annotations: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | spec: 28 | {{- with .Values.nodeSelector }} 29 | nodeSelector: 30 | {{- toYaml . | nindent 8 }} 31 | {{- end }} 32 | {{- with .Values.affinity }} 33 | affinity: 34 | {{- toYaml . | nindent 8 }} 35 | {{- end }} 36 | {{- with .Values.tolerations }} 37 | tolerations: 38 | {{- toYaml . | nindent 8 }} 39 | {{- end }} 40 | containers: 41 | - name: slv 42 | {{- if and (.Values.image) (not (hasSuffix .Chart.Version .Values.image)) -}} 43 | {{- fail (printf "The image tag must be set to the Chart.Version '%s'" .Chart.Version) -}} 44 | {{- end }} 45 | image: {{ .Values.image | default (printf "ghcr.io/amagioss/slv:%s" .Chart.AppVersion) }} 46 | resources: 47 | {{- toYaml .Values.resource | nindent 10 }} 48 | env: 49 | {{- with .Values.env }} 50 | {{- toYaml . | nindent 10 }} 51 | {{- end }} 52 | {{- if ne .Values.k8sSecret ""}} 53 | - name: SLV_K8S_ENV_SECRET 54 | value: {{ .Values.k8sSecret }} 55 | {{- end }} 56 | {{- if ne .Values.secretBinding "" }} 57 | - name: SLV_ENV_SECRET_BINDING 58 | value: {{ .Values.secretBinding }} 59 | {{- end }} 60 | - name: SLV_MODE 61 | value: "k8s_job" 62 | restartPolicy: Never 63 | serviceAccountName: slv-serviceaccount 64 | backoffLimit: {{ .Values.backoffLimit | default 4 }} 65 | {{- end -}} 66 | -------------------------------------------------------------------------------- /website/docs/command-reference/vault/access.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Manage Vault Access 6 | Add or Remove access to the vault. 7 | 8 | > **Before you begin:** You need to have a vault created and an environment with access to that vault. The environment managing access to a vault must be able to access the vault in the first place. 9 | 10 | **Important Condition:** The environment managing access to a vault must be able to access the vault in the first place. 11 | 12 | #### General Usage: 13 | ```bash 14 | slv vault --vault access [flags] 15 | slv vault --vault access [flags] [command] 16 | ``` 17 | #### Flags: 18 | | Flag | Arguments | Required | Default | Description | 19 | | -- | -- | -- | -- | -- | 20 | | --env-self | None | NA | NA | Modify vault access for the environment set to `self` | 21 | | --env-k8s | None | NA | NA | Modify vault access for the environment in current kubernetes context | 22 | | --env-pubkey | String(s) | False | None | Modify vault access for the environment with given Public Keys | 23 | | --env-search | String(s) | False | None | Share vault with environment based on search string | 24 | | --quantum-safe | None | NA | NA | Use Quantum Resistant Cryptography (Kyber1024) | 25 | | --vault | String | True | NA | Path to the SLV Vault file | 26 | | --help | None | NA | NA | Help text for `slv vault access` | 27 | 28 | --- 29 | ## Add Access to a Vault 30 | #### Usage: 31 | ```bash 32 | slv vault --vault access --env-search add 33 | ``` 34 | #### Example: 35 | ```bash 36 | $ slv vault --vault test.slv.yaml access --env-search alice add 37 | Shared vault: test.slv.yaml 38 | ``` 39 | --- 40 | ## Remove Access to a Vault 41 | #### Usage: 42 | ```bash 43 | slv vault --vault access --env-search remove 44 | ``` 45 | #### Example: 46 | ```bash 47 | $ slv vault --vault test.slv.yaml access --env-search bob@example.com remove 48 | Shared vault: test.slv.yaml 49 | ``` 50 | 51 | --- 52 | 53 | ## See Also 54 | 55 | - [Create a New Vault](/docs/command-reference/vault/new) - Create a new vault 56 | - [Get a Secret](/docs/command-reference/vault/get) - Retrieve secrets from your vault 57 | - [Environment Component](/docs/components/environment) - Learn about environments 58 | - [Vault Component](/docs/components/vault) - Learn more about vaults 59 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdenv/const.go: -------------------------------------------------------------------------------- 1 | package cmdenv 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "slv.sh/slv/internal/cli/commands/utils" 6 | ) 7 | 8 | var ( 9 | envCmd *cobra.Command 10 | envNewCmd *cobra.Command 11 | envNewServiceCmd *cobra.Command 12 | envNewDirectServicetextCmd *cobra.Command 13 | envNewUserCmd *cobra.Command 14 | envAddCmd *cobra.Command 15 | envListCmd *cobra.Command 16 | envDeleteCmd *cobra.Command 17 | envSetSelfSCmd *cobra.Command 18 | envShowCmd *cobra.Command 19 | envShowRootCmd *cobra.Command 20 | envShowSelfCmd *cobra.Command 21 | envShowK8sCmd *cobra.Command 22 | ) 23 | 24 | var ( 25 | envNameFlag = utils.FlagDef{ 26 | Name: "name", 27 | Shorthand: "n", 28 | Usage: "Environment name", 29 | } 30 | 31 | envEmailFlag = utils.FlagDef{ 32 | Name: "email", 33 | Shorthand: "e", 34 | Usage: "Environment email", 35 | } 36 | 37 | envTagsFlag = utils.FlagDef{ 38 | Name: "tags", 39 | Shorthand: "t", 40 | Usage: "Environment tags", 41 | } 42 | 43 | envAddFlag = utils.FlagDef{ 44 | Name: "add", 45 | Usage: "Adds environment to active profile", 46 | } 47 | 48 | envSetRootFlag = utils.FlagDef{ 49 | Name: "root", 50 | Usage: "Set the given environment as root", 51 | } 52 | 53 | EnvSearchFlag = utils.FlagDef{ 54 | Name: "env-search", 55 | Shorthand: "s", 56 | Usage: "Searches query to filter environments", 57 | } 58 | 59 | showEnvDefFlag = utils.FlagDef{ 60 | Name: "show-env-def", 61 | Usage: "Shows the environment definition in the output", 62 | } 63 | 64 | EnvSelfFlag = utils.FlagDef{ 65 | Name: "env-self", 66 | Usage: "References to the self environment (the local environment where the command is executed)", 67 | } 68 | 69 | envDefFlag = utils.FlagDef{ 70 | Name: "env-def", 71 | Shorthand: "e", 72 | Usage: "Environment definition that begins with SLV_EDS_", 73 | } 74 | 75 | EnvPublicKeysFlag = utils.FlagDef{ 76 | Name: "env-pubkey", 77 | Shorthand: "k", 78 | Usage: "Public keys of environments that can access the vault", 79 | } 80 | 81 | EnvK8sFlag = utils.FlagDef{ 82 | Name: "env-k8s", 83 | Usage: "Shares vault access with the accessible k8s cluster", 84 | } 85 | ) 86 | -------------------------------------------------------------------------------- /internal/tui/navigation/navigation.go: -------------------------------------------------------------------------------- 1 | package navigation 2 | 3 | import ( 4 | "os" 5 | 6 | "slv.sh/slv/internal/tui/interfaces" 7 | ) 8 | 9 | // Navigation handles page navigation and routing 10 | type Navigation struct { 11 | app interfaces.TUIInterface 12 | vaultDir string // Store current vault directory 13 | customHelp string // Store custom help text for current page 14 | pageInstances map[string]interfaces.Page // Store actual page instances for refresh 15 | 16 | // General state management for all pages 17 | pageStates map[string]map[string]interface{} // pageName -> stateKey -> stateValue 18 | } 19 | 20 | // NewNavigation creates a new navigation controller 21 | func NewNavigation(app interfaces.TUIInterface) *Navigation { 22 | // Get user's home directory 23 | currentDir, err := os.Getwd() 24 | if err != nil { 25 | currentDir = "." 26 | } 27 | 28 | nav := &Navigation{ 29 | app: app, 30 | vaultDir: currentDir, 31 | pageInstances: make(map[string]interfaces.Page), 32 | pageStates: make(map[string]map[string]interface{}), 33 | } 34 | 35 | nav.UpdateStatus() // Initialize status bar with content 36 | nav.setupInputHandling() // Setup global input handling 37 | return nav 38 | } 39 | 40 | // GetApp returns the TUI interface 41 | func (n *Navigation) GetApp() interfaces.TUIInterface { 42 | return n.app 43 | } 44 | 45 | // GetVaultDir returns the current vault directory 46 | func (n *Navigation) GetVaultDir() string { 47 | return n.vaultDir 48 | } 49 | 50 | // SetVaultDir sets the current vault directory 51 | func (n *Navigation) SetVaultDir(dir string) { 52 | n.vaultDir = dir 53 | } 54 | 55 | // GetCustomHelp returns the custom help text 56 | func (n *Navigation) GetCustomHelp() string { 57 | return n.customHelp 58 | } 59 | 60 | // StorePageInstance stores a page instance for later refresh 61 | func (n *Navigation) StorePageInstance(pageName string, page interfaces.Page) { 62 | n.pageInstances[pageName] = page 63 | } 64 | 65 | // GetPageInstance retrieves a stored page instance 66 | func (n *Navigation) GetPageInstance(pageName string) (interfaces.Page, bool) { 67 | page, exists := n.pageInstances[pageName] 68 | return page, exists 69 | } 70 | 71 | // RemovePageInstance removes a page instance from storage 72 | func (n *Navigation) RemovePageInstance(pageName string) { 73 | delete(n.pageInstances, pageName) 74 | } 75 | -------------------------------------------------------------------------------- /internal/tui/pages/environments_new/core.go: -------------------------------------------------------------------------------- 1 | package environments_new 2 | 3 | import ( 4 | "fmt" 5 | 6 | "slv.sh/slv/internal/core/crypto" 7 | "slv.sh/slv/internal/core/environments" 8 | "slv.sh/slv/internal/core/environments/envproviders" 9 | "slv.sh/slv/internal/core/profiles" 10 | ) 11 | 12 | // createEnvironment creates the environment based on the collected information 13 | func (nep *NewEnvironmentPage) createEnvironment() { 14 | var env *environments.Environment 15 | var secretKey *crypto.SecretKey 16 | var err error 17 | 18 | // Handle direct service creation 19 | if nep.selectedProvider == "direct" { 20 | env, secretKey, err = environments.New(nep.envName, nep.selectedType, nep.quantumSafe) 21 | if err != nil { 22 | nep.ShowError(fmt.Sprintf("Failed to create environment: %v", err)) 23 | return 24 | } 25 | 26 | // Store secret key string if available 27 | if secretKey != nil { 28 | nep.secretKey = secretKey.String() 29 | } 30 | } else { 31 | // Create environment using provider 32 | env, err = envproviders.NewEnv( 33 | nep.selectedProvider, 34 | nep.envName, 35 | nep.selectedType, 36 | nep.providerInputs, 37 | nep.quantumSafe, 38 | ) 39 | if err != nil { 40 | nep.ShowError(fmt.Sprintf("Failed to create environment: %v", err)) 41 | return 42 | } 43 | } 44 | 45 | // Set metadata 46 | if nep.envEmail != "" { 47 | env.SetEmail(nep.envEmail) 48 | } 49 | if len(nep.envTags) > 0 { 50 | env.AddTags(nep.envTags...) 51 | } 52 | 53 | // Handle self environment 54 | if nep.selectedType == environments.USER { 55 | if err = env.SetAsSelf(); err != nil { 56 | nep.ShowError(fmt.Sprintf("Failed to set as self environment: %v", err)) 57 | return 58 | } 59 | } 60 | 61 | // Add to profile if requested 62 | if nep.addToProfile { 63 | profile, err := profiles.GetActiveProfile() 64 | if err != nil { 65 | nep.ShowError(fmt.Sprintf("Failed to get active profile: %v", err)) 66 | return 67 | } 68 | 69 | if !profile.IsPushSupported() { 70 | nep.ShowError(fmt.Sprintf("Profile (%s) does not support adding environments", profile.Name())) 71 | return 72 | } 73 | 74 | if err = profile.PutEnv(env); err != nil { 75 | nep.ShowError(fmt.Sprintf("Failed to add environment to profile: %v", err)) 76 | return 77 | } 78 | } 79 | 80 | // Store created environment 81 | nep.createdEnv = env 82 | 83 | // Show result 84 | nep.showResult() 85 | } 86 | -------------------------------------------------------------------------------- /internal/tui/pages/environments_new/navigation.go: -------------------------------------------------------------------------------- 1 | package environments_new 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | ) 6 | 7 | type FormNavigation struct { 8 | nep *NewEnvironmentPage 9 | helpTexts map[int]string // Step-specific help texts 10 | } 11 | 12 | func (fn *FormNavigation) NewFormNavigation(nep *NewEnvironmentPage) *FormNavigation { 13 | return &FormNavigation{ 14 | nep: nep, 15 | helpTexts: make(map[int]string), 16 | } 17 | } 18 | 19 | func (fn *FormNavigation) SetupNavigation() { 20 | // Set up help texts for each step 21 | fn.setupHelpTexts() 22 | 23 | // Set initial help text 24 | fn.updateHelpText() 25 | } 26 | 27 | // setupHelpTexts sets up help text for each step 28 | func (fn *FormNavigation) setupHelpTexts() { 29 | fn.helpTexts[StepProviderSelection] = "Select Provider: ↑/↓: Navigate list | Enter: Select | Tab: Switch to Cancel | Esc: Cancel" 30 | fn.helpTexts[StepMetadata] = "Environment Metadata: Tab: Next field | Enter: Submit | Backspace: Back" 31 | fn.helpTexts[StepProviderConfig] = "Provider Configuration: Tab: Next field | Enter: Submit | Backspace: Back" 32 | fn.helpTexts[StepConfirmation] = "Review Environment: Tab: Navigate | Enter: Create | Backspace: Back" 33 | fn.helpTexts[StepResult] = "Environment Created: ↑/↓: Navigate table | c: Copy field | Tab: To buttons | Shift+Tab: To table" 34 | } 35 | 36 | // updateHelpText updates the status bar with help text for the current step 37 | func (fn *FormNavigation) updateHelpText() { 38 | if helpText, exists := fn.helpTexts[fn.nep.currentStep]; exists { 39 | fn.nep.GetTUI().UpdateStatusBar(helpText) 40 | } 41 | } 42 | 43 | // HandleInput handles input for the new environment page 44 | func (fn *FormNavigation) HandleInput(event *tcell.EventKey) *tcell.EventKey { 45 | switch event.Key() { 46 | case tcell.KeyEsc: 47 | // ESC to go back or cancel 48 | switch fn.nep.currentStep { 49 | case StepProviderSelection: 50 | fn.nep.GetTUI().GetNavigation().GoBack() 51 | return nil 52 | case StepMetadata: 53 | fn.nep.showProviderSelection() 54 | return nil 55 | case StepProviderConfig: 56 | fn.nep.showMetadataForm() 57 | return nil 58 | case StepConfirmation: 59 | if fn.nep.selectedProvider == "direct" { 60 | fn.nep.showMetadataForm() 61 | } else { 62 | fn.nep.showProviderConfig() 63 | } 64 | return nil 65 | } 66 | } 67 | 68 | // Let the form handle other inputs 69 | return event 70 | } 71 | -------------------------------------------------------------------------------- /internal/core/input/password.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | type PasswordPolicy struct { 11 | MinLength uint8 12 | MinUppercase uint8 13 | MinLowercase uint8 14 | MinDigits uint8 15 | MinSpecialChars uint8 16 | } 17 | 18 | func DefaultPasswordPolicy() *PasswordPolicy { 19 | if defaultPwdPolicy == nil { 20 | defaultPwdPolicyMutex.Lock() 21 | defer defaultPwdPolicyMutex.Unlock() 22 | if defaultPwdPolicy == nil { 23 | defaultPwdPolicy = &PasswordPolicy{ 24 | MinLength: pwdDefaultMinLength, 25 | MinUppercase: pwdDefaultMinUppercase, 26 | MinLowercase: pwdDefaultMinLowercase, 27 | MinDigits: pwdDefaultMinDigits, 28 | MinSpecialChars: pwdDefaultMinSpecialChars, 29 | } 30 | } 31 | } 32 | return defaultPwdPolicy 33 | } 34 | 35 | func (policy *PasswordPolicy) error() error { 36 | return fmt.Errorf("password must be at least %d characters long, contain at least %d uppercase, %d lowercase, %d digits, and %d special characters", 37 | policy.MinLength, policy.MinUppercase, policy.MinLowercase, policy.MinDigits, policy.MinSpecialChars) 38 | } 39 | 40 | func (policy *PasswordPolicy) Validate(password string) error { 41 | if len(password) < int(policy.MinLength) { 42 | return policy.error() 43 | } 44 | var ( 45 | upp, low, num, sym uint8 46 | ) 47 | for _, char := range password { 48 | switch { 49 | case unicode.IsUpper(char): 50 | upp++ 51 | case unicode.IsLower(char): 52 | low++ 53 | case unicode.IsNumber(char): 54 | num++ 55 | case strings.ContainsRune(pwdSpecialCharset, char): 56 | sym++ 57 | default: 58 | return policy.error() 59 | } 60 | } 61 | if !(upp >= policy.MinUppercase && low >= policy.MinLowercase && num >= policy.MinDigits && sym >= policy.MinSpecialChars) { 62 | return policy.error() 63 | } 64 | return nil 65 | } 66 | 67 | func NewPasswordFromUser(policy *PasswordPolicy) ([]byte, error) { 68 | password, err := GetHiddenInput("Enter a Password: ") 69 | if err != nil { 70 | return nil, err 71 | } 72 | if policy != nil { 73 | if err = policy.Validate(string(password)); err != nil { 74 | return nil, err 75 | } 76 | } 77 | if confirmPassword, err := GetHiddenInput("Confirm Password: "); err != nil { 78 | return nil, err 79 | } else if !bytes.Equal(password, confirmPassword) { 80 | return nil, fmt.Errorf("passwords do not match") 81 | } 82 | return password, nil 83 | } 84 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdenv/list.go: -------------------------------------------------------------------------------- 1 | package cmdenv 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "slv.sh/slv/internal/cli/commands/utils" 8 | "slv.sh/slv/internal/core/environments" 9 | "slv.sh/slv/internal/core/profiles" 10 | ) 11 | 12 | func envListCommand() *cobra.Command { 13 | if envListCmd == nil { 14 | envListCmd = &cobra.Command{ 15 | Use: "ls", 16 | Aliases: []string{"list", "search", "find", "get"}, 17 | Short: "List/Search environments from the active profile", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | profile, err := profiles.GetActiveProfile() 20 | if err != nil { 21 | utils.ExitOnError(err) 22 | } 23 | queries, err := cmd.Flags().GetStringSlice(EnvSearchFlag.Name) 24 | if err != nil { 25 | utils.ExitOnError(err) 26 | } 27 | showEnvDef, _ := cmd.Flags().GetBool(showEnvDefFlag.Name) 28 | var envs []*environments.Environment 29 | if len(queries) == 0 { 30 | envs, err = profile.ListEnvs() 31 | } else { 32 | envs, err = profile.SearchEnvs(queries) 33 | } 34 | if err != nil { 35 | utils.ExitOnError(err) 36 | } 37 | for i, env := range envs { 38 | ShowEnv(*env, showEnvDef, false) 39 | if i < len(envs)-1 { 40 | fmt.Println() 41 | } 42 | } 43 | utils.SafeExit() 44 | }, 45 | } 46 | envListCmd.Flags().StringSliceP(EnvSearchFlag.Name, EnvSearchFlag.Shorthand, []string{}, EnvSearchFlag.Usage) 47 | envListCmd.Flags().BoolP(showEnvDefFlag.Name, showEnvDefFlag.Shorthand, false, showEnvDefFlag.Usage) 48 | if err := envListCmd.RegisterFlagCompletionFunc(EnvSearchFlag.Name, EnvSearchCompletion); err != nil { 49 | utils.ExitOnError(err) 50 | } 51 | } 52 | return envListCmd 53 | } 54 | 55 | func EnvSearchCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 56 | profile, err := profiles.GetActiveProfile() 57 | if err != nil { 58 | return nil, cobra.ShellCompDirectiveError 59 | } 60 | var envs []*environments.Environment 61 | if toComplete == "" { 62 | envs, err = profile.ListEnvs() 63 | } else { 64 | envs, err = profile.SearchEnvs([]string{toComplete}) 65 | } 66 | if err != nil { 67 | return nil, cobra.ShellCompDirectiveError 68 | } 69 | var envNames []string 70 | for _, env := range envs { 71 | if env.Name != "" { 72 | envNames = append(envNames, env.Name) 73 | } 74 | } 75 | return envNames, cobra.ShellCompDirectiveNoFileComp 76 | } 77 | -------------------------------------------------------------------------------- /internal/cli/commands/cmdvault/new.go: -------------------------------------------------------------------------------- 1 | package cmdvault 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | "github.com/spf13/cobra" 8 | "slv.sh/slv/internal/cli/commands/cmdenv" 9 | "slv.sh/slv/internal/cli/commands/utils" 10 | "slv.sh/slv/internal/core/vaults" 11 | ) 12 | 13 | func vaultNewCommand() *cobra.Command { 14 | if vaultNewCmd == nil { 15 | vaultNewCmd = &cobra.Command{ 16 | Use: "new", 17 | Aliases: []string{"create"}, 18 | Short: "Creates a new vault", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | vaultFile := cmd.Flag(vaultFileFlag.Name).Value.String() 21 | pq, _ := cmd.Flags().GetBool(utils.QuantumSafeFlag.Name) 22 | publicKeys, err := cmdenv.GetPublicKeys(cmd, true, pq) 23 | if err != nil { 24 | utils.ExitOnError(err) 25 | } 26 | enableHash, _ := cmd.Flags().GetBool(vaultEnableHashingFlag.Name) 27 | name := cmd.Flag(vaultNameFlag.Name).Value.String() 28 | k8sNamespace := cmd.Flag(vaultK8sNamespaceFlag.Name).Value.String() 29 | if _, err = vaults.New(vaultFile, name, k8sNamespace, enableHash, pq, publicKeys...); err != nil { 30 | utils.ExitOnError(err) 31 | } 32 | fmt.Println("Created vault:", color.GreenString(vaultFile)) 33 | utils.SafeExit() 34 | }, 35 | } 36 | vaultNewCmd.Flags().StringSliceP(cmdenv.EnvPublicKeysFlag.Name, cmdenv.EnvPublicKeysFlag.Shorthand, []string{}, cmdenv.EnvPublicKeysFlag.Usage) 37 | vaultNewCmd.Flags().StringSliceP(cmdenv.EnvSearchFlag.Name, cmdenv.EnvSearchFlag.Shorthand, []string{}, cmdenv.EnvSearchFlag.Usage) 38 | if err := vaultNewCmd.RegisterFlagCompletionFunc(cmdenv.EnvSearchFlag.Name, cmdenv.EnvSearchCompletion); err != nil { 39 | utils.ExitOnError(err) 40 | } 41 | vaultNewCmd.Flags().BoolP(cmdenv.EnvSelfFlag.Name, cmdenv.EnvSelfFlag.Shorthand, false, cmdenv.EnvSelfFlag.Usage) 42 | vaultNewCmd.Flags().BoolP(cmdenv.EnvK8sFlag.Name, cmdenv.EnvK8sFlag.Shorthand, false, cmdenv.EnvK8sFlag.Usage) 43 | vaultNewCmd.Flags().StringP(vaultNameFlag.Name, vaultNameFlag.Shorthand, "", vaultNameFlag.Usage) 44 | vaultNewCmd.Flags().StringP(vaultK8sNamespaceFlag.Name, vaultK8sNamespaceFlag.Shorthand, "", vaultK8sNamespaceFlag.Usage) 45 | vaultNewCmd.Flags().BoolP(vaultEnableHashingFlag.Name, vaultEnableHashingFlag.Shorthand, false, vaultEnableHashingFlag.Usage) 46 | vaultNewCmd.Flags().BoolP(utils.QuantumSafeFlag.Name, utils.QuantumSafeFlag.Shorthand, false, utils.QuantumSafeFlag.Usage) 47 | } 48 | return vaultNewCmd 49 | } 50 | --------------------------------------------------------------------------------