├── web ├── .dockerignore ├── src │ ├── pages │ │ ├── index.tsx │ │ └── Targets │ │ │ ├── index.test.tsx │ │ │ └── index.tsx │ ├── react-app-env.d.ts │ ├── components │ │ ├── Application │ │ │ ├── index.tsx │ │ │ └── ApplicationContext.tsx │ │ ├── NavBar │ │ │ ├── index.tsx │ │ │ ├── NavBar.tsx │ │ │ └── NavButton.tsx │ │ ├── Targets │ │ │ ├── index.tsx │ │ │ ├── TargetContext.tsx │ │ │ ├── CreateTarget.tsx │ │ │ └── Targets.tsx │ │ ├── Delegations │ │ │ ├── index.tsx │ │ │ ├── DelegationContext.tsx │ │ │ ├── Delegations.tsx │ │ │ └── RegisterDelegationKey.tsx │ │ ├── index.tsx │ │ ├── TrashButton │ │ │ └── index.tsx │ │ ├── Notification │ │ │ └── index.tsx │ │ └── Form │ │ │ └── index.tsx │ ├── index.css │ ├── setupTests.ts │ ├── models │ │ └── index.tsx │ ├── index.tsx │ ├── App.tsx │ └── serviceWorker.ts ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── .prettierrc ├── craco.config.js ├── tailwind.config.js ├── Dockerfile ├── .gitignore ├── tsconfig.json ├── nginx │ └── templates │ │ └── default.conf.template ├── package.json └── README.md ├── .vscode ├── settings.json ├── extensions.json └── launch.json ├── .gitmodules ├── docs ├── diagrams │ ├── deployment-architecture.png │ └── deployment-architecture.plantuml └── README.md ├── .dockerignore ├── .notary ├── config.json ├── tuf │ └── localhost:5000 │ │ └── dct-notary-admin │ │ └── metadata │ │ ├── targets.json │ │ ├── snapshot.json │ │ └── root.json └── private │ ├── 760e57b96f72ed27e523633d2ffafe45ae0ff804e78dfc014a50f01f823d161d.key │ ├── 4ea1fec36392486d4bd99795ffc70f3ffa4a76185b39c8c2ab1d9cf5054dbbc9.key │ └── de7a5c03ef2602716c3af92b69e6d1f4a1cc07f0d9cfd5fc18928d1e0370fee6.key ├── lib ├── middleware │ ├── middleware.go │ └── zap.go ├── notary │ ├── errors.go │ ├── cmds_test.go │ ├── config.go │ ├── repo_factory.go │ ├── cmds.go │ ├── utils.go │ ├── service.go │ ├── service_test.go │ └── tuf.go ├── config.go ├── targets │ ├── dto.go │ ├── targets.go │ └── targets_test.go ├── secrets │ ├── secrets.go │ ├── secrets_test.go │ ├── vault_test.go │ └── vault.go ├── api.go ├── errors │ └── errors.go ├── api_test.go └── server.go ├── CODEOWNERS ├── vault ├── docker-compose.dev.yml ├── policies │ └── dctna-policy.hcl └── prepare.sh ├── cmd ├── dctna-server │ └── main.go ├── command_test.go ├── vault_config.go ├── version_test.go ├── version.go ├── config.go ├── root.go └── config_test.go ├── .github ├── workflows │ ├── greeting.yml │ └── ci.yml └── dependabot.yml ├── .editorconfig ├── .gitignore ├── docker-compose.yml ├── codecov.yml ├── certs ├── server.csr ├── server.crt └── server.key ├── bootstrap-sandbox.sh ├── LICENSE ├── Dockerfile ├── .goreleaser.yml ├── go.mod ├── Makefile └── README.md /web/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | -------------------------------------------------------------------------------- /web/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Targets'; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.formatTool": "goimports" 3 | } 4 | -------------------------------------------------------------------------------- /web/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/src/components/Application/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './ApplicationContext'; 2 | -------------------------------------------------------------------------------- /web/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /web/src/components/NavBar/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './NavButton'; 2 | export * from './NavBar'; 3 | -------------------------------------------------------------------------------- /web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philips-labs/dct-notary-admin/HEAD/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philips-labs/dct-notary-admin/HEAD/web/public/logo192.png -------------------------------------------------------------------------------- /web/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philips-labs/dct-notary-admin/HEAD/web/public/logo512.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "notary"] 2 | path = notary 3 | url = git@github.com:philips-forks/notary.git 4 | branch = feature/sandbox 5 | -------------------------------------------------------------------------------- /web/src/components/Targets/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './CreateTarget'; 2 | export * from './TargetContext'; 3 | export * from './Targets'; 4 | -------------------------------------------------------------------------------- /docs/diagrams/deployment-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philips-labs/dct-notary-admin/HEAD/docs/diagrams/deployment-architecture.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .vscode 3 | .github 4 | .gitignore 5 | bin 6 | Makefile 7 | Dockerfile 8 | docker-compose.yml 9 | *.out 10 | vault/ 11 | -------------------------------------------------------------------------------- /web/src/components/Delegations/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './RegisterDelegationKey'; 2 | export * from './DelegationContext'; 3 | export * from './Delegations'; 4 | -------------------------------------------------------------------------------- /web/src/components/Targets/TargetContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export const TargetContext = createContext({ refresh: () => {} }); 4 | -------------------------------------------------------------------------------- /.notary/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "trust_dir": ".", 3 | "remote_server": { 4 | "url": "https://localhost:4443", 5 | "skipTLSVerify": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /web/src/components/Delegations/DelegationContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export const DelegationContext = createContext({ 4 | refresh: () => {}, 5 | }); 6 | -------------------------------------------------------------------------------- /lib/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | type contextKey struct { 4 | name string 5 | } 6 | 7 | func (k *contextKey) String() string { 8 | return "api context value " + k.name 9 | } 10 | -------------------------------------------------------------------------------- /lib/notary/errors.go: -------------------------------------------------------------------------------- 1 | package notary 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | // ErrInvalidID error thrown when an invalid ID is provided 9 | ErrInvalidID = errors.New("invalid id") 10 | ) 11 | -------------------------------------------------------------------------------- /web/craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | style: { 3 | postcss: { 4 | plugins: [ 5 | require('tailwindcss'), 6 | require('autoprefixer'), 7 | ], 8 | }, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /web/src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Form'; 2 | export * from './NavBar'; 3 | export * from './Delegations'; 4 | export * from './Targets'; 5 | export * from './TrashButton'; 6 | export * from './Notification'; 7 | export * from './Application'; 8 | -------------------------------------------------------------------------------- /lib/config.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | // ServerConfig holds configuration options 4 | type ServerConfig struct { 5 | ListenAddr string `json:"listen_addr" mapstructure:"listen_addr"` 6 | ListenAddrTLS string `json:"listen_addr_tls" mapstructure:"listen_addr_tls"` 7 | } 8 | -------------------------------------------------------------------------------- /web/src/components/Application/ApplicationContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export const ApplicationContext = createContext({ 4 | displayError: (message: string, autoHide: boolean) => {}, 5 | displayInfo: (message: string, autoHide: boolean) => {}, 6 | }); 7 | -------------------------------------------------------------------------------- /web/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [require('@tailwindcss/forms')], 11 | }; 12 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # DCTNA architecture 2 | 3 | This document described the deployment architecture of DCTNA and component dependencies. 4 | 5 | [![Deployment Architecture](diagrams/deployment-architecture.png)][DeploymentArchitectureSVG] 6 | 7 | [DeploymentArchitectureSVG]: diagrams/deployment-architecture.svg "Open Deployment Architecture as SVG" 8 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | # they will be requested for review when someone opens a 4 | # pull request. 5 | * @philips-labs/secure-software-supply-chain 6 | 7 | # See CODEOWNERS syntax here: https://help.github.com/articles/about-codeowners/#codeowners-syntax 8 | -------------------------------------------------------------------------------- /vault/docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | 5 | vault: 6 | image: vault:1.5.4 7 | ports: 8 | - "8200:8200" 9 | restart: always 10 | volumes: 11 | - ./volumes/plugins:/vault/plugins 12 | cap_add: 13 | - IPC_LOCK 14 | # command: server -dev 15 | command: server -dev -dev-plugin-dir=/vault/plugins 16 | -------------------------------------------------------------------------------- /web/src/models/index.tsx: -------------------------------------------------------------------------------- 1 | export interface Target { 2 | id: string; 3 | gun: string; 4 | role: string; 5 | } 6 | 7 | export interface Delegation { 8 | id: string; 9 | role: string; 10 | } 11 | 12 | export interface TargetListData { 13 | targets: Target[]; 14 | } 15 | 16 | export interface DelegationListData { 17 | delegations: Delegation[]; 18 | } 19 | -------------------------------------------------------------------------------- /cmd/dctna-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/philips-labs/dct-notary-admin/cmd" 5 | ) 6 | 7 | var ( 8 | version = "dev" 9 | commit = "none" 10 | date = "unknown" 11 | ) 12 | 13 | func main() { 14 | v := cmd.VersionInfo{ 15 | Version: version, 16 | Commit: commit, 17 | Date: cmd.ParseDate(date), 18 | } 19 | cmd.Execute(v) 20 | } 21 | -------------------------------------------------------------------------------- /.notary/tuf/localhost:5000/dct-notary-admin/metadata/targets.json: -------------------------------------------------------------------------------- 1 | {"signed":{"_type":"Targets","delegations":{"keys":{},"roles":[]},"expires":"2023-04-06T12:50:14.328531721+02:00","targets":{},"version":1},"signatures":[{"keyid":"4ea1fec36392486d4bd99795ffc70f3ffa4a76185b39c8c2ab1d9cf5054dbbc9","method":"ecdsa","sig":"5WQTnXgsUw6R0oLhWPTZw940G4a2LTOEdRwTZNnU40ii8ikJYbJn5RoOVQyk6QhvhqMkXHG1Jzj6HXRT3ZIDkA=="}]} -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | "golang.go", 7 | "editorconfig.editorconfig", 8 | "jebbs.plantuml", 9 | "davidanson.vscode-markdownlint", 10 | "esbenp.prettier-vscode" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:25-alpine as build 2 | 3 | WORKDIR /app 4 | COPY package*.json yarn.lock ./ 5 | RUN yarn install 6 | COPY tsconfig.json craco.config.js tailwind.config.js tsconfig.json ./ 7 | COPY public public 8 | COPY src src 9 | RUN yarn build 10 | 11 | FROM nginx:1.29.3-alpine 12 | ENV DCTNA_API https://host.docker.internal:8443 13 | COPY nginx/templates /etc/nginx/templates/ 14 | COPY --from=build /app/build /usr/share/nginx/html 15 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .eslintcache 26 | -------------------------------------------------------------------------------- /.github/workflows/greeting.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [issues, pull_request] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - uses: actions/first-interaction@v3.1.0 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: 'Thank you for submitting your first issue. We will be looking into it as soon as possible.' 13 | pr-message: 'Thanks for your first PR. We really appreciate it!' 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 4 10 | tab_width = 4 11 | 12 | [*.go] 13 | indent_style = tab 14 | # required for multiline strings in test cases 15 | trim_trailing_whitespace = false 16 | 17 | [Makefile] 18 | indent_style = tab 19 | 20 | [*.yml] 21 | indent_size = 2 22 | 23 | [*.{ts*,css}] 24 | indent_size = 2 25 | tab_width = 2 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # binary builds 18 | bin/ 19 | ./dct-notary-admin 20 | 21 | vault/volumes/ 22 | 23 | plantuml.jar 24 | .env 25 | 26 | # goreleaser 27 | dist/ 28 | -------------------------------------------------------------------------------- /.notary/private/760e57b96f72ed27e523633d2ffafe45ae0ff804e78dfc014a50f01f823d161d.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | role: root 3 | 4 | MIHuMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAiD6antceEpAwICCAAw 5 | HQYJYIZIAWUDBAEqBBAj0gUwHLi57VM1yjTV2MweBIGg5KAzhn/QsGKP8NoOrA7+ 6 | kTzttga/0pS7/dAFwxP7hD9QU2BoIiOzhUgHf8/jMzHWrr3GSVAkZgstmV9Y1+Wk 7 | jIJyVBP6alXtnQld48wI/r9chPKcBdJJCXj3wz2n8mKypemAjcOwoCxGUpFLkub3 8 | /ddCcnV3G2xTAzZ4LU7nLqo2e/GtUkj0YBDVIz3lIsoowmRTMPeKQldS96TJLlU2 9 | uQ== 10 | -----END ENCRYPTED PRIVATE KEY----- 11 | -------------------------------------------------------------------------------- /web/src/components/TrashButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faTrashAlt } from '@fortawesome/free-solid-svg-icons'; 4 | 5 | interface TrashButtonProps { 6 | action: () => Promise; 7 | } 8 | 9 | export const TrashButton: FC = ({ action }) => { 10 | return ( 11 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /web/src/pages/Targets/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { act, render } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | import { TargetsPage } from '.'; 4 | 5 | describe('TargetPage', () => { 6 | it('should render a loading state', async () => { 7 | await act(async () => { 8 | const { getByText } = render( 9 | 10 | 11 | , 12 | ); 13 | expect(getByText('Loading…')).toBeInTheDocument(); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}", 13 | "env": {}, 14 | "args": [] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /cmd/command_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func executeCommand(root *cobra.Command, args ...string) (output string, err error) { 10 | _, output, err = executeCommandC(root, args...) 11 | return output, err 12 | } 13 | 14 | func executeCommandC(root *cobra.Command, args ...string) (c *cobra.Command, output string, err error) { 15 | buf := new(bytes.Buffer) 16 | root.SetOutput(buf) 17 | root.SetArgs(args) 18 | 19 | c, err = root.ExecuteC() 20 | 21 | return c, buf.String(), err 22 | } 23 | -------------------------------------------------------------------------------- /.notary/private/4ea1fec36392486d4bd99795ffc70f3ffa4a76185b39c8c2ab1d9cf5054dbbc9.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | gun: localhost:5000/dct-notary-admin 3 | role: targets 4 | 5 | MIHuMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAiWQ4hNOR6LhQICCAAw 6 | HQYJYIZIAWUDBAEqBBBToVkcXD0sKqr/S9HSyfb+BIGghPzW8f0g8Xuh3pqx6knA 7 | MxnFH4qNPLGg5kpy8zSPM2DfqOP0s66/YNT87ZUlI+XAFzc9z/ttn6uN+kaKP+MW 8 | jQKrKYHHtkPZwJasxLHbiEh+HA03zakQYGvEXnhQDmbKV9lsanbIALw8NidjLFFl 9 | l4Kyr52k87YL7Al2ZkZTHzZsyi/KVrP+hWnPcpiHgALXrEVQ+gV+sTinhjDgaeRX 10 | Ww== 11 | -----END ENCRYPTED PRIVATE KEY----- 12 | -------------------------------------------------------------------------------- /.notary/private/de7a5c03ef2602716c3af92b69e6d1f4a1cc07f0d9cfd5fc18928d1e0370fee6.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | gun: localhost:5000/dct-notary-admin 3 | role: snapshot 4 | 5 | MIHuMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAgOefNwhO6aHgICCAAw 6 | HQYJYIZIAWUDBAEqBBANV+MP6brt7qf7uOBJ7XxEBIGgxw0/3JKHpCgUGI6b4TYI 7 | rDBHkGWI6fR7URzPa/t6k3hmJUMDJ4EdO+QPB/HT+8vPPsLLsSUBBpIHX3ZfIvO8 8 | glcbBn+E5oX2tJr6+QJ+JAvu48W0RGVUVeYz4zd1uQtbU+yrvGzNGiFqB4mV/whe 9 | YMbh5oWAk1roxc+4Qhx7Du1azYriJ8ZF4QLKUvWWYAH+A9HdEagOqh/O12Va/fip 10 | WA== 11 | -----END ENCRYPTED PRIVATE KEY----- 12 | -------------------------------------------------------------------------------- /web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root'), 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.notary/tuf/localhost:5000/dct-notary-admin/metadata/snapshot.json: -------------------------------------------------------------------------------- 1 | {"signed":{"_type":"Snapshot","expires":"2023-04-06T12:50:14.328855297+02:00","meta":{"root":{"hashes":{"sha256":"3BdGKfgfwO2nToexflTmeqENOySyz2xsFCd5JuKEajQ=","sha512":"vBk5M0Nd5mrBk5NPqR/aIoxlnuc12bhc0KNq0wo0zY/nGYuWIroyCzHX/8td1AgTW9CnghCOrl7R9syRUG2Lfw=="},"length":2421},"targets":{"hashes":{"sha256":"EEc2KZ7pQSvKtyBPW03+e+YRuLXpm2G4Q+rfL7nd/3E=","sha512":"MFupMI4vnDx9lak6Ndx6CSwpfJVBMGYTcJXpsIhmo1QBVJWokfGpjhiPFLQowzzpZiU4zDj4m8Fmn1ApcPMyNw=="},"length":346}},"version":1},"signatures":[{"keyid":"de7a5c03ef2602716c3af92b69e6d1f4a1cc07f0d9cfd5fc18928d1e0370fee6","method":"ecdsa","sig":"IsGaLEvhRd6gX7bKkgmlQ+XVKJD8Zs73asXlJBPh0WEEJ4trrW3skWseMUGBOwTqtlT8fk2CJBOYHmTHUvEutw=="}]} -------------------------------------------------------------------------------- /web/src/pages/Targets/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import { Delegations, Targets } from '../../components'; 4 | 5 | export const TargetsPage: FC = () => { 6 | return ( 7 |
8 |
9 |

Targets

10 | 11 |
12 |
13 |

Delegations

14 | 15 | 16 | 17 |
18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.0" 2 | services: 3 | dctna-web: 4 | image: dctna-web 5 | build: web 6 | ports: 7 | - "3000:80" 8 | environment: 9 | DCTNA_API: https://dctna-server:8443 10 | depends_on: 11 | - dctna-server 12 | networks: 13 | - sig 14 | dctna-server: 15 | image: dctna-server 16 | build: . 17 | networks: 18 | - sig 19 | ports: 20 | - "8086:8086" 21 | - "8443:8443" 22 | environment: 23 | VAULT_ADDR: http://host.docker.internal:8200 24 | REMOTE_SERVER_URL: https://host.docker.internal:4443 25 | volumes: 26 | - dct_data:/root/.docker/trust 27 | 28 | volumes: 29 | dct_data: 30 | external: false 31 | networks: 32 | sig: 33 | external: false 34 | -------------------------------------------------------------------------------- /cmd/vault_config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | // VaultConfig is a copy of https://github.com/hashicorp/vault/blob/master/command/agent/config/config.go#L34 4 | // 5 | // as per https://github.com/hashicorp/vault/issues/9575 we are not supposed to depend on the 6 | // toplevel hashicorp/vault module, which breaks go modules in version 1.5.0 7 | type VaultConfig struct { 8 | Address string `hcl:"address"` 9 | CACert string `hcl:"ca_cert"` 10 | CAPath string `hcl:"ca_path"` 11 | TLSSkipVerify bool `hcl:"-"` 12 | TLSSkipVerifyRaw interface{} `hcl:"tls_skip_verify"` 13 | ClientCert string `hcl:"client_cert"` 14 | ClientKey string `hcl:"client_key"` 15 | TLSServerName string `hcl:"tls_server_name"` 16 | } 17 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | notify: 4 | after_n_builds: 3 5 | 6 | coverage: 7 | range: "70...100" 8 | status: 9 | # project will give us the diff in the total code coverage between a commit 10 | # and its parent 11 | project: 12 | default: 13 | target: auto 14 | threshold: "0.05%" 15 | # patch would give us the code coverage of the diff only 16 | patch: false 17 | # changes tells us if there are unexpected code coverage changes in other files 18 | # which were not changed by the diff 19 | changes: false 20 | ignore: # ignore testutils for coverage 21 | - "tuf/testutils/*" 22 | - "vendor/*" 23 | - "proto/*.pb.go" 24 | - "trustmanager/remoteks/*.pb.go" 25 | 26 | comment: 27 | layout: "reach,diff,flags,tree" 28 | behavior: default 29 | require_changes: no 30 | 31 | -------------------------------------------------------------------------------- /vault/policies/dctna-policy.hcl: -------------------------------------------------------------------------------- 1 | path "dctna/data/dev" { 2 | capabilities = [ "read" ] 3 | } 4 | 5 | # Create read and update secrets 6 | path "dctna/data/dev/*" { 7 | capabilities = ["create", "read", "update"] 8 | } 9 | 10 | # Delete last version of secret 11 | path "dctna/data/dev/*" { 12 | capabilities = ["delete"] 13 | } 14 | 15 | # Delete any version of secret 16 | path "dctna/delete/dev/*" { 17 | capabilities = ["update"] 18 | } 19 | 20 | # Destroy version 21 | path "dctna/destroy/dev/*" { 22 | capabilities = ["update"] 23 | } 24 | 25 | # Un-delete any version of secret 26 | path "dctna/undelete/dev/*" { 27 | capabilities = ["update"] 28 | } 29 | 30 | # list keys, view metadata and permanently remove all versions and destroy metadata for a key 31 | path "dctna/metadata/dev/*" { 32 | capabilities = ["list", "read", "delete"] 33 | } 34 | 35 | # password generation 36 | path "gen/password" { 37 | capabilities = ["create", "update"] 38 | } 39 | -------------------------------------------------------------------------------- /web/src/components/Notification/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faInfoCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; 4 | import cn from 'classnames'; 5 | 6 | interface NotificationProps { 7 | type: 'unknown' | 'error' | 'info'; 8 | message: string; 9 | } 10 | 11 | export const Notification: FC = ({ type, message }) => { 12 | return ( 13 |
19 |
20 | {type === 'error' ? ( 21 | 22 | ) : ( 23 | 24 | )} 25 |

{message}

26 |
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /certs/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICfTCCAWUCAQAwODELMAkGA1UEBhMCTkwxFTATBgNVBAoMDFBoaWxpcHMgTGFi 3 | czESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 4 | CgKCAQEA6luQ0ngrUNfabQ7WlLIpGx2IZgWliJkZElXEPJK9q8JIBF7jra614y5k 5 | Ix+yi6seE6BMVJF4uAMQnycp4SAWYXtwiCx8Q7ESFylg1drolHQdpiNHhXDhwzDM 6 | 7jP+/LiySj+ATHmWpvAhuMQx1duP2CfNjM9ZPL8sFUxeP30mMPK+Yt67/HSITXKk 7 | 6CvjjGPD3FBn+27UQ7MxzrwtkZD/KwDNll+dLQHNz/2nmuRcbbuTN6DvBCeY0hej 8 | yljRwjLq0G3+vWZzopi50gUdqbvrIncboQFAdrcGSJCIl/I5c9xTln4cgruwIuzD 9 | 0TWL+woLfgCQ1BS8KU5N9ZPq5Nh+5wIDAQABoAAwDQYJKoZIhvcNAQELBQADggEB 10 | AELD+MGPLTQfPPOhVFBiTcHMYRWVuycUNd/PEk2zbR4QZGJLOb4/J7jxNE9ROvTV 11 | Lwze1v5UM2pvP62l75sk6KH6G5HwOcnTwqBz4j0vXk0z+zvFrzJ7W3Ok8G2SOb+H 12 | uBzxV/ayfDV+Gd0cvdaDCagyBPk0HreyrDmcJvEIipi9WQEylJjYRYFpamq5koqa 13 | UW8lmzHaN5JL85hrvc4jsoI+ERa+IJzk/BY2fcTXytXFDfilmLdry8WNWj11mxOV 14 | 3Rv/JJPASvam7tdohkkMf99ZTggwjgvtZ5AtUgZKFIsAUcvHB3GH1yv4hKCPy5uY 15 | btcSf9BhKKL0wD9xVFKvwv8= 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /web/src/components/NavBar/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ClassAttributes, HTMLAttributes } from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faDatabase } from '@fortawesome/free-solid-svg-icons'; 4 | import { NavButton } from './NavButton'; 5 | 6 | export const NavBar: FC & HTMLAttributes> = () => { 7 | return ( 8 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /bootstrap-sandbox.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | export NOTARY_ROOT_PASSPHRASE=t0pS3cr3t 4 | export NOTARY_TARGETS_PASSPHRASE=t4rg3tr3p0 5 | export NOTARY_SNAPSHOT_PASSPHRASE=sn4psh0t 6 | export NOTARY_DELEGATION_PASSPHRASE=IcanSignImag3s 7 | export NOTARY_AUTH= 8 | 9 | export DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE=$NOTARY_ROOT_PASSPHRASE 10 | export DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE=$NOTARY_TARGETS_PASSPHRASE 11 | 12 | DOCKER_CONTENT_TRUST=0 13 | 14 | REGISTRY=registry:5000 15 | 16 | IMAGES="nginx:alpine alpine:latest busybox:latest traefik:latest" 17 | 18 | docker trust key generate ci --dir ~/.docker/trust 19 | 20 | for img in ${IMAGES}; do 21 | docker pull $img 22 | docker tag $img $REGISTRY/$img 23 | docker trust signer add continuous-integration --key ~/.docker/trust/ci.pub $REGISTRY/${img%:*} 24 | docker trust sign $REGISTRY/$img 25 | done 26 | 27 | unset NOTARY_ROOT_PASSPHRASE 28 | unset NOTARY_TARGETS_PASSPHRASE 29 | unset NOTARY_SNAPSHOT_PASSPHRASE 30 | unset NOTARY_DELEGATION_PASSPHRASE 31 | unset DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE 32 | unset DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE 33 | -------------------------------------------------------------------------------- /lib/notary/cmds_test.go: -------------------------------------------------------------------------------- 1 | package notary 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/theupdateframework/notary/tuf/data" 9 | ) 10 | 11 | func TestGuardHasGun(t *testing.T) { 12 | testCases := []struct { 13 | gun string 14 | shouldSucceed bool 15 | }{ 16 | {"", false}, 17 | {" ", false}, 18 | {"\t ", false}, 19 | {"\t\t", false}, 20 | {" \n", false}, 21 | {"\r\n\t", false}, 22 | {"abc", true}, 23 | {"localhost:5000/dct-notary-admin", true}, 24 | {"localhost:5000/dct-notary-admin\t", true}, 25 | {"localhost:5000/dct-notary-admin\n", true}, 26 | {" localhost:5000/dct-notary-admin", true}, 27 | {" localhost:5000/dct-notary-admin\r\n", true}, 28 | {" localhost:5000/ dct-notary-admin\r\n", true}, 29 | } 30 | 31 | for i, tc := range testCases { 32 | t.Run(strconv.Itoa(i), func(tt *testing.T) { 33 | cmd := TargetCommand{GUN: data.GUN(tc.gun)} 34 | err := cmd.GuardHasGUN() 35 | if tc.shouldSucceed { 36 | assert.NoError(tt, err) 37 | } else { 38 | assert.Error(tt, err) 39 | } 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Philips Labs 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 | -------------------------------------------------------------------------------- /certs/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC7DCCAdQCCQCSFWOtNPVBRTANBgkqhkiG9w0BAQUFADA4MQswCQYDVQQGEwJO 3 | TDEVMBMGA1UECgwMUGhpbGlwcyBMYWJzMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcN 4 | MjAwNTI3MTY1MjQ1WhcNMjEwNTI3MTY1MjQ1WjA4MQswCQYDVQQGEwJOTDEVMBMG 5 | A1UECgwMUGhpbGlwcyBMYWJzMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqG 6 | SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDqW5DSeCtQ19ptDtaUsikbHYhmBaWImRkS 7 | VcQ8kr2rwkgEXuOtrrXjLmQjH7KLqx4ToExUkXi4AxCfJynhIBZhe3CILHxDsRIX 8 | KWDV2uiUdB2mI0eFcOHDMMzuM/78uLJKP4BMeZam8CG4xDHV24/YJ82Mz1k8vywV 9 | TF4/fSYw8r5i3rv8dIhNcqToK+OMY8PcUGf7btRDszHOvC2RkP8rAM2WX50tAc3P 10 | /aea5Fxtu5M3oO8EJ5jSF6PKWNHCMurQbf69ZnOimLnSBR2pu+sidxuhAUB2twZI 11 | kIiX8jlz3FOWfhyCu7Ai7MPRNYv7Cgt+AJDUFLwpTk31k+rk2H7nAgMBAAEwDQYJ 12 | KoZIhvcNAQEFBQADggEBAK9kIBYGLzRPif1KhNLY408vwPOL9ztbO3Fw78yUgfb9 13 | nESCWsi//+ECR5Gp3RdK2DYJC/CIFKt3Zv8v3H9bq/qKiI/RTYqJHGPObc7X+pB5 14 | Yr2KeEN6OMeSKoPgrJ4LHv+G/IepOlil426vI9kBuXTnpXM2V0zLbyQJuMK+yggA 15 | wRFEIBWONbUvxV3dfGtcoGlVHHy6EZabPIQdTHG6dqiQhi/lNCwMOyATFM6mH9eQ 16 | wHBKIVYyhoV86T+CL+6PmH97featIV6nK/MVzfTC5mxe1nteUm9PN5QJ5dmECrM1 17 | bI6paIc1y49eKQLr341p6J2NcjRHsAyZ2VFP2AYch20= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /lib/notary/config.go: -------------------------------------------------------------------------------- 1 | package notary 2 | 3 | // Config notary configuration 4 | type Config struct { 5 | TrustDir string `json:"trust_dir" mapstructure:"trust_dir"` 6 | RemoteServer RemoteServerConfig `json:"remote_server" mapstructure:"remote_server"` 7 | TrustPinning TrustPinningConfig `json:"trust_pinning" mapstructure:"trust_pinning"` 8 | } 9 | 10 | // RemoteServerConfig notary remote server configuration 11 | type RemoteServerConfig struct { 12 | URL string `json:"url" mapstructure:"url"` 13 | RootCA string `json:"root_ca" mapstructure:"root_ca"` 14 | TLSClientKey string `json:"tls_client_key" mapstructure:"tls_client_key"` 15 | TLSClientCert string `json:"tls_client_cert" mapstructure:"tls_client_cert"` 16 | SkipTLSVerify bool `json:"skipTLSVerify" mapstructure:"skipTLSVerify"` 17 | } 18 | 19 | // TrustPinningConfig notary trust pinning configuration 20 | type TrustPinningConfig struct { 21 | DisableTofu bool `json:"disable_tofu" mapstructure:"disable_tofu"` 22 | CA map[string]string `json:"ca" mapstructure:"ca"` 23 | Certs map[string]interface{} `json:"certs" mapstructure:"certs"` 24 | } 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "npm" 14 | directory: "/web" 15 | schedule: 16 | interval: "monthly" 17 | 18 | - package-ecosystem: "github-actions" 19 | # Workflow files stored in the 20 | # default location of `.github/workflows` 21 | directory: "/" 22 | schedule: 23 | interval: "monthly" 24 | 25 | - package-ecosystem: "gitsubmodule" 26 | directory: "/" 27 | schedule: 28 | interval: "monthly" 29 | 30 | - package-ecosystem: "docker" 31 | directory: "/" 32 | schedule: 33 | interval: "monthly" 34 | 35 | - package-ecosystem: "docker" 36 | directory: "/web" 37 | schedule: 38 | interval: "monthly" 39 | -------------------------------------------------------------------------------- /lib/notary/repo_factory.go: -------------------------------------------------------------------------------- 1 | package notary 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/theupdateframework/notary" 7 | "github.com/theupdateframework/notary/client" 8 | "github.com/theupdateframework/notary/tuf/data" 9 | ) 10 | 11 | const remoteConfigField = "api" 12 | 13 | // RepoFactory takes a GUN and returns an initialized client.Repository, or an error. 14 | type RepoFactory func(gun data.GUN) (client.Repository, error) 15 | 16 | // ConfigureRepo takes in the configuration parameters and returns a repoFactory that can 17 | // initialize new client.Repository objects with the correct upstreams and password 18 | // retrieval mechanisms. 19 | func ConfigureRepo(config *Config, retriever notary.PassRetriever, onlineOperation bool, permission httpAccess) RepoFactory { 20 | localRepo := func(gun data.GUN) (client.Repository, error) { 21 | var rt http.RoundTripper 22 | trustPin, err := getTrustPinning(config) 23 | if err != nil { 24 | return nil, err 25 | } 26 | if onlineOperation { 27 | rt, err = getTransport(config, gun, permission) 28 | if err != nil { 29 | return nil, err 30 | } 31 | } 32 | return client.NewFileCachedRepository( 33 | config.TrustDir, 34 | gun, 35 | config.RemoteServer.URL, 36 | rt, 37 | retriever, 38 | trustPin, 39 | ) 40 | } 41 | 42 | return localRepo 43 | } 44 | -------------------------------------------------------------------------------- /cmd/version_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestParseDateUnknown(t *testing.T) { 12 | d := ParseDate("unknown") 13 | 14 | assert.WithinDuration(t, time.Now(), d, 1*time.Millisecond) 15 | } 16 | 17 | func TestParseDateRFC3339(t *testing.T) { 18 | exp := "2019-11-17T16:11:22Z" 19 | d := ParseDate(exp) 20 | 21 | assert.Equal(t, exp, d.Format(time.RFC3339)) 22 | } 23 | 24 | func TestParseDateNonRFC3339(t *testing.T) { 25 | d := ParseDate("01 Jan 15 10:00 UTC") 26 | exp := time.Now() 27 | 28 | assert.Equal(t, exp.Format(time.RFC3339), d.Format(time.RFC3339)) 29 | } 30 | 31 | func TestVersionCommands(t *testing.T) { 32 | version = VersionInfo{ 33 | Version: "test", 34 | Commit: "ab23f6", 35 | Date: ParseDate("2019-11-17T16:11:22Z"), 36 | } 37 | exp := fmt.Sprintf(`%s 38 | 39 | version: %s 40 | commit: %s 41 | date: %s 42 | 43 | `, rootCmd.Short, version.Version, version.Commit, version.Date.Format(time.RFC3339)) 44 | args := []string{"version", "--version", "-v"} 45 | 46 | for _, arg := range args { 47 | t.Run(arg, func(tt *testing.T) { 48 | output, err := executeCommand(rootCmd, arg) 49 | assert := assert.New(tt) 50 | assert.NoError(err) 51 | assert.Equal(exp, output) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; 3 | import { TargetsPage } from './pages'; 4 | import { NavBar, ApplicationContext, Notification } from './components'; 5 | import { setTimeout } from 'timers'; 6 | 7 | function App() { 8 | const [error, setError] = useState(''); 9 | const [info, setInfo] = useState(''); 10 | 11 | const displayError = (message: string, autoHide: boolean) => { 12 | setError(message); 13 | if (autoHide) { 14 | setTimeout(() => setError(''), 3000); 15 | } 16 | }; 17 | const displayInfo = (message: string, autoHide: boolean) => { 18 | setInfo(message); 19 | if (autoHide) { 20 | setTimeout(() => setInfo(''), 3000); 21 | } 22 | }; 23 | 24 | return ( 25 | 26 | 27 |
28 | 29 |
30 | {error ? : null} 31 | {info ? : null} 32 | 33 | 34 | 35 |
36 |
37 |
38 |
39 | ); 40 | } 41 | 42 | export default App; 43 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Get latest ca-certificates 2 | FROM alpine AS certs 3 | RUN apk --update add ca-certificates 4 | 5 | FROM golang:1.25.4-alpine AS base 6 | 7 | # To fix go get and build with cgo 8 | RUN apk add --no-cache --virtual .build-deps \ 9 | bash \ 10 | gcc \ 11 | git \ 12 | musl-dev 13 | 14 | RUN mkdir build 15 | WORKDIR /build 16 | 17 | COPY go.mod . 18 | COPY go.sum . 19 | 20 | RUN go mod download 21 | 22 | # Build the image 23 | FROM base as builder 24 | COPY . . 25 | ARG VERSION=dev-docker 26 | ARG DATE= 27 | ARG COMMIT= 28 | 29 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo \ 30 | -ldflags "-X 'main.version=${VERSION}' -X 'main.date=${DATE}' -X 'main.commit=${COMMIT}' -extldflags '-static'" \ 31 | -o dctna-server ./cmd/dctna-server 32 | 33 | # Collect certificates and binary 34 | FROM gcr.io/distroless/base-debian11 35 | EXPOSE 8086 8443 36 | COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 37 | # root user required as the volumes mount as root 38 | # files in the volumes can only be accessed by the owner of the files 39 | # which are in this case root 40 | # TODO: find a way arround this. 41 | WORKDIR /root 42 | VOLUME [ "/root/.notary", "/root/.docker/trust", "/root/certs" ] 43 | COPY certs/ /root/certs/ 44 | COPY .notary/config.json /root/.notary/config.json 45 | COPY --from=builder /build/dctna-server /root/ 46 | CMD [ "./dctna-server" ] 47 | -------------------------------------------------------------------------------- /lib/middleware/zap.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/go-chi/chi/middleware" 9 | 10 | "go.uber.org/zap" 11 | ) 12 | 13 | var LogEntryCtxKey = &contextKey{"ZapLogger"} 14 | 15 | // GetZapLogger retrieves the *zp.Logger from http.Request Context 16 | func GetZapLogger(r *http.Request) *zap.Logger { 17 | entry, _ := r.Context().Value(LogEntryCtxKey).(*zap.Logger) 18 | return entry 19 | } 20 | 21 | // ZapLogger logger middleware to add the *zap.Logger 22 | func ZapLogger(l *zap.Logger) func(next http.Handler) http.Handler { 23 | return func(next http.Handler) http.Handler { 24 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) 26 | 27 | rl := l.With( 28 | zap.String("proto", r.Proto), 29 | zap.String("path", r.URL.Path), 30 | zap.String("reqId", middleware.GetReqID(r.Context())), 31 | ) 32 | t1 := time.Now() 33 | defer func() { 34 | rl.Info("Served", 35 | zap.Duration("lat", time.Since(t1)), 36 | zap.Int("status", ww.Status()), 37 | zap.Int("size", ww.BytesWritten()), 38 | ) 39 | }() 40 | 41 | next.ServeHTTP(ww, withZapLogger(r, rl)) 42 | }) 43 | } 44 | } 45 | 46 | func withZapLogger(r *http.Request, logger *zap.Logger) *http.Request { 47 | return r.WithContext(context.WithValue(r.Context(), LogEntryCtxKey, logger)) 48 | } 49 | -------------------------------------------------------------------------------- /web/src/components/NavBar/NavButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC, cloneElement, MouseEvent } from 'react'; 2 | import { matchPath, useHistory, useLocation } from 'react-router'; 3 | import classNames from 'classnames'; 4 | 5 | export interface IconButton { 6 | label: string; 7 | icon: any; 8 | } 9 | 10 | export interface RoutedButtonProps { 11 | path: string; 12 | } 13 | 14 | export const NavButton: FC = ({ path, label, icon }) => { 15 | // const match = useRouteMatch(path); 16 | const location = useLocation(); 17 | const history = useHistory(); 18 | 19 | const onClick = (event: MouseEvent) => { 20 | event.preventDefault(); 21 | history.push(path); 22 | }; 23 | // const pathMatch = matchPath(location.pathname, { exact: match?.isExact, path }); 24 | const pathMatch = matchPath(location.pathname, { exact: true, path }); 25 | 26 | return ( 27 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "text/tabwriter" 7 | "time" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // VersionInfo holds version information 13 | type VersionInfo struct { 14 | Version string 15 | Commit string 16 | Date time.Time 17 | } 18 | 19 | func (v VersionInfo) String() string { 20 | sb := new(strings.Builder) 21 | w := tabwriter.NewWriter(sb, 0, 0, 2, ' ', 0) 22 | fmt.Fprintf(w, "\tversion:\t%s\n", v.Version) 23 | fmt.Fprintf(w, "\tcommit:\t%s\n", v.Commit) 24 | fmt.Fprintf(w, "\tdate:\t%s\n", v.Date.Format(time.RFC3339)) 25 | w.Flush() 26 | return sb.String() 27 | } 28 | 29 | var ( 30 | version VersionInfo 31 | versionCmd = &cobra.Command{ 32 | Use: "version", 33 | Short: "shows version information", 34 | Run: func(cmd *cobra.Command, _ []string) { 35 | cmd.Println(sprintVersion(rootCmd)) 36 | }, 37 | } 38 | ) 39 | 40 | func init() { 41 | rootCmd.AddCommand(versionCmd) 42 | } 43 | 44 | // ParseDate parse string using time.RFC3339 format or default to time.Now() 45 | func ParseDate(value string) time.Time { 46 | if value == "unknown" { 47 | return time.Now() 48 | } 49 | 50 | parsedDate, err := time.Parse(time.RFC3339, value) 51 | if err != nil { 52 | parsedDate = time.Now() 53 | } 54 | return parsedDate 55 | } 56 | 57 | func sprintVersion(cmd *cobra.Command) string { 58 | return fmt.Sprintf("%s\n\n%s", cmd.Short, version) 59 | } 60 | -------------------------------------------------------------------------------- /lib/notary/cmds.go: -------------------------------------------------------------------------------- 1 | package notary 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/theupdateframework/notary/tuf/data" 7 | ) 8 | 9 | // TargetCommand holds the target parameters 10 | type TargetCommand struct { 11 | GUN data.GUN 12 | } 13 | 14 | // CreateRepoCommand holds data to create a new repository for the given data.GUN 15 | type CreateRepoCommand struct { 16 | TargetCommand 17 | RootKey string 18 | RootCert string 19 | AutoPublish bool 20 | } 21 | 22 | // DeleteRepositoryCommand holds data to delete the repository for the given data.GUN 23 | type DeleteRepositoryCommand struct { 24 | TargetCommand 25 | DeleteRemote bool 26 | } 27 | 28 | // AddDelegationCommand holds parameters to create a delegation 29 | type AddDelegationCommand struct { 30 | TargetCommand 31 | Role data.RoleName 32 | DelegationKeys []data.PublicKey 33 | Paths []string 34 | AutoPublish bool 35 | } 36 | 37 | // RemoveDelegationCommand holds parameters to remove a delegation 38 | type RemoveDelegationCommand struct { 39 | TargetCommand 40 | Role data.RoleName 41 | KeyID string 42 | AutoPublish bool 43 | } 44 | 45 | // GuardHasGUN guards that a valid GUN has been provided 46 | func (cmd TargetCommand) GuardHasGUN() error { 47 | if cmd.SanitizedGUN() == "" { 48 | return ErrGunMandatory 49 | } 50 | return nil 51 | } 52 | 53 | // SanitizedGUN removes all whitespace characters from a GUN 54 | func (cmd TargetCommand) SanitizedGUN() data.GUN { 55 | return data.GUN(strings.Trim(cmd.GUN.String(), " \t\r\n")) 56 | } 57 | -------------------------------------------------------------------------------- /lib/targets/dto.go: -------------------------------------------------------------------------------- 1 | package targets 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/go-chi/render" 9 | 10 | "github.com/philips-labs/dct-notary-admin/lib/notary" 11 | ) 12 | 13 | type RepositoryRequest struct { 14 | GUN string `json:"gun"` 15 | } 16 | 17 | type DelegationRequest struct { 18 | DelegationPublicKey string `json:"delegationPublicKey"` 19 | DelegationName string `json:"delegationName"` 20 | } 21 | 22 | func (rr *DelegationRequest) Bind(r *http.Request) error { 23 | return nil 24 | } 25 | 26 | // Bind unmarshals request into structure and validates / cleans input 27 | func (rr *RepositoryRequest) Bind(r *http.Request) error { 28 | rr.GUN = strings.Trim(rr.GUN, " \t") 29 | if rr.GUN == "" { 30 | return errors.New("gun is required") 31 | } 32 | 33 | return nil 34 | } 35 | 36 | // KeyResponse returns a notary.Key structure 37 | type KeyResponse struct { 38 | *notary.Key 39 | } 40 | 41 | // NewKeyResponse creates a KeyResponse from a notary.Key structure 42 | func NewKeyResponse(target notary.Key) *KeyResponse { 43 | return &KeyResponse{&target} 44 | } 45 | 46 | // Render renders a KeyResponse 47 | func (t *KeyResponse) Render(w http.ResponseWriter, r *http.Request) error { 48 | // preprocessing possible here 49 | return nil 50 | } 51 | 52 | // NewKeyListResponse returns a slice of KeyResponse 53 | func NewKeyListResponse(targets []notary.Key) []render.Renderer { 54 | list := make([]render.Renderer, len(targets)) 55 | 56 | for i, t := range targets { 57 | list[i] = NewKeyResponse(t) 58 | } 59 | 60 | return list 61 | } 62 | -------------------------------------------------------------------------------- /lib/secrets/secrets.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "github.com/sethvargo/go-password/password" 5 | ) 6 | 7 | type PasswordGenerator interface { 8 | Generate() (string, error) 9 | } 10 | 11 | type DefaultPasswordOptions struct { 12 | Len *int `json:"length,omitempty"` 13 | Digits *int `json:"digits,omitempty"` 14 | Symbols *int `json:"symbols,omitempty"` 15 | AllowUppercase *bool `json:"allow_uppercase,omitempty"` 16 | AllowRepeat *bool `json:"allow_repeat,omitempty"` 17 | } 18 | 19 | type DefaultPasswordGenerator struct { 20 | options DefaultPasswordOptions 21 | } 22 | 23 | func intPtr(v int) *int { 24 | return &v 25 | } 26 | func boolPtr(v bool) *bool { 27 | return &v 28 | } 29 | 30 | func NewDefaultPasswordGenerator(options DefaultPasswordOptions) *DefaultPasswordGenerator { 31 | if options.Len == nil { 32 | options.Len = intPtr(64) 33 | } 34 | if options.Digits == nil { 35 | options.Digits = intPtr(10) 36 | } 37 | if options.Symbols == nil { 38 | options.Symbols = intPtr(10) 39 | } 40 | if options.AllowUppercase == nil { 41 | options.AllowUppercase = boolPtr(true) 42 | } 43 | if options.AllowRepeat == nil { 44 | options.AllowRepeat = boolPtr(true) 45 | } 46 | 47 | return &DefaultPasswordGenerator{ 48 | options: options, 49 | } 50 | } 51 | 52 | func (g *DefaultPasswordGenerator) Generate() (string, error) { 53 | res, err := password.Generate( 54 | *g.options.Len, 55 | *g.options.Digits, 56 | *g.options.Symbols, 57 | !*g.options.AllowUppercase, 58 | *g.options.AllowRepeat, 59 | ) 60 | return res, err 61 | } 62 | -------------------------------------------------------------------------------- /certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDqW5DSeCtQ19pt 3 | DtaUsikbHYhmBaWImRkSVcQ8kr2rwkgEXuOtrrXjLmQjH7KLqx4ToExUkXi4AxCf 4 | JynhIBZhe3CILHxDsRIXKWDV2uiUdB2mI0eFcOHDMMzuM/78uLJKP4BMeZam8CG4 5 | xDHV24/YJ82Mz1k8vywVTF4/fSYw8r5i3rv8dIhNcqToK+OMY8PcUGf7btRDszHO 6 | vC2RkP8rAM2WX50tAc3P/aea5Fxtu5M3oO8EJ5jSF6PKWNHCMurQbf69ZnOimLnS 7 | BR2pu+sidxuhAUB2twZIkIiX8jlz3FOWfhyCu7Ai7MPRNYv7Cgt+AJDUFLwpTk31 8 | k+rk2H7nAgMBAAECggEAQ3bMf+ob/g/FTzPATdnjv2yHAWeKuYHpF8Ac15segr1Q 9 | 4gE25Z6vuZB3Py02xsIhzGCO2KFv8Pjg5g/uHGKSTsmsIWc89neYz5YXzDhST7oF 10 | dvgc9KsEMp5vv/qz4qiAmqMhGeK+nZvu+TNxbTDhfcYng+uEstoNpKvxTbGHNvt9 11 | CSDZ/535msNbU1c0YIfgAS1fiQP04A9Ij7tROQST51yeS0UnpongDR4E+y+16mME 12 | Yoz5iz/RpC38386W4qnNrmXSF7gGFpaR10Kcj3y6Yvck6Ab4kbTzWaPUJKMQ0rGK 13 | 1t2Fzpji0j0WCWV4nrgf3Ve9G8QYQeJfYb/aD+FOgQKBgQD6L5QYq6YasZt9gpOz 14 | 85zLUseppiEqSsPVMYKIkKd4OTSQ/2h308Eep+B08It9uSPZ8B0W5YC5AhHOybJs 15 | 6XCC5cCze25lD2hp1UPOnlSL17YW1JvjDxNOTVFYT0HccuXDPQK8QYkPfhXrg5Qh 16 | tgcmw/sZkw4UyYUim/vbYS3S4QKBgQDvzdI8LpWxCeAG7e61STPz779IYgLe+WTr 17 | IhklgEb0EgEigsgaFxhEPl3sK2/XblFXqsUqe5xcBtkexLwfaKPJ6X1VRzSGVH0+ 18 | urho4RqnFAV92Tv4W6aUv071jKDEDYzyNNb10JbwtxPnRvz7KS8YN1OQqaEuf5V6 19 | 3WrZl93SxwKBgQCKCEIPMnTceW0uXCKdwCSb0TozkWLu8H0HXN+F9TlQjU9BSIkc 20 | w4njHgAqxKRRYxoyndWO0mSorkmg64szp7/ZmGUvIUSRIWUcLvvQPW6sEQF3qymu 21 | ppyiMOpWbLQLqqC1jS27K/UwxQHXBKtvxfxAGRSq0YWNVnnL1H+qU/1yQQKBgQCY 22 | O0zvL5OPo/k37OvQ4fu9K8F+rdmsux25txfHmrtwTc2ynttw1KBvEjiFqL2DQWEa 23 | CirIYVhNg3tp8PKL7d2sXDFYZa+OZyxrGhXfXqeQ0UD7N2C+tU/2tcvmTbPxQbjs 24 | OWcM/bIUdeSIuaP3vZlMAjNmbokH/65hJRajs6j6GQKBgQC/Q1hhMiNzctjnP6x/ 25 | UleHBYR/tDP26Eoij61ETRfIScqst8gjOWRqx4aRwXsqb28jytK8nBlLt52ec0t6 26 | ahR4p3ZVxx+RW0mOjYPSyljtFfG9nsTGYKKgsYw3WmYkWEr7muWnfVj3b1xXEBPp 27 | pbEIf/23+Xc+rWu6cVplXRD8vA== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /lib/api.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/go-chi/chi" 8 | "github.com/go-chi/chi/middleware" 9 | "github.com/go-chi/render" 10 | 11 | "go.uber.org/zap" 12 | 13 | m "github.com/philips-labs/dct-notary-admin/lib/middleware" 14 | "github.com/philips-labs/dct-notary-admin/lib/notary" 15 | "github.com/philips-labs/dct-notary-admin/lib/targets" 16 | ) 17 | 18 | func configureAPI(n *notary.Service, l *zap.Logger) *chi.Mux { 19 | r := chi.NewRouter() 20 | 21 | r.Use(middleware.RequestID) 22 | r.Use(m.ZapLogger(l)) 23 | r.Use(middleware.RedirectSlashes) 24 | r.Use(middleware.Recoverer) 25 | 26 | r.Get("/", func(w http.ResponseWriter, r *http.Request) { 27 | w.Header().Set("Content-Type", ContentTypePlainText) 28 | w.WriteHeader(http.StatusOK) 29 | }) 30 | r.Get("/ping", func(w http.ResponseWriter, r *http.Request) { 31 | w.Header().Set("Content-Type", ContentTypePlainText) 32 | w.WriteHeader(http.StatusOK) 33 | w.Write([]byte("pong\n")) 34 | }) 35 | r.Route("/api", func(rr chi.Router) { 36 | rr.Use(render.SetContentType(render.ContentTypeJSON)) 37 | 38 | tr := targets.NewResource(n) 39 | tr.RegisterRoutes(rr) 40 | }) 41 | 42 | logRoutes(r, l) 43 | 44 | return r 45 | } 46 | 47 | func logRoutes(r *chi.Mux, logger *zap.Logger) { 48 | if err := chi.Walk(r, zapPrintRoute(logger)); err != nil { 49 | logger.Error("Failed to walk routes:", zap.Error(err)) 50 | } 51 | } 52 | 53 | func zapPrintRoute(logger *zap.Logger) chi.WalkFunc { 54 | return func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { 55 | route = strings.Replace(route, "/*/", "/", -1) 56 | logger.Debug("Registering route", zap.String("method", method), zap.String("route", route)) 57 | return nil 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/diagrams/deployment-architecture.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | !$authors = "Marco Franssen" 4 | 5 | title DCTNA Deployment Architecture 6 | center header Author: $authors 7 | center footer Author: $authors 8 | 9 | skinparam sequence { 10 | ArrowColor black 11 | ActorBorderColor DeepSkyBlue 12 | LifeLineBorderColor black 13 | LifeLineBackgroundColor grey 14 | } 15 | 16 | skinparam component<> { 17 | BackgroundColor lightblue 18 | BorderColor blue 19 | } 20 | 21 | skinparam component<> { 22 | BackgroundColor lightgreen 23 | BorderColor green 24 | } 25 | 26 | skinparam actor { 27 | BackgroundColor lightgrey 28 | BorderColor black 29 | } 30 | 31 | skinparam database { 32 | BackgroundColor lightgrey 33 | BorderColor black 34 | } 35 | 36 | skinparam database<> { 37 | BackgroundColor lightgreen 38 | BorderColor green 39 | } 40 | 41 | skinparam component { 42 | BackgroundColor lightgrey 43 | BorderColor black 44 | } 45 | 46 | actor "Docker Trust Admin" as Admin 47 | actor "CI Job" as CiJob 48 | 49 | database ServerDB <> [ 50 | server-db 51 | ] 52 | database SignerDB <> [ 53 | signer-db 54 | ] 55 | 56 | [dctna-web] <> 57 | [dctna-server] <> 58 | [notary-signer] <> 59 | [notary-server] <> 60 | 61 | CiJob ..> [docker-cli] : sign docker image\nand push to registry 62 | [docker-cli] ..> [docker-registry] : push/pull\nimages 63 | [docker-cli] ..> [notary-server] : sign image /\nfetch tuf metadata 64 | Admin ..> [dctna-web] : administer docker repositories\nand delegations 65 | [dctna-web] ..> [dctna-server] : rest api 66 | [dctna-server] .right.> [vault] : stores passphrases\nfor keys 67 | [dctna-server] ..> [notary-server] 68 | 69 | [notary-server] .left.> [notary-signer] 70 | [notary-server] ..> [ServerDB] 71 | [notary-signer] ..> [SignerDB] 72 | 73 | @enduml 74 | -------------------------------------------------------------------------------- /web/nginx/templates/default.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name localhost; 5 | 6 | gzip on; 7 | gzip_disable "msie6"; 8 | 9 | gzip_vary on; 10 | gzip_proxied any; 11 | gzip_comp_level 6; 12 | gzip_buffers 16 8k; 13 | gzip_http_version 1.1; 14 | gzip_min_length 0; 15 | gzip_types text/plain application/javascript text/css text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype; 16 | 17 | root /usr/share/nginx/html; 18 | 19 | location / { 20 | index index.html index.htm; 21 | expires -1; 22 | try_files $uri $uri/ /index.html; 23 | } 24 | 25 | location /api { 26 | proxy_pass ${DCTNA_API}; 27 | } 28 | 29 | #error_page 404 /404.html; 30 | 31 | # redirect server error pages to the static page /50x.html 32 | # 33 | error_page 500 502 503 504 /50x.html; 34 | location = /50x.html { 35 | root /usr/share/nginx/html; 36 | } 37 | 38 | location ~* \.(?:manifest|appcache|html?|xml|json)$ { 39 | expires -1; 40 | # access_log logs/static.log; # I don't usually include a static log 41 | } 42 | 43 | # Feed 44 | location ~* \.(?:rss|atom)$ { 45 | expires 1h; 46 | add_header Cache-Control "public"; 47 | } 48 | 49 | # Media: images, icons, video, audio, HTC 50 | location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ { 51 | expires 1M; 52 | access_log off; 53 | add_header Cache-Control "public"; 54 | } 55 | 56 | # CSS and Javascript 57 | location ~* \.(?:css|js)$ { 58 | expires 1y; 59 | access_log off; 60 | add_header Cache-Control "public"; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /lib/secrets/secrets_test.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDefaultPasswordGenerator(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | testCases := []struct { 14 | name string 15 | options DefaultPasswordOptions 16 | expLen int 17 | exp *regexp.Regexp 18 | }{ 19 | { 20 | name: "all explicit", expLen: 12, exp: regexp.MustCompile("^[a-z]+$"), 21 | options: DefaultPasswordOptions{ 22 | Len: intPtr(12), Digits: intPtr(0), Symbols: intPtr(0), AllowUppercase: boolPtr(false), AllowRepeat: boolPtr(true), 23 | }, 24 | }, 25 | {name: "defaults", expLen: 64, exp: nil}, 26 | { 27 | name: "lowercase alpha numeric only", expLen: 64, exp: regexp.MustCompile("^[a-z]+$"), 28 | options: DefaultPasswordOptions{ 29 | AllowUppercase: boolPtr(false), Digits: intPtr(0), Symbols: intPtr(0), 30 | }, 31 | }, 32 | { 33 | name: "alpha numeric only", expLen: 64, exp: regexp.MustCompile("^[A-Za-z]+$"), 34 | options: DefaultPasswordOptions{ 35 | Digits: intPtr(0), Symbols: intPtr(0), 36 | }, 37 | }, 38 | { 39 | name: "alpha numeric and digits only", expLen: 64, exp: regexp.MustCompile("^[A-Za-z\\d]+$"), 40 | options: DefaultPasswordOptions{ 41 | Digits: intPtr(5), Symbols: intPtr(0), 42 | }, 43 | }, 44 | { 45 | name: "alpha numeric only shorter Length", expLen: 32, exp: regexp.MustCompile("^[a-zA-Z]+$"), 46 | options: DefaultPasswordOptions{ 47 | Len: intPtr(32), Digits: intPtr(0), Symbols: intPtr(0), 48 | }, 49 | }, 50 | } 51 | 52 | for _, tt := range testCases { 53 | t.Run(tt.name, func(t *testing.T) { 54 | pgen := NewDefaultPasswordGenerator(tt.options) 55 | password, err := pgen.Generate() 56 | if !assert.NoError(err) { 57 | return 58 | } 59 | assert.Len(password, tt.expLen) 60 | if tt.exp != nil { 61 | assert.Regexp(tt.exp, password) 62 | } 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dct-notary-admin-web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "https://localhost:8443", 6 | "scripts": { 7 | "start": "craco start", 8 | "build": "craco build", 9 | "test": "craco test", 10 | "eject": "react-scripts eject" 11 | }, 12 | "eslintConfig": { 13 | "extends": "react-app" 14 | }, 15 | "browserslist": { 16 | "production": [ 17 | ">0.2%", 18 | "not dead", 19 | "not op_mini all" 20 | ], 21 | "development": [ 22 | "last 1 chrome version", 23 | "last 1 firefox version", 24 | "last 1 safari version" 25 | ] 26 | }, 27 | "dependencies": { 28 | "@fortawesome/fontawesome-svg-core": "^6.4.0", 29 | "@fortawesome/free-solid-svg-icons": "^6.4.0", 30 | "@fortawesome/react-fontawesome": "^0.2.2", 31 | "axios": "^1.7.7", 32 | "classnames": "^2.3.2", 33 | "polished": "^4.1.4", 34 | "react": "^17.0.2", 35 | "react-dom": "^17.0.2", 36 | "react-router-dom": "^6.27.0", 37 | "react-scripts": "^4.0.3" 38 | }, 39 | "devDependencies": { 40 | "@craco/craco": "^6.4.3", 41 | "@tailwindcss/forms": "^0.3.4", 42 | "@tailwindcss/postcss7-compat": "^2.2.4", 43 | "@testing-library/jest-dom": "^6.6.3", 44 | "@testing-library/react": "^12.1.5", 45 | "@testing-library/user-event": "^14.2.1", 46 | "@types/classnames": "^2.3.1", 47 | "@types/jest": "^29.5.5", 48 | "@types/node": "^20.3.3", 49 | "@types/react": "^17.0.43", 50 | "@types/react-dom": "^17.0.14", 51 | "@types/react-router-dom": "^5.3.3", 52 | "autoprefixer": "^9.8.8", 53 | "postcss": "^8.4.47", 54 | "prettier": "^3.0.3", 55 | "tailwindcss": "npm:@tailwindcss/postcss7-compat", 56 | "typescript": "^5.8.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/errors/errors.go: -------------------------------------------------------------------------------- 1 | package targets 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/render" 7 | ) 8 | 9 | // ErrResponse renderer type for handling all sorts of errors. 10 | // 11 | // In the best case scenario, the excellent github.com/pkg/errors package 12 | // helps reveal information on the error, setting it on Err, and in the Render() 13 | // method, using it to set the application-specific error code in AppCode. 14 | type ErrResponse struct { 15 | Err error `json:"-"` // low-level runtime error 16 | HTTPStatusCode int `json:"-"` // http response status code 17 | 18 | StatusText string `json:"status"` // user-level status message 19 | AppCode int64 `json:"code,omitempty"` // application-specific error code 20 | ErrorText string `json:"error,omitempty"` // application-level error message, for debugging 21 | } 22 | 23 | func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error { 24 | render.Status(r, e.HTTPStatusCode) 25 | return nil 26 | } 27 | 28 | var ( 29 | ErrNotImplemented = &ErrResponse{HTTPStatusCode: http.StatusNotImplemented, StatusText: "Not implemented."} 30 | ErrNotFound = &ErrResponse{HTTPStatusCode: http.StatusNotFound, StatusText: "Resource not found."} 31 | ) 32 | 33 | func ErrInternalServer(err error) render.Renderer { 34 | return &ErrResponse{ 35 | Err: err, 36 | HTTPStatusCode: http.StatusInternalServerError, 37 | StatusText: "Internal server error.", 38 | ErrorText: err.Error(), 39 | } 40 | } 41 | 42 | func ErrRender(err error) render.Renderer { 43 | return &ErrResponse{ 44 | Err: err, 45 | HTTPStatusCode: http.StatusUnprocessableEntity, 46 | StatusText: "Error rendering response.", 47 | ErrorText: err.Error(), 48 | } 49 | } 50 | 51 | func ErrInvalidRequest(err error) render.Renderer { 52 | return &ErrResponse{ 53 | Err: err, 54 | HTTPStatusCode: http.StatusBadRequest, 55 | StatusText: "Invalid request.", 56 | ErrorText: err.Error(), 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /web/src/components/Targets/CreateTarget.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState, useContext, FormEvent, ChangeEvent } from 'react'; 2 | import axios from 'axios'; 3 | import { TargetContext } from './TargetContext'; 4 | import { FormTextInput } from '../Form'; 5 | 6 | type CreateTargetState = { gun: string; errorMessage?: string }; 7 | const defaultFormValue = { gun: '', errorMessage: '' }; 8 | 9 | export const CreateTarget: FC = () => { 10 | const [value, setValue] = useState(defaultFormValue); 11 | const { refresh } = useContext(TargetContext); 12 | const submitForm = async (event: FormEvent) => { 13 | event.preventDefault(); 14 | try { 15 | const { errorMessage, ...requestBody } = value; 16 | await axios.post(`/api/targets`, JSON.stringify(requestBody), { 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | Accept: 'application/json', 20 | }, 21 | }); 22 | setValue(defaultFormValue); 23 | refresh(); 24 | } catch (e: any) { 25 | const response = e.response; 26 | const errorMessage = `${response.data.status} ${response.data.error}`; 27 | setValue({ ...value, errorMessage }); 28 | console.log(value); 29 | } 30 | }; 31 | 32 | return ( 33 |
34 | ) => { 40 | setValue({ gun: event.target.value }); 41 | }} 42 | className="mb-2" 43 | /> 44 | {value.errorMessage &&

{value.errorMessage}

} 45 |
46 | 52 |
53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /.notary/tuf/localhost:5000/dct-notary-admin/metadata/root.json: -------------------------------------------------------------------------------- 1 | {"signed":{"_type":"Root","consistent_snapshot":false,"expires":"2030-04-04T12:50:14.327465661+02:00","keys":{"4ea1fec36392486d4bd99795ffc70f3ffa4a76185b39c8c2ab1d9cf5054dbbc9":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEomsC2TNraFxS+TlT0UBVpmrDczZ1ABfR6Egfganr/hRDxoXGw/qp2knAxebzxSB1Zpby0vc7jNN2rpNZvxBG2Q=="}},"7470a49792d0fe6d5512f7cf3e247c84195cd1b8a8c0ebd730c1904df8d22554":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeddxdqZdalijyji0TjeUUyEqPEilrkZ2SOIFVeSZiSRxiz5+z/7TGRKSD83CC9QQ17IgEdabA8F2N7BvYJGhNg=="}},"dd62f2fe10c0a8c3d328c302c5595ca728885a6f07c6193496ac3f95e3efc945":{"keytype":"ecdsa-x509","keyval":{"private":null,"public":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJpRENDQVM2Z0F3SUJBZ0lSQVBPTGQrOHhlZHUzT29ORWRuY3U2bGt3Q2dZSUtvWkl6ajBFQXdJd0tqRW8KTUNZR0ExVUVBeE1mYkc5allXeG9iM04wT2pVd01EQXZaR04wTFc1dmRHRnllUzFoWkcxcGJqQWVGdzB5TURBMApNRFl4TURRNU5URmFGdzB6TURBME1EUXhNRFE1TlRGYU1Db3hLREFtQmdOVkJBTVRIMnh2WTJGc2FHOXpkRG8xCk1EQXdMMlJqZEMxdWIzUmhjbmt0WVdSdGFXNHdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBUzAKdHFPQ0RnVWVXS1hWeWNSVE8vSExQSy9iYlN6S21JL1dPZGY0YTRONDRIMzVDRlR4RlNjVjV6WFo5QlBtVndQLwp6WHh5d1hyVFRsa0pxaGdOZGtUWG96VXdNekFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJCkt3WUJCUVVIQXdNd0RBWURWUjBUQVFIL0JBSXdBREFLQmdncWhrak9QUVFEQWdOSUFEQkZBaUVBemtZbXVYSHoKY2tGdkwwcTNHaS9rU0dBb2pqeno5Z0JNejk2cWlDN05NY1lDSUVXdjBkZ3hBRG8zbG1BY0IyZFA3d01TcUdVZAplQlFiQ1Z4czNxWE1QL1I2Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"}},"de7a5c03ef2602716c3af92b69e6d1f4a1cc07f0d9cfd5fc18928d1e0370fee6":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE67IEk61nOSZK04a/ERkmbv1fgK0AlWHprmeoVWGuavkRGHGvAyE33JTuegt5ui4IWfaRIkd5FgK9+hUyYO4HNw=="}}},"roles":{"root":{"keyids":["dd62f2fe10c0a8c3d328c302c5595ca728885a6f07c6193496ac3f95e3efc945"],"threshold":1},"snapshot":{"keyids":["de7a5c03ef2602716c3af92b69e6d1f4a1cc07f0d9cfd5fc18928d1e0370fee6"],"threshold":1},"targets":{"keyids":["4ea1fec36392486d4bd99795ffc70f3ffa4a76185b39c8c2ab1d9cf5054dbbc9"],"threshold":1},"timestamp":{"keyids":["7470a49792d0fe6d5512f7cf3e247c84195cd1b8a8c0ebd730c1904df8d22554"],"threshold":1}},"version":1},"signatures":[{"keyid":"dd62f2fe10c0a8c3d328c302c5595ca728885a6f07c6193496ac3f95e3efc945","method":"ecdsa","sig":"jTmS/Yr0c0b9qQ8qzot/KmKF+lpiTclD747FzUGNWJ3ALaI9FeWD6bLP56/7RoM8mgpkiL793zJMxqQac/cWiQ=="}]} -------------------------------------------------------------------------------- /web/src/components/Form/index.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEventHandler } from 'react'; 2 | import cn from 'classnames'; 3 | 4 | const uniqueId = ((): ((prefix: string) => string) => { 5 | let counter = 0; 6 | return (prefix: string): string => `${prefix}${++counter}`; 7 | })(); 8 | 9 | interface FormElementProps { 10 | label: string; 11 | name: string; 12 | placeholder?: string; 13 | value?: string; 14 | help?: string; 15 | required: boolean; 16 | className?: string; 17 | } 18 | interface FormInputProps { 19 | onChange?: ChangeEventHandler; 20 | } 21 | 22 | interface FormTextAreaProps { 23 | onChange?: ChangeEventHandler; 24 | } 25 | 26 | export function FormTextInput({ 27 | label, 28 | name, 29 | placeholder, 30 | required, 31 | value, 32 | help, 33 | className, 34 | onChange, 35 | }: FormElementProps & FormInputProps) { 36 | const id = uniqueId(`input-${name?.replaceAll(' ', '').toLowerCase()}`); 37 | 38 | return ( 39 | 53 | ); 54 | } 55 | 56 | export function FormTextArea({ 57 | label, 58 | name, 59 | placeholder, 60 | required, 61 | value, 62 | help, 63 | className, 64 | onChange, 65 | }: FormElementProps & FormTextAreaProps) { 66 | const id = uniqueId(`input-${name?.replaceAll(' ', '').toLowerCase()}`); 67 | 68 | return ( 69 |