├── .gitignore ├── docker-compose.yml ├── frontend ├── gql │ ├── index.ts │ └── fragment-masking.ts ├── app │ ├── favicon.ico │ ├── fonts │ │ ├── GeistVF.woff │ │ └── GeistMonoVF.woff │ ├── console │ │ ├── layout.tsx │ │ ├── Footer.tsx │ │ ├── gql.ts │ │ ├── [teamSlug] │ │ │ └── project │ │ │ │ └── [projectSlug] │ │ │ │ ├── scan │ │ │ │ └── [scanId] │ │ │ │ │ ├── details │ │ │ │ │ └── [itemId] │ │ │ │ │ │ └── gql.ts │ │ │ │ │ ├── gql.ts │ │ │ │ │ └── page.tsx │ │ │ │ ├── gql.ts │ │ │ │ ├── stripe_checkout_form.tsx │ │ │ │ └── combobox.tsx │ │ ├── Navbar.tsx │ │ └── page.tsx │ ├── sitemap.ts │ ├── providers │ │ └── PostHogProvider.tsx │ ├── ApolloProviderWrapper.tsx │ ├── layout.tsx │ ├── globals.css │ └── page.tsx ├── postcss.config.mjs ├── example.env ├── .eslintrc.json ├── codegen.ts ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── input.tsx │ │ ├── checkbox.tsx │ │ ├── badge.tsx │ │ ├── popover.tsx │ │ ├── button.tsx │ │ ├── tabs.tsx │ │ ├── card.tsx │ │ ├── accordion.tsx │ │ ├── table.tsx │ │ ├── dialog.tsx │ │ ├── sheet.tsx │ │ ├── command.tsx │ │ └── select.tsx │ ├── theme-provider.tsx │ ├── Icons.tsx │ ├── color-mode-toggle.tsx │ └── PricingTable.tsx ├── public │ └── robots.txt ├── next.config.mjs ├── components.json ├── .gitignore ├── docker │ ├── development │ │ └── Dockerfile │ └── production │ │ └── Dockerfile ├── tsconfig.json ├── middleware.ts ├── apollo-client.ts ├── README.md ├── package.json ├── lib │ └── utils.ts └── tailwind.config.ts ├── backend ├── tools.go ├── types │ └── main.go ├── sqlc.yaml ├── docker │ ├── production │ │ └── Dockerfile │ └── development │ │ └── Dockerfile ├── modelapi │ ├── types.go │ └── grokapi │ │ └── main_test.go ├── graph │ ├── resolver.go │ ├── model │ │ └── models_gen.go │ └── schema.graphqls ├── database │ └── postgres │ │ ├── db.go │ │ ├── main.go │ │ ├── models.go │ │ ├── schema.sql │ │ └── query.sql ├── logger │ └── main.go ├── httpmiddleware │ └── main.go ├── utils │ └── main.go ├── gqlgen.yml ├── awsmiddleware │ ├── scanner.go │ ├── main.go │ ├── dynamodbscanner │ │ ├── scanner.go │ │ └── main.go │ └── lambdascanner │ │ └── scanner.go ├── auth │ └── main.go ├── server.go └── go.mod ├── run_services.sh ├── example.env ├── docker-compose.frontend.yml ├── supervisord.conf ├── .github └── workflows │ └── docker-publish-self-host.yml ├── docker-compose.backend.yml ├── internal.env ├── LICENSE ├── Dockerfile.selfhost ├── start ├── CONTRIBUTING.md ├── templates ├── guard-self-host-policy.json └── guard-scan-role.yaml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | tmp 3 | 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | -------------------------------------------------------------------------------- /frontend/gql/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fragment-masking"; 2 | export * from "./gql"; -------------------------------------------------------------------------------- /frontend/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guard-dev/guard/HEAD/frontend/app/favicon.ico -------------------------------------------------------------------------------- /frontend/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guard-dev/guard/HEAD/frontend/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /frontend/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guard-dev/guard/HEAD/frontend/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /backend/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "github.com/99designs/gqlgen" 7 | _ "github.com/99designs/gqlgen/graphql/introspection" 8 | ) 9 | -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /backend/types/main.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type SummarizeFindings struct { 4 | Summary string `json:"summary"` 5 | Remedies string `json:"remedies"` 6 | Commands []string `json:"commands"` 7 | } 8 | -------------------------------------------------------------------------------- /frontend/example.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 2 | CLERK_SECRET_KEY= 3 | NEXT_PUBLIC_API_BASE_URL= 4 | NEXT_SERVER_API_BASE_URL= 5 | 6 | NEXT_PUBLIC_POSTHOG_KEY= 7 | NEXT_PUBLIC_POSTHOG_HOST= 8 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-empty-object-type": "off", 5 | "@typescript-eslint/no-explicit-any": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /backend/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | sql: 3 | - engine: "postgresql" 4 | queries: "database/postgres/query.sql" 5 | schema: "database/postgres/schema.sql" 6 | gen: 7 | go: 8 | package: "postgres" 9 | out: "database/postgres" 10 | -------------------------------------------------------------------------------- /backend/docker/production/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN go mod download 8 | 9 | RUN CGO_ENABLED=0 GOOS=linux go build -o /guard-gql-server server.go 10 | 11 | EXPOSE 80 12 | 13 | ENTRYPOINT [ "/guard-gql-server" ] 14 | -------------------------------------------------------------------------------- /backend/docker/development/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23 2 | 3 | WORKDIR /app 4 | 5 | RUN go install github.com/air-verse/air@latest 6 | 7 | COPY . . 8 | 9 | RUN go mod download 10 | 11 | RUN CGO_ENABLED=0 GOOS=linux go build -o /guard-gql-server server.go 12 | 13 | EXPOSE 80 14 | 15 | ENTRYPOINT ["air"] 16 | 17 | -------------------------------------------------------------------------------- /frontend/codegen.ts: -------------------------------------------------------------------------------- 1 | import { CodegenConfig } from '@graphql-codegen/cli'; 2 | 3 | const config: CodegenConfig = { 4 | schema: '../backend/graph/schema.graphqls', 5 | documents: ['app/**/*.{tsx,ts}'], 6 | generates: { 7 | './gql/': { 8 | preset: 'client', 9 | }, 10 | }, 11 | }; 12 | export default config; 13 | -------------------------------------------------------------------------------- /frontend/app/console/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from "./Navbar"; 2 | 3 | export default function RootLayout({ 4 | children, 5 | }: Readonly<{ 6 | children: React.ReactNode; 7 | }>) { 8 | return ( 9 | 10 | 11 | 12 | {children} 13 | 14 | 15 | ); 16 | } 17 | 18 | -------------------------------------------------------------------------------- /frontend/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /frontend/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from 'next'; 2 | 3 | const baseUrl = 'https://www.guard.dev'; 4 | 5 | export default async function sitemap(): Promise { 6 | return [ 7 | { 8 | url: baseUrl, 9 | lastModified: new Date().toISOString(), 10 | changeFrequency: 'yearly', 11 | priority: 1, 12 | }, 13 | ]; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /frontend/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | import { type ThemeProviderProps } from "next-themes/dist/types" 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children} 9 | } 10 | 11 | -------------------------------------------------------------------------------- /run_services.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Ensure required environment variables are set 4 | : "${GEMINI_SECRET_KEY:?GEMINI_SECRET_KEY is not set}" 5 | : "${AWS_REGION:?AWS_REGION is not set}" 6 | : "${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID is not set}" 7 | : "${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY is not set}" 8 | 9 | # Start Supervisor to manage services 10 | exec supervisord -c /etc/supervisor/supervisord.conf 11 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | 3 | Allow: /$ # Allow the index page 4 | 5 | Allow: /blog # Allow the /blog page (if it exists as a standalone page) 6 | Allow: /blog/ # Allow everything under /blog/ (all subdirectories and pages) 7 | 8 | Disallow: /app # Disallow the /app page (if it exists as a standalone page) 9 | Disallow: /app/ # Disallow everything under /app/ (all subdirectories and pages) 10 | 11 | 12 | -------------------------------------------------------------------------------- /backend/modelapi/types.go: -------------------------------------------------------------------------------- 1 | package modelapi 2 | 3 | import ( 4 | "context" 5 | "guarddev/types" 6 | ) 7 | 8 | // ModelAPI defines the interface that all model implementations must satisfy 9 | type ModelAPI interface { 10 | // SummarizeFindings analyzes AWS resource findings and provides summary, remedies, and commands 11 | SummarizeFindings(ctx context.Context, service string, region string, accountId string, findings []string) (*types.SummarizeFindings, error) 12 | } 13 | -------------------------------------------------------------------------------- /frontend/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | webpack: (config, context) => { 6 | // Enable polling based on env variable being set 7 | if (process.env.NEXT_WEBPACK_USEPOLLING) { 8 | config.watchOptions = { 9 | poll: 500, 10 | aggregateTimeout: 300 11 | } 12 | } 13 | return config 14 | }, 15 | } 16 | 17 | export default nextConfig; 18 | -------------------------------------------------------------------------------- /frontend/app/providers/PostHogProvider.tsx: -------------------------------------------------------------------------------- 1 | // app/providers.js 2 | 'use client' 3 | import posthog from 'posthog-js' 4 | import { PostHogProvider } from 'posthog-js/react' 5 | 6 | if (typeof window !== 'undefined') { 7 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY || "", { 8 | api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, 9 | }) 10 | } 11 | export function CSPostHogProvider({ children }: { children: React.ReactNode }) { 12 | return {children} 13 | } 14 | -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /frontend/.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /frontend/docker/development/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Node.js image as the base 2 | FROM node:18 3 | 4 | # Set the working directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy package.json, package-lock.json and other config files to the container 8 | COPY . . 9 | 10 | # Install dependencies 11 | RUN rm -rf node_modules && npm ci --loglevel verbose 12 | 13 | # Copy the app source code to the container 14 | COPY . . 15 | 16 | # Build the Next.js app 17 | RUN npm run build --loglevel verbose 18 | # RUN npm run build 19 | 20 | # Expose the port the app will run on 21 | EXPOSE 3000 22 | 23 | # Start the app 24 | CMD ["npm", "run", "dev"] 25 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | # Configure the following 2 | 3 | # Configure Gemini 4 | GEMINI_SECRET_KEY= 5 | 6 | # Configure Anthropic 7 | ANTHROPIC_SECRET_KEY= 8 | 9 | # Configure xAI 10 | GROK_SECRET_KEY= 11 | 12 | MODEL_TYPE= 13 | 14 | # Configure AWS 15 | AWS_REGION= 16 | AWS_ACCESS_KEY_ID= 17 | AWS_SECRET_ACCESS_KEY= 18 | 19 | ## Already configured 20 | 21 | NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/graph 22 | NEXT_WEBPACK_USEPOLLING=true 23 | 24 | SELF_HOSTING=1 25 | NEXT_PUBLIC_SELF_HOSTING=true 26 | 27 | POSTGRES_DB_HOST=postgres_db 28 | POSTGRES_DB_PORT=5432 29 | POSTGRES_DB_USER=postgres 30 | POSTGRES_DB_PASS=postgrespw 31 | POSTGRES_DB_NAME=postgres 32 | -------------------------------------------------------------------------------- /backend/graph/resolver.go: -------------------------------------------------------------------------------- 1 | //go:generate go run github.com/99designs/gqlgen generate 2 | 3 | package graph 4 | 5 | import ( 6 | "guarddev/awsmiddleware" 7 | "guarddev/database/postgres" 8 | "guarddev/logger" 9 | "guarddev/modelapi" 10 | "guarddev/paymentsmiddleware" 11 | ) 12 | 13 | // This file will not be regenerated automatically. 14 | // 15 | // It serves as dependency injection for your app, add any dependencies you require here. 16 | 17 | type Resolver struct { 18 | Database *postgres.Database 19 | Logger *logger.LogMiddleware 20 | AWSMiddleware *awsmiddleware.AWSMiddleware 21 | Gemini modelapi.ModelAPI 22 | Payments *paymentsmiddleware.Payments 23 | } 24 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /frontend/app/console/Footer.tsx: -------------------------------------------------------------------------------- 1 | export const FooterSection = () => { 2 | return ( 3 |
4 |
5 |

© 2024 Guard. All rights reserved.

6 |
7 |
8 | Community Discord 9 | GitHub 10 | Contact Us 11 |
12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /backend/database/postgres/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.27.0 4 | 5 | package postgres 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/app/console/gql.ts: -------------------------------------------------------------------------------- 1 | 2 | import { gql } from '@apollo/client'; 3 | 4 | export const GET_EXTERNAL_ID = gql` 5 | query GetExternalId { 6 | getExternalId 7 | } 8 | ` 9 | 10 | export const GET_PROJECTS = gql` 11 | query GetProjects($teamSlug: String) { 12 | teams(teamSlug: $teamSlug) { 13 | projects { 14 | projectSlug 15 | projectName 16 | } 17 | } 18 | } 19 | `; 20 | 21 | export const GET_TEAMS = gql` 22 | query GetTeams { 23 | teams { 24 | teamName 25 | teamSlug 26 | projects { 27 | projectSlug 28 | } 29 | } 30 | } 31 | `; 32 | 33 | export const VERIFY_ACCOUNT_ID = gql` 34 | mutation VerifyAccountId($accountId: String!) { 35 | verifyAccountId(accountId:$accountId) 36 | } 37 | ` 38 | -------------------------------------------------------------------------------- /docker-compose.frontend.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | 4 | frontend: 5 | container_name: frontend 6 | build: 7 | context: ./frontend 8 | dockerfile: docker/${ENVIRONMENT}/Dockerfile 9 | args: 10 | NEXT_PUBLIC_API_BASE_URL: https://api.guard.dev/graph 11 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY} 12 | ports: 13 | - "3000:3000" 14 | env_file: 15 | - ./.env 16 | environment: 17 | NEXT_SERVER_API_BASE_URL: http://backend:8080/graph 18 | NEXT_PUBLIC_API_BASE_URL: http://localhost:8080/graph 19 | volumes: 20 | - ./frontend:/app 21 | - node_modules:/app/node_modules 22 | - next_data:/app/.next 23 | profiles: ["development", "production"] 24 | 25 | volumes: 26 | node_modules: 27 | next_data: 28 | -------------------------------------------------------------------------------- /frontend/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /frontend/app/console/[teamSlug]/project/[projectSlug]/scan/[scanId]/details/[itemId]/gql.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const GET_SCAN_ITEM = gql` 4 | query GetScanItem($teamSlug: String!, $projectSlug: String!, $scanId: String!, $scanItemId: Int64!) { 5 | teams(teamSlug: $teamSlug) { 6 | projects(projectSlug: $projectSlug) { 7 | scans(scanId: $scanId) { 8 | scanItems(scanItemId: $scanItemId) { 9 | scanItemId 10 | service 11 | region 12 | findings 13 | summary 14 | remedy 15 | resourceCost 16 | scanItemEntries { 17 | findings 18 | title 19 | summary 20 | remedy 21 | commands 22 | resourceCost 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /frontend/app/console/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { DarkModeToggle } from "@/components/color-mode-toggle"; 2 | import { UserButton } from "@clerk/nextjs"; 3 | 4 | const Navbar = () => { 5 | const selfHosting = process.env.NEXT_PUBLIC_SELF_HOSTING === 'true'; 6 | 7 | return ( 8 |
9 | 10 |
11 |
12 |

13 | guard 14 |

15 |
16 | 17 |
18 | 19 | {!selfHosting && } 20 |
21 |
22 |
23 | ) 24 | }; 25 | 26 | export default Navbar; 27 | -------------------------------------------------------------------------------- /frontend/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /frontend/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /frontend/app/console/[teamSlug]/project/[projectSlug]/scan/[scanId]/gql.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const GET_SCANS = gql` 4 | query GetScans($teamSlug: String!, $projectSlug: String!, $scanId: String!) { 5 | teams(teamSlug: $teamSlug) { 6 | projects(projectSlug: $projectSlug) { 7 | scans(scanId: $scanId) { 8 | scanCompleted 9 | serviceCount 10 | regionCount 11 | resourceCost 12 | scanItems { 13 | scanItemId 14 | service 15 | region 16 | findings 17 | summary 18 | remedy 19 | resourceCost 20 | scanItemEntries { 21 | findings 22 | title 23 | summary 24 | remedy 25 | commands 26 | resourceCost 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | `; 34 | 35 | -------------------------------------------------------------------------------- /supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:postgresql] 5 | command=/usr/local/bin/docker-entrypoint.sh postgres 6 | autostart=true 7 | autorestart=true 8 | stdout_logfile=/dev/stdout 9 | stdout_logfile_maxbytes=0 10 | stderr_logfile=/dev/stderr 11 | stderr_logfile_maxbytes=0 12 | 13 | [program:backend] 14 | command=/app/backend/guard-gql-server 15 | autostart=true 16 | autorestart=true 17 | stdout_logfile=/dev/stdout 18 | stdout_logfile_maxbytes=0 19 | stderr_logfile=/dev/stderr 20 | stderr_logfile_maxbytes=0 21 | environment=PORT=8080,GEMINI_SECRET_KEY=%(ENV_GEMINI_SECRET_KEY)s,AWS_REGION=%(ENV_AWS_REGION)s,AWS_ACCESS_KEY_ID=%(ENV_AWS_ACCESS_KEY_ID)s,AWS_SECRET_ACCESS_KEY=%(ENV_AWS_SECRET_ACCESS_KEY)s 22 | 23 | [program:frontend] 24 | command=npm start --prefix /app/frontend 25 | autostart=true 26 | autorestart=true 27 | stdout_logfile=/dev/stdout 28 | stdout_logfile_maxbytes=0 29 | stderr_logfile=/dev/stderr 30 | stderr_logfile_maxbytes=0 31 | environment=PORT=3000 32 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish-self-host.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Docker Image to GHCR 2 | 3 | on: 4 | push: 5 | branches: ['main'] # Trigger on push to the main branch 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build-and-push-image: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Log in to GitHub Container Registry 22 | uses: docker/login-action@v2 23 | with: 24 | registry: ${{ env.REGISTRY }} 25 | username: ${{ github.actor }} 26 | password: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Build and push Docker image 29 | uses: docker/build-push-action@v5 30 | with: 31 | context: . 32 | file: Dockerfile.selfhost # Custom Dockerfile name 33 | push: true 34 | tags: | 35 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest 36 | 37 | -------------------------------------------------------------------------------- /docker-compose.backend.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | 4 | postgres_db: 5 | container_name: postgres-db 6 | image: postgres:latest 7 | environment: 8 | POSTGRES_DB: postgres 9 | POSTGRES_USER: postgres 10 | POSTGRES_PASSWORD: postgrespw 11 | volumes: 12 | - ./backend/database/postgres/schema.sql:/docker-entrypoint-initdb.d/schema.sql 13 | - postgres_data:/var/lib/postgresql/data 14 | ports: 15 | - "5432:5432" 16 | profiles: ["production", "development"] 17 | 18 | backend: 19 | container_name: backend 20 | build: 21 | context: ./backend/ 22 | dockerfile: docker/${ENVIRONMENT}/Dockerfile 23 | deploy: 24 | restart_policy: 25 | condition: on-failure 26 | delay: 5s # Delay before the restart 27 | max_attempts: 3 28 | window: 120s 29 | ports: 30 | - "80:80" 31 | - "8080:8080" 32 | - "443:443" 33 | env_file: 34 | - ./.env 35 | volumes: 36 | - ./backend:/app 37 | profiles: ["production", "development"] 38 | 39 | volumes: 40 | postgres_data: 41 | -------------------------------------------------------------------------------- /frontend/app/ApolloProviderWrapper.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { GetApolloClient } from '@/apollo-client'; 4 | import { ApolloProvider } from '@apollo/client'; 5 | import { useAuth } from '@clerk/nextjs'; 6 | import { PropsWithChildren } from 'react'; 7 | 8 | const ClerkApolloProvider = ({ children }: PropsWithChildren) => { 9 | const { getToken } = useAuth(); 10 | const client = GetApolloClient(false, getToken); 11 | return {children}; 12 | }; 13 | 14 | const SelfHostedApolloProvider = ({ children }: PropsWithChildren) => { 15 | const client = GetApolloClient(false, () => Promise.resolve('self-hosted')); 16 | return {children}; 17 | }; 18 | 19 | export const ApolloProviderWrapper = ({ children }: PropsWithChildren) => { 20 | const selfHosting = process.env.NEXT_PUBLIC_SELF_HOSTING === 'true'; 21 | 22 | if (selfHosting) { 23 | return {children}; 24 | } 25 | 26 | return {children}; 27 | }; 28 | -------------------------------------------------------------------------------- /backend/graph/model/models_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. 2 | 3 | package model 4 | 5 | type CheckoutSessionResponse struct { 6 | SessionID string `json:"sessionId"` 7 | } 8 | 9 | type Mutation struct { 10 | } 11 | 12 | type NewProject struct { 13 | ProjectName string `json:"projectName"` 14 | } 15 | 16 | type PortalSessionResponse struct { 17 | SessionURL string `json:"sessionUrl"` 18 | } 19 | 20 | type Query struct { 21 | } 22 | 23 | type SubscriptionData struct { 24 | CurrentPeriodStart string `json:"currentPeriodStart"` 25 | CurrentPeriodEnd string `json:"currentPeriodEnd"` 26 | Status string `json:"status"` 27 | Interval string `json:"interval"` 28 | PlanName string `json:"planName"` 29 | CostInUsd int64 `json:"costInUsd"` 30 | LastFourCardDigits string `json:"lastFourCardDigits"` 31 | ResourcesIncluded int `json:"resourcesIncluded"` 32 | ResourcesUsed int `json:"resourcesUsed"` 33 | } 34 | 35 | type Userinfo struct { 36 | UserID int64 `json:"userId"` 37 | Email string `json:"email"` 38 | FullName string `json:"fullName"` 39 | } 40 | -------------------------------------------------------------------------------- /internal.env: -------------------------------------------------------------------------------- 1 | # SECRET KEYS 2 | CLERK_SECRET_KEY= 3 | GEMINI_SECRET_KEY= 4 | STRIPE_SECRET_KEY= 5 | STRIPE_WEBHOOK_ENDPOINT_SECRET= 6 | ANTHROPIC_SECRET_KEY= 7 | GROK_SECRET_KEY= 8 | 9 | MODEL_TYPE= 10 | 11 | # THIS NEEDS TO BE SET TO NOTHING IN NON PROD ENVIRONMENTS 12 | PRODUCTION= 13 | 14 | # BACKEND OTEL 15 | OTEL_EXPORTER_OTLP_ENDPOINT=https://in-otel.hyperdx.io 16 | OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf 17 | OTEL_SERVICE_NAME='' 18 | OTEL_EXPORTER_OTLP_HEADERS='authorization=' 19 | 20 | # AWS 21 | AWS_REGION= 22 | AWS_ACCESS_KEY_ID= 23 | AWS_SECRET_ACCESS_KEY= 24 | 25 | ## Frontend 26 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 27 | NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/graph 28 | NEXT_SERVER_API_BASE_URL= 29 | NEXT_PUBLIC_POSTHOG_KEY= 30 | NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com 31 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= 32 | NEXT_WEBPACK_USEPOLLING=true 33 | 34 | # THIS NEEDS TO BE SET TO NOTHING IF NOT SELF HOSTING 35 | SELF_HOSTING=1 36 | NEXT_PUBLIC_SELF_HOSTING=true 37 | 38 | POSTGRES_DB_HOST=postgres_db 39 | POSTGRES_DB_PORT=5432 40 | POSTGRES_DB_USER=postgres 41 | POSTGRES_DB_PASS=postgrespw 42 | POSTGRES_DB_NAME=postgres 43 | -------------------------------------------------------------------------------- /frontend/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { CheckIcon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )) 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | export { Checkbox } 31 | -------------------------------------------------------------------------------- /backend/logger/main.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hyperdxio/opentelemetry-go/otelzap" 7 | sdk "github.com/hyperdxio/opentelemetry-logs-go/sdk/logs" 8 | "go.opentelemetry.io/otel/trace" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | type LoggerConnectProps struct { 13 | Production bool 14 | LoggerProvider *sdk.LoggerProvider 15 | } 16 | 17 | type LogMiddleware struct { 18 | logger *zap.Logger 19 | } 20 | 21 | func Connect(args LoggerConnectProps) *LogMiddleware { 22 | 23 | var logger *zap.Logger 24 | 25 | if args.Production == true { 26 | logger = zap.New(otelzap.NewOtelCore(args.LoggerProvider)) 27 | zap.ReplaceGlobals(logger) 28 | logger.Info("[Logger] Starting Logger with Prod Config") 29 | } else { 30 | logger, _ = zap.NewDevelopment() 31 | } 32 | 33 | return &LogMiddleware{logger: logger} 34 | } 35 | 36 | func (l *LogMiddleware) Logger(ctx context.Context) *zap.Logger { 37 | 38 | spanContext := trace.SpanContextFromContext(ctx) 39 | if !spanContext.IsValid() { 40 | return l.logger 41 | } 42 | 43 | return l.logger.With( 44 | zap.String("trace_id", spanContext.TraceID().String()), 45 | zap.String("span_id", spanContext.SpanID().String()), 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /frontend/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import type { NextRequest } from 'next/server' 3 | 4 | // Default middleware for self-hosted mode 5 | function selfHostedMiddleware(request: NextRequest) { 6 | return NextResponse.next(); 7 | } 8 | 9 | // Create the middleware based on hosting mode 10 | const middleware = () => { 11 | if (process.env.NEXT_PUBLIC_SELF_HOSTING === 'true') { 12 | return selfHostedMiddleware; 13 | } 14 | 15 | // Only import and use Clerk in non-self-hosted mode 16 | const { clerkMiddleware, createRouteMatcher } = require("@clerk/nextjs/server"); 17 | const isPublicRoute = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)', '/']) 18 | 19 | return clerkMiddleware((auth: any, request: any) => { 20 | if (!isPublicRoute(request)) { 21 | auth().protect() 22 | } 23 | }); 24 | } 25 | 26 | export default middleware(); 27 | 28 | export const config = { 29 | matcher: [ 30 | // Skip Next.js internals and all static files, unless found in search params 31 | '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', 32 | // Always run for API routes 33 | '/(api|trpc)(.*)', 34 | ], 35 | }; 36 | -------------------------------------------------------------------------------- /frontend/docker/production/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Node.js image as the base 2 | FROM node:18 3 | 4 | # Set the working directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy package.json, package-lock.json and other config files to the container 8 | COPY . . 9 | 10 | ARG NEXT_PUBLIC_API_BASE_URL 11 | ARG NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY 12 | 13 | ARG NEXT_SERVER_API_BASE_URL 14 | ARG NEXT_PUBLIC_POSTHOG_KEY 15 | ARG NEXT_PUBLIC_POSTHOG_HOST 16 | ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY 17 | 18 | # Install dependencies 19 | RUN npm ci --loglevel verbose 20 | 21 | # Copy the app source code to the container 22 | COPY . . 23 | 24 | ENV NODE_ENV=production 25 | 26 | ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL 27 | ENV NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY 28 | ENV NEXT_SERVER_API_BASE_URL=$NEXT_SERVER_API_BASE_URL 29 | ENV NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY 30 | ENV NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST 31 | ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY 32 | 33 | # Build the Next.js app 34 | RUN npm run build --loglevel verbose 35 | 36 | # Expose the port the app will run on 37 | EXPOSE 3000 38 | 39 | # Start the app 40 | CMD ["npm", "start"] 41 | -------------------------------------------------------------------------------- /frontend/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /frontend/app/console/[teamSlug]/project/[projectSlug]/gql.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const GET_PROJECT_INFO = gql` 4 | query GetProjectInfo($teamSlug: String!, $projectSlug: String!) { 5 | teams(teamSlug: $teamSlug) { 6 | subscriptionPlans { 7 | id 8 | stripeSubscriptionId 9 | subscriptionData { 10 | currentPeriodStart 11 | currentPeriodEnd 12 | status 13 | interval 14 | planName 15 | costInUsd 16 | lastFourCardDigits 17 | resourcesIncluded 18 | resourcesUsed 19 | } 20 | } 21 | projects(projectSlug: $projectSlug) { 22 | projectSlug 23 | projectName 24 | accountConnections { 25 | externalId 26 | accountId 27 | } 28 | scans { 29 | scanId 30 | scanCompleted 31 | created 32 | serviceCount 33 | regionCount 34 | resourceCost 35 | } 36 | } 37 | } 38 | } 39 | ` 40 | 41 | export const START_SCAN = gql` 42 | mutation StartScan($teamSlug: String!, $projectSlug: String!, $regions: [String!]!, $services: [String!]!) { 43 | startScan(teamSlug: $teamSlug, projectSlug: $projectSlug, services: $services, regions: $regions) 44 | } 45 | `; 46 | -------------------------------------------------------------------------------- /backend/httpmiddleware/main.go: -------------------------------------------------------------------------------- 1 | package httpmiddleware 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | type HttpRequestStruct struct { 10 | Method string 11 | Url string 12 | Body io.Reader 13 | Headers map[string]string 14 | } 15 | 16 | func HttpRequest(args HttpRequestStruct) ([]byte, error) { 17 | req, err := http.NewRequest(args.Method, args.Url, args.Body) 18 | if err != nil { 19 | return nil, fmt.Errorf("Failed to create request: " + err.Error()) 20 | } 21 | 22 | for key, val := range args.Headers { 23 | req.Header.Set(key, val) 24 | } 25 | 26 | client := &http.Client{} 27 | 28 | res, err := client.Do(req) 29 | if err != nil { 30 | return nil, fmt.Errorf("Failed to fetch response: " + err.Error()) 31 | } 32 | 33 | defer res.Body.Close() 34 | 35 | responseBody, err := io.ReadAll(res.Body) 36 | 37 | // Error out if response code is not 200 or 202. 38 | // But what if the response code is okay but not equal to 200 or 202? 39 | if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusAccepted && res.StatusCode != http.StatusCreated { 40 | return nil, fmt.Errorf("Request failed: %d %s", res.StatusCode, responseBody) 41 | } 42 | 43 | if err != nil { 44 | return nil, fmt.Errorf("Failed to read response body: " + err.Error()) 45 | } 46 | 47 | return responseBody, nil 48 | } 49 | -------------------------------------------------------------------------------- /frontend/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 32 | -------------------------------------------------------------------------------- /backend/modelapi/grokapi/main_test.go: -------------------------------------------------------------------------------- 1 | package grokapi 2 | 3 | import ( 4 | "context" 5 | "guarddev/logger" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | func TestSummarizeFindings(t *testing.T) { 13 | // Initialize logger 14 | logMiddleware := logger.Connect(logger.LoggerConnectProps{Production: false}) 15 | ctx := context.Background() 16 | log := logMiddleware.Logger(ctx) 17 | 18 | // Create Grok client 19 | grokClient := Connect(context.Background(), GrokConnectProps{ 20 | Logger: logMiddleware, 21 | }) 22 | 23 | // Test data 24 | service := "s3" 25 | region := "us-east-1" 26 | accountId := "123456789012" 27 | findings := []string{ 28 | "S3 bucket 'example-bucket' has public access enabled through its bucket policy", 29 | "S3 bucket 'example-bucket' does not have encryption enabled", 30 | "S3 bucket 'example-bucket' does not have versioning enabled", 31 | } 32 | 33 | // Make the API call 34 | log.Info("Making API call to Grok...") 35 | result, err := grokClient.SummarizeFindings(context.Background(), service, region, accountId, findings) 36 | 37 | // Assert no error occurred 38 | assert.NoError(t, err) 39 | 40 | // Assert the result is not nil 41 | assert.NotNil(t, result) 42 | 43 | log.Info("=== Test Results ===", zap.String("Summary", result.Summary), zap.String("Remediation", result.Remedies), zap.Strings("Commmands", result.Commands)) 44 | } 45 | -------------------------------------------------------------------------------- /frontend/apollo-client.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, InMemoryCache, from } from '@apollo/client'; 2 | import { setContext } from '@apollo/client/link/context'; 3 | import { createUploadLink } from 'apollo-upload-client'; 4 | 5 | export const GetApolloClient = (ssrMode: boolean, getToken: any) => { 6 | const apiServer = 7 | process.env.NEXT_SERVER_API_BASE_URL || 8 | process.env.NEXT_PUBLIC_API_BASE_URL; 9 | 10 | const vercel_environent = 11 | process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV; 12 | 13 | const vercel_env = vercel_environent ? vercel_environent : 'development'; 14 | const selfHosting = process.env.NEXT_PUBLIC_SELF_HOSTING === 'true'; 15 | 16 | const httpLink: any = createUploadLink({ uri: apiServer }); 17 | 18 | const authMiddleware = setContext(async (_, { headers }) => { 19 | if (selfHosting) { 20 | return { 21 | headers: { 22 | ...headers, 23 | authorization: 'Bearer self-hosted', 24 | vercel_env, 25 | }, 26 | }; 27 | } 28 | 29 | const token = await getToken({ template: 'Guard_GQL_Server' }); 30 | return { 31 | headers: { 32 | ...headers, 33 | authorization: token ? `Bearer ${token}` : '', 34 | vercel_env, 35 | }, 36 | }; 37 | }); 38 | return new ApolloClient({ 39 | ssrMode, 40 | link: from([authMiddleware, httpLink]), 41 | cache: new InMemoryCache(), 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Server Side Public License (SSPL) Version 1.0 2 | 3 | Copyright (C) 2024 Guard.dev 4 | 5 | This program is free software: you can redistribute it and/or modify it under the terms of the Server Side Public License, version 1, as published by MongoDB, Inc. 6 | 7 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Server Side Public License for more details. 8 | 9 | You should have received a copy of the Server Side Public License along with this program. If not, see . 10 | 11 | ## Terms 12 | 13 | 1. **Usage**: You are free to use, modify, and self-host this software. However, you may not offer the software as a service to third parties without complying with Section 13 of the SSPL, which requires making your modifications publicly available. 14 | 15 | 2. **Redistribution**: If you modify this software and offer it as a publicly hosted service, you must make the entire source code, including your modifications, available under the SSPL. 16 | 17 | 3. **Commercial Use**: Commercial use of this software is allowed as long as it is not provided as a service. To provide the software as a service, you must obtain a commercial license or comply with Section 13. 18 | 19 | For more information, please refer to the full text of the Server Side Public License at . 20 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /frontend/components/Icons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface IconProps { 4 | className?: string; 5 | } 6 | 7 | export const IconCloudSecurity: React.FC = ({ className }) => ( 8 | 9 | 10 | 11 | ); 12 | 13 | export const IconAI: React.FC = ({ className }) => ( 14 | 15 | 16 | 17 | ); 18 | 19 | export const IconRealTime: React.FC = ({ className }) => ( 20 | 21 | 22 | 23 | 24 | ); 25 | 26 | export const IconActionable: React.FC = ({ className }) => ( 27 | 28 | 29 | 30 | ); 31 | 32 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "guard-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "generate": "graphql-codegen" 11 | }, 12 | "dependencies": { 13 | "@apollo/client": "^3.11.8", 14 | "@clerk/nextjs": "^5.6.2", 15 | "@radix-ui/react-accordion": "^1.2.1", 16 | "@radix-ui/react-checkbox": "^1.1.2", 17 | "@radix-ui/react-dialog": "^1.1.2", 18 | "@radix-ui/react-dropdown-menu": "^2.1.1", 19 | "@radix-ui/react-icons": "^1.3.0", 20 | "@radix-ui/react-label": "^2.1.0", 21 | "@radix-ui/react-popover": "^1.1.1", 22 | "@radix-ui/react-select": "^2.1.1", 23 | "@radix-ui/react-separator": "^1.1.0", 24 | "@radix-ui/react-slot": "^1.1.0", 25 | "@radix-ui/react-tabs": "^1.1.0", 26 | "@stripe/stripe-js": "^4.9.0", 27 | "@tanstack/react-table": "^8.20.5", 28 | "@vercel/analytics": "^1.3.1", 29 | "apollo-upload-client": "^17.0.0", 30 | "class-variance-authority": "^0.7.0", 31 | "clsx": "^2.1.1", 32 | "cmdk": "^1.0.0", 33 | "graphql": "^16.9.0", 34 | "lucide-react": "^0.445.0", 35 | "next": "14.2.13", 36 | "next-themes": "^0.3.0", 37 | "posthog-js": "^1.176.0", 38 | "react": "^18", 39 | "react-dom": "^18", 40 | "react-icons": "^5.3.0", 41 | "tailwind-merge": "^2.5.2", 42 | "tailwindcss-animate": "^1.0.7" 43 | }, 44 | "devDependencies": { 45 | "@graphql-codegen/cli": "5.0.2", 46 | "@graphql-codegen/client-preset": "4.3.3", 47 | "@types/apollo-upload-client": "^17.0.5", 48 | "@types/node": "^20", 49 | "@types/react": "^18", 50 | "@types/react-dom": "^18", 51 | "eslint": "^8", 52 | "eslint-config-next": "14.2.13", 53 | "postcss": "^8", 54 | "tailwindcss": "^3.4.1", 55 | "typescript": "^5.6.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /frontend/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | import { ThemeProvider } from "@/components/theme-provider"; 6 | import { ApolloProviderWrapper } from "./ApolloProviderWrapper"; 7 | import { CSPostHogProvider } from "./providers/PostHogProvider"; 8 | 9 | import { Analytics } from "@vercel/analytics/react" 10 | 11 | const geistSans = localFont({ 12 | src: "./fonts/GeistVF.woff", 13 | variable: "--font-geist-sans", 14 | weight: "100 900", 15 | }); 16 | const geistMono = localFont({ 17 | src: "./fonts/GeistMonoVF.woff", 18 | variable: "--font-geist-mono", 19 | weight: "100 900", 20 | }); 21 | 22 | export const metadata: Metadata = { 23 | title: "Guard", 24 | description: "AI-Powered Cloud Security, Simplified.", 25 | }; 26 | 27 | export default function RootLayout({ 28 | children, 29 | }: Readonly<{ 30 | children: React.ReactNode; 31 | }>) { 32 | const selfHosting = process.env.NEXT_PUBLIC_SELF_HOSTING === 'true'; 33 | 34 | const content = ( 35 | 36 | 39 | 45 | 46 | {children} 47 | 48 | 49 | 50 | ); 51 | 52 | // Skip ClerkProvider and wrap directly with ApolloProvider for self-hosting 53 | if (selfHosting) { 54 | return {content}; 55 | } 56 | 57 | // Normal flow with Clerk authentication 58 | return ( 59 | 60 | 61 | {content} 62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /frontend/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | 8 | export const titleCase = (str: string) => { 9 | return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); 10 | }; 11 | 12 | 13 | export const getTimeDifference = ( 14 | unixTimestamp: number, 15 | full?: boolean, 16 | ): string => { 17 | const currentTime = Date.now(); 18 | const differenceInMillis = currentTime - unixTimestamp; // Convert seconds to milliseconds 19 | 20 | const differenceInMinutes = Math.floor(differenceInMillis / (1000 * 60)); 21 | const differenceInHours = Math.floor(differenceInMinutes / 60); 22 | const differenceInDays = Math.floor(differenceInHours / 24); 23 | 24 | if (differenceInDays >= 1) { 25 | if (full) { 26 | return `${differenceInDays} ${differenceInDays === 1 ? 'day' : 'days'}`; 27 | } 28 | return `${differenceInDays}d`; 29 | } else if (differenceInHours >= 1) { 30 | if (full) { 31 | return `${differenceInHours} ${differenceInHours === 1 ? 'hour' : 'hours'}`; 32 | } 33 | return `${differenceInHours}h`; 34 | } else { 35 | if (full) { 36 | return `${differenceInMinutes} ${differenceInMinutes === 1 ? 'minute' : 'minutes'}`; 37 | } 38 | return `${differenceInMinutes}m`; 39 | } 40 | }; 41 | 42 | export const formatTimestamp = (unixTimestamp: number) => { 43 | const date = new Date(unixTimestamp); 44 | 45 | const optionsDate: Intl.DateTimeFormatOptions = { 46 | month: 'short', 47 | day: 'numeric', 48 | }; 49 | const optionsTime: Intl.DateTimeFormatOptions = { 50 | hour: 'numeric', 51 | hour12: true, 52 | }; 53 | 54 | const formattedDate = date.toLocaleDateString('en-US', optionsDate); 55 | const formattedTime = date 56 | .toLocaleTimeString('en-US', optionsTime) 57 | .toLowerCase() 58 | .replace(/\s/g, ''); 59 | 60 | return `${formattedDate}, ${formattedTime}`; 61 | }; 62 | -------------------------------------------------------------------------------- /Dockerfile.selfhost: -------------------------------------------------------------------------------- 1 | # Build the frontend 2 | FROM node:18 AS frontend-builder 3 | 4 | ENV NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/graph 5 | ENV NEXT_PUBLIC_SELF_HOSTING=true 6 | ENV NEXT_WEBPACK_USEPOLLING=true 7 | 8 | WORKDIR /frontend 9 | COPY frontend/ . 10 | RUN npm ci --loglevel verbose 11 | RUN npm run build --loglevel verbose 12 | 13 | # Build the backend 14 | FROM golang:1.23 AS backend-builder 15 | 16 | WORKDIR /backend 17 | COPY backend/ . 18 | RUN go mod download 19 | RUN CGO_ENABLED=0 GOOS=linux go build -o /guard-gql-server server.go 20 | 21 | # Final runtime image 22 | FROM postgres:17 AS postgres-base 23 | 24 | # Install additional dependencies 25 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ 26 | python3 \ 27 | python3-pip \ 28 | supervisor \ 29 | curl \ 30 | gnupg 31 | 32 | # Install Node.js and npm explicitly 33 | RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \ 34 | apt-get install -y nodejs 35 | 36 | # Copy PostgreSQL environment configuration 37 | ENV POSTGRES_DB=postgres 38 | ENV POSTGRES_USER=postgres 39 | ENV POSTGRES_PASSWORD=postgrespw 40 | ENV SELF_HOSTING=1 41 | ENV POSTGRES_DB_NAME=postgres 42 | ENV POSTGRES_DB_USER=postgres 43 | ENV POSTGRES_DB_PASS=postgrespw 44 | ENV POSTGRES_DB_HOST=127.0.0.1 45 | ENV POSTGRES_DB_PORT=5432 46 | 47 | # Copy frontend and backend artifacts 48 | WORKDIR /app 49 | COPY --from=frontend-builder /frontend /app/frontend 50 | COPY --from=backend-builder /backend /app/backend 51 | COPY --from=backend-builder /guard-gql-server /app/backend/ 52 | COPY --from=backend-builder backend/database/postgres/schema.sql /docker-entrypoint-initdb.d/schema.sql 53 | 54 | # Copy the startup script and Supervisor configuration 55 | COPY run_services.sh /app/run_services.sh 56 | RUN chmod +x /app/run_services.sh 57 | COPY supervisord.conf /etc/supervisor/supervisord.conf 58 | 59 | EXPOSE 80 8080 5432 3000 60 | 61 | # Start Supervisor to manage all services 62 | CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] 63 | -------------------------------------------------------------------------------- /frontend/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 border", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /frontend/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | TabsList.displayName = TabsPrimitive.List.displayName 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )) 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )) 53 | TabsContent.displayName = TabsPrimitive.Content.displayName 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent } 56 | -------------------------------------------------------------------------------- /frontend/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /frontend/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer utilities { 10 | .text-balance { 11 | text-wrap: balance; 12 | } 13 | } 14 | 15 | @layer base { 16 | :root { 17 | --background: 0 0% 100%; 18 | --foreground: 240 10% 3.9%; 19 | --card: 0 0% 100%; 20 | --card-foreground: 240 10% 3.9%; 21 | --popover: 0 0% 100%; 22 | --popover-foreground: 240 10% 3.9%; 23 | --primary: 240 5.9% 10%; 24 | --primary-foreground: 0 0% 98%; 25 | --secondary: 240 4.8% 95.9%; 26 | --secondary-foreground: 240 5.9% 10%; 27 | --muted: 240 4.8% 95.9%; 28 | --muted-foreground: 240 3.8% 46.1%; 29 | --accent: 240 4.8% 95.9%; 30 | --accent-foreground: 240 5.9% 10%; 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | --border: 240 5.9% 90%; 34 | --input: 240 5.9% 90%; 35 | --ring: 240 10% 3.9%; 36 | --chart-1: 12 76% 61%; 37 | --chart-2: 173 58% 39%; 38 | --chart-3: 197 37% 24%; 39 | --chart-4: 43 74% 66%; 40 | --chart-5: 27 87% 67%; 41 | --radius: 0.5rem; 42 | } 43 | .dark { 44 | --background: 240 10% 3.9%; 45 | --foreground: 0 0% 98%; 46 | --card: 240 10% 3.9%; 47 | --card-foreground: 0 0% 98%; 48 | --popover: 240 10% 3.9%; 49 | --popover-foreground: 0 0% 98%; 50 | --primary: 0 0% 98%; 51 | --primary-foreground: 240 5.9% 10%; 52 | --secondary: 240 3.7% 15.9%; 53 | --secondary-foreground: 0 0% 98%; 54 | --muted: 240 3.7% 15.9%; 55 | --muted-foreground: 240 5% 64.9%; 56 | --accent: 240 3.7% 15.9%; 57 | --accent-foreground: 0 0% 98%; 58 | --destructive: 0 62.8% 30.6%; 59 | --destructive-foreground: 0 0% 98%; 60 | --border: 240 3.7% 15.9%; 61 | --input: 240 3.7% 15.9%; 62 | --ring: 240 4.9% 83.9%; 63 | --chart-1: 220 70% 50%; 64 | --chart-2: 160 60% 45%; 65 | --chart-3: 30 80% 55%; 66 | --chart-4: 280 65% 60%; 67 | --chart-5: 340 75% 55%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /frontend/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDownIcon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )) 55 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 56 | 57 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 58 | -------------------------------------------------------------------------------- /frontend/components/color-mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons" 5 | import { useTheme } from "next-themes" 6 | 7 | import { useEffect, useState } from 'react'; 8 | import { Moon, Sun } from 'lucide-react'; 9 | 10 | import { Button } from "@/components/ui/button" 11 | import { 12 | DropdownMenu, 13 | DropdownMenuContent, 14 | DropdownMenuItem, 15 | DropdownMenuTrigger, 16 | } from "@/components/ui/dropdown-menu" 17 | 18 | export function ModeToggle() { 19 | const { setTheme } = useTheme() 20 | 21 | return ( 22 | 23 | 24 | 29 | 30 | 31 | setTheme("light")}> 32 | Light 33 | 34 | setTheme("dark")}> 35 | Dark 36 | 37 | setTheme("system")}> 38 | System 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | 46 | export const DarkModeToggle = () => { 47 | const { setTheme, resolvedTheme } = useTheme(); 48 | const [mounted, setMounted] = useState(false); 49 | 50 | useEffect(() => { 51 | // When the component mounts on the client, update the state to indicate it is mounted 52 | setMounted(true); 53 | }, []); 54 | 55 | const toggleDarkMode = () => { 56 | setTheme(resolvedTheme === 'dark' ? 'light' : 'dark'); 57 | }; 58 | 59 | // Render nothing on the server 60 | if (!mounted) return null; 61 | 62 | // Once the component has mounted, we can safely render 63 | return ( 64 | 71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /start: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | import subprocess 6 | 7 | # Default arguments 8 | default_args = ["production", "all", "up"] 9 | 10 | # If no arguments are given, use the default arguments 11 | if len(sys.argv) == 1: 12 | sys.argv.extend(default_args) 13 | elif len(sys.argv) < 3: 14 | print("Usage: ./script.py <...docker-compose args>") 15 | print("Example: ./start development all up -d") 16 | print("Example: ./start production backend up -d") 17 | print("Example: ./start production backend down --remove-orphans") 18 | sys.exit(1) 19 | 20 | # Extract arguments 21 | environment = sys.argv[1] 22 | service_list_arg = sys.argv[2] 23 | 24 | # Environment configurations, if any 25 | ports = { 26 | "development": { 27 | "backend": "8080", 28 | "frontend": "3000", 29 | }, 30 | "production": { 31 | "backend": "8080", 32 | "frontend": "3000", 33 | }, 34 | } 35 | 36 | # Service to docker-compose file mapping 37 | service_files = { 38 | "backend": "./docker-compose.backend.yml", 39 | "frontend": "./docker-compose.frontend.yml", 40 | #"kafka": "./docker-compose.kafka.yml", 41 | } 42 | 43 | # Set environment variables based on the environment 44 | backend_port = ports[environment]["backend"] 45 | frontend_port = ports[environment]["frontend"] 46 | os.environ["ENVIRONMENT"] = environment 47 | os.environ["BACKEND_PORT"] = backend_port 48 | os.environ["FRONTEND_PORT"] = frontend_port 49 | 50 | # Prepare the docker-compose command 51 | docker_compose_command = [ 52 | "docker", 53 | "compose", 54 | "-f", "./docker-compose.yml", 55 | "--profile", environment 56 | ] 57 | 58 | # Add service-specific compose files if 'all' isn't specified 59 | if service_list_arg != "all": 60 | services = service_list_arg.split(',') 61 | for service in services: 62 | if service in service_files: 63 | docker_compose_command += ["-f", service_files[service]] 64 | else: 65 | # Include all service-specific compose files if 'all' is specified 66 | for file in service_files.values(): 67 | docker_compose_command += ["-f", file] 68 | 69 | # Append the remaining docker-compose arguments 70 | docker_compose_command += sys.argv[3:] 71 | 72 | print("Executing the following docker compose command") 73 | print(" ".join(docker_compose_command)) 74 | 75 | # Execute the command 76 | subprocess.run(docker_compose_command) 77 | -------------------------------------------------------------------------------- /frontend/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))' 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))' 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))' 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))' 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))' 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))' 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))' 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))' 52 | } 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)' 58 | }, 59 | keyframes: { 60 | 'accordion-down': { 61 | from: { 62 | height: '0' 63 | }, 64 | to: { 65 | height: 'var(--radix-accordion-content-height)' 66 | } 67 | }, 68 | 'accordion-up': { 69 | from: { 70 | height: 'var(--radix-accordion-content-height)' 71 | }, 72 | to: { 73 | height: '0' 74 | } 75 | } 76 | }, 77 | animation: { 78 | 'accordion-down': 'accordion-down 0.2s ease-out', 79 | 'accordion-up': 'accordion-up 0.2s ease-out' 80 | } 81 | } 82 | }, 83 | plugins: [require("tailwindcss-animate")], 84 | }; 85 | export default config; 86 | -------------------------------------------------------------------------------- /frontend/app/console/[teamSlug]/project/[projectSlug]/stripe_checkout_form.tsx: -------------------------------------------------------------------------------- 1 | import PricingTable from '@/components/PricingTable'; 2 | import { Button } from '@/components/ui/button'; 3 | import { Dialog, DialogContent } from '@/components/ui/dialog'; 4 | import { useMutation, gql } from '@apollo/client'; 5 | import { useRouter } from 'next/navigation'; 6 | import { useState } from 'react'; 7 | 8 | const CREATE_STRIPE_PORTAL = gql` 9 | mutation CreateStripePortalSession($teamSlug: String!) { 10 | createPortalSession(teamSlug: $teamSlug) { 11 | sessionUrl 12 | } 13 | } 14 | `; 15 | 16 | interface StripeCheckoutFormProps { 17 | teamSlug: string; 18 | subscriptionActive: boolean; 19 | handleCheckout: (lookupKey: string) => void; 20 | } 21 | 22 | const StripeCheckoutForm: React.FC = ({ 23 | teamSlug, 24 | subscriptionActive, 25 | handleCheckout, 26 | }) => { 27 | const router = useRouter(); 28 | const [createPortalSession, { loading: portalLoading, error: portalError }] = 29 | useMutation(CREATE_STRIPE_PORTAL); 30 | 31 | const handlePortalSession = async () => { 32 | const response = await createPortalSession({ variables: { teamSlug } }); 33 | const sessionUrl = response.data?.createPortalSession?.sessionUrl; 34 | if (sessionUrl) { 35 | router.push(sessionUrl); 36 | } else { 37 | console.log('[stripe error]', portalError?.message); 38 | } 39 | }; 40 | 41 | const [open, setOpen] = useState(false); 42 | 43 | return ( 44 |
45 | {!subscriptionActive && ( 46 | 49 | )} 50 | 51 | setOpen(e)}> 52 | 53 | 54 | 55 | 56 | 57 | {subscriptionActive && ( 58 | 65 | )} 66 | 67 | {subscriptionActive && ( 68 | 76 | )} 77 |
78 | ); 79 | }; 80 | 81 | export default StripeCheckoutForm; 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Guard.dev 2 | 3 | Thank you for your interest in contributing to Guard.dev! We welcome all kinds of contributions, whether it's improving our documentation, fixing bugs, or adding new features. This guide will help you get started. 4 | 5 | ## How Can You Contribute? 6 | 7 | ### 1. Reporting Bugs 8 | 9 | If you encounter any issues while using Guard.dev, please report them by [creating a new issue](https://github.com/guard-dev/guard/issues). Make sure to include: 10 | 11 | - A clear description of the problem. 12 | - Steps to reproduce the issue. 13 | - Any relevant logs or screenshots. 14 | 15 | ### 2. Suggesting Enhancements 16 | 17 | We are always looking to improve Guard.dev! If you have ideas for new features or improvements, please submit them by [creating a new issue](https://github.com/guard-dev/guard/issues) and tagging it as an enhancement. 18 | 19 | ### 3. Submitting Pull Requests 20 | 21 | We appreciate your help in fixing bugs or adding new features. Follow these steps to contribute code: 22 | 23 | 1. **Fork the Repository** 24 | - Go to [https://github.com/guard-dev/guard](https://github.com/guard-dev/guard) and click "Fork". 25 | 26 | 2. **Clone the Forked Repository** 27 | 28 | ```bash 29 | git clone https://github.com/yourusername/guard.git 30 | cd guard 31 | ``` 32 | 33 | 3. **Create a Branch** 34 | 35 | ```bash 36 | git checkout -b feature/your-feature-name 37 | ``` 38 | 39 | 4. **Make Your Changes** 40 | - Make the necessary changes or additions to the code. 41 | - Make sure to write tests for any new functionality. 42 | 43 | 5. **Commit Your Changes** 44 | 45 | ```bash 46 | git commit -m "Add feature: your feature name" 47 | ``` 48 | 49 | 6. **Push to Your Fork** 50 | 51 | ```bash 52 | git push origin feature/your-feature-name 53 | ``` 54 | 55 | 7. **Create a Pull Request** 56 | - Go to your forked repository on GitHub. 57 | - Click on "Compare & pull request". 58 | - Provide a clear description of the changes and reference any relevant issues. 59 | 60 | ### 4. Code Style 61 | 62 | - Please follow the existing coding style used in the project. 63 | - Ensure your code is properly formatted and includes comments where necessary. 64 | 65 | ### 5. Writing Tests 66 | 67 | - Add tests for all new functionality to ensure that your changes do not break existing features. 68 | - Use the existing test suites as a reference. 69 | 70 | ## Code of Conduct 71 | 72 | We are committed to fostering a welcoming and respectful community. Please read and adhere to our [Code of Conduct](CODE_OF_CONDUCT.md). 73 | 74 | ## Need Help? 75 | 76 | If you have questions, feel free to reach out by [opening an issue](https://github.com/guard-dev/guard/issues) or contacting us via [support@guard.dev](mailto:support@guard.dev). 77 | 78 | Thank you for contributing to Guard.dev! 79 | -------------------------------------------------------------------------------- /frontend/app/console/[teamSlug]/project/[projectSlug]/combobox.tsx: -------------------------------------------------------------------------------- 1 | import { Check, ChevronsUpDown } from "lucide-react" 2 | 3 | import { cn } from "@/lib/utils" 4 | import { Button } from "@/components/ui/button" 5 | import { 6 | Command, 7 | CommandEmpty, 8 | CommandGroup, 9 | CommandInput, 10 | CommandItem, 11 | CommandList, 12 | } from "@/components/ui/command" 13 | import { 14 | Popover, 15 | PopoverContent, 16 | PopoverTrigger, 17 | } from "@/components/ui/popover" 18 | import { Dispatch, SetStateAction, useState } from "react" 19 | 20 | 21 | export function ComboboxDemo({ availableOptions, value, setValue }: { availableOptions: { value: string; label: string; }[]; value: string[]; setValue: Dispatch>; }) { 22 | const [open, setOpen] = useState(false) 23 | 24 | const handleSetValue = (val: string) => { 25 | if (value.includes(val)) { 26 | value.splice(value.indexOf(val), 1); 27 | setValue(value.filter((item) => item !== val)); 28 | } else { 29 | setValue(prevValue => [...prevValue, val]); 30 | } 31 | } 32 | 33 | return ( 34 | 35 | 36 | 51 | 52 | 53 | 54 | 55 | No framework found. 56 | 57 | 58 | {availableOptions.map((opt) => ( 59 | { 63 | handleSetValue(currVal); 64 | }}> 65 | 70 | {opt.label} 71 | 72 | ))} 73 | 74 | 75 | 76 | 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /backend/graph/schema.graphqls: -------------------------------------------------------------------------------- 1 | # GraphQL schema example 2 | # 3 | # https://gqlgen.com/getting-started/ 4 | # 5 | 6 | scalar DateTime 7 | scalar Upload 8 | scalar Int64 9 | 10 | directive @loggedIn on FIELD_DEFINITION 11 | directive @isAdmin on FIELD_DEFINITION 12 | directive @memberTeam on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION 13 | directive @subActive on FIELD_DEFINITION 14 | 15 | type Team { 16 | teamSlug: String! 17 | teamName: String! 18 | projects(projectSlug: String): [Project!]! 19 | members: [TeamMembership!]! 20 | 21 | subscriptionPlans(subscriptionId: Int64): [SubscriptionPlan!]! 22 | } 23 | 24 | type SubscriptionPlan { 25 | id: Int64! 26 | teamId: Int64! 27 | stripeSubscriptionId: String 28 | subscriptionData: SubscriptionData 29 | } 30 | 31 | type SubscriptionData { 32 | currentPeriodStart: DateTime! 33 | currentPeriodEnd: DateTime! 34 | status: String! 35 | interval: String! 36 | planName: String! 37 | costInUsd: Int64! 38 | lastFourCardDigits: String! 39 | resourcesIncluded: Int! 40 | resourcesUsed: Int! 41 | } 42 | 43 | type Project { 44 | projectSlug: String! 45 | projectName: String! 46 | accountConnections: [AccountConnection!]! 47 | scans(scanId: String): [Scan!]! 48 | } 49 | 50 | type AccountConnection { 51 | externalId: String! 52 | accountId: String! 53 | } 54 | 55 | type Scan { 56 | scanId: String! 57 | scanItems(scanItemId: Int64): [ScanItem!]! 58 | scanCompleted: Boolean! 59 | created: Int64! 60 | regionCount: Int! 61 | serviceCount: Int! 62 | resourceCost: Int! 63 | } 64 | 65 | type ScanItem { 66 | scanItemId: Int64! 67 | service: String! 68 | region: String! 69 | findings: [String!]! 70 | summary: String! 71 | remedy: String! 72 | scanItemEntries: [ScanItemEntry!]! 73 | resourceCost: Int! 74 | } 75 | 76 | type ScanItemEntry { 77 | findings: [String!]! 78 | title: String! 79 | summary: String! 80 | remedy: String! 81 | commands: [String!]! 82 | resourceCost: Int! 83 | } 84 | 85 | type Userinfo { 86 | userId: Int64! 87 | email: String! 88 | fullName: String! 89 | } 90 | 91 | type TeamMembership { 92 | membershipType: String! 93 | user: Userinfo! 94 | teamId: Int64! 95 | teamSlug: String! 96 | teamName: String! 97 | } 98 | 99 | type Query { 100 | teams(teamSlug: String @memberTeam): [Team!]! @loggedIn 101 | getExternalId: String! @loggedIn 102 | } 103 | 104 | type Mutation { 105 | createProject(teamSlug: String! @memberTeam, input: NewProject!): Project! @loggedIn 106 | verifyAccountId(accountId: String!): Boolean! @loggedIn 107 | startScan(teamSlug: String! @memberTeam, projectSlug: String!, services: [String!]!, regions: [String!]!): String! @subActive 108 | 109 | createCheckoutSession(teamSlug: String! @memberTeam, lookUpKey: String!): CheckoutSessionResponse! @loggedIn 110 | createPortalSession(teamSlug: String! @memberTeam): PortalSessionResponse! @loggedIn 111 | } 112 | 113 | type CheckoutSessionResponse { 114 | sessionId: String! 115 | } 116 | 117 | type PortalSessionResponse { 118 | sessionUrl: String! 119 | } 120 | 121 | input NewProject { 122 | projectName: String! 123 | } 124 | -------------------------------------------------------------------------------- /backend/utils/main.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "compress/zlib" 7 | "fmt" 8 | "io/ioutil" 9 | "math" 10 | "net/url" 11 | "os/exec" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // Used for retry logic. 17 | func GetExponentialDelaySeconds(retryNumber int) int { 18 | delayTime := int(5 * math.Pow(2, float64(retryNumber))) 19 | return delayTime 20 | } 21 | 22 | // Wrapper for running commands that my not print out stderr normally otherwise 23 | func ExecCommand(command string) (string, error) { 24 | cmd := exec.Command("sh", "-c", command) 25 | 26 | var stderr bytes.Buffer 27 | cmd.Stderr = &stderr 28 | output, err := cmd.Output() 29 | errMsg := stderr.String() 30 | 31 | if err != nil { 32 | return "", fmt.Errorf("Command failed: %s, %s", err.Error(), errMsg) 33 | } 34 | return string(output), nil 35 | } 36 | 37 | func ConvertUnixToTime(unixVal int64) time.Time { 38 | epochMillis := unixVal 39 | seconds := epochMillis / 1000 40 | nanoseconds := (epochMillis % 1000) * 1000000 41 | timeObj := time.Unix(seconds, nanoseconds) 42 | return timeObj 43 | } 44 | 45 | // function to uncompress a gzip blob 46 | func UncompressGzip(blob []byte) ([]byte, error) { 47 | buf := bytes.NewBuffer(blob) 48 | r, err := gzip.NewReader(buf) 49 | if err != nil { 50 | return nil, err 51 | } 52 | defer r.Close() 53 | return ioutil.ReadAll(r) 54 | } 55 | 56 | func UnpackEventZlib(blob []byte) ([]byte, error) { 57 | reader, err := zlib.NewReader(bytes.NewReader(blob)) 58 | if err != nil { 59 | return nil, err 60 | } 61 | defer reader.Close() 62 | 63 | return ioutil.ReadAll(reader) 64 | } 65 | 66 | // Helper function to check if the data is gzipped 67 | func IsGzipped(data []byte) bool { 68 | return len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b 69 | } 70 | 71 | // Convert a 2D matrix to a 1D list of unique entries 72 | func FlattenUniqueEntries(matrix [][]string) []string { 73 | uniqueMap := make(map[string]bool) 74 | var uniqueList []string 75 | 76 | for _, row := range matrix { 77 | for _, entry := range row { 78 | if _, found := uniqueMap[entry]; !found { 79 | uniqueMap[entry] = true 80 | uniqueList = append(uniqueList, entry) 81 | } 82 | } 83 | } 84 | 85 | return uniqueList 86 | } 87 | 88 | // Convert a list of urls to a list of unique endpoints. 89 | func GetUniqueEndpoints(urls []string) []string { 90 | endpointSet := make(map[string]struct{}) 91 | 92 | for _, rawURL := range urls { 93 | parsedURL, err := url.Parse(rawURL) 94 | if err != nil { 95 | fmt.Println("Error parsing URL:", err) 96 | continue 97 | } 98 | 99 | endpoint := strings.TrimRight(parsedURL.Path, "/") 100 | endpointSet[endpoint] = struct{}{} 101 | } 102 | 103 | uniqueEndpoints := make([]string, 0, len(endpointSet)) 104 | for endpoint := range endpointSet { 105 | uniqueEndpoints = append(uniqueEndpoints, endpoint) 106 | } 107 | 108 | return uniqueEndpoints 109 | } 110 | 111 | // remove empty strings from a list 112 | func RemoveEmptyEntries(input []string) []string { 113 | var result []string 114 | for _, str := range input { 115 | trimmed := strings.TrimSpace(str) 116 | if trimmed != "" && trimmed != "about:blank" && trimmed != "http://" && trimmed != "https://" { 117 | result = append(result, trimmed) 118 | } 119 | } 120 | return result 121 | } 122 | -------------------------------------------------------------------------------- /frontend/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )) 52 | TableFooter.displayName = "TableFooter" 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )) 67 | TableRow.displayName = "TableRow" 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
[role=checkbox]]:translate-y-[2px]", 77 | className 78 | )} 79 | {...props} 80 | /> 81 | )) 82 | TableHead.displayName = "TableHead" 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | [role=checkbox]]:translate-y-[2px]", 92 | className 93 | )} 94 | {...props} 95 | /> 96 | )) 97 | TableCell.displayName = "TableCell" 98 | 99 | const TableCaption = React.forwardRef< 100 | HTMLTableCaptionElement, 101 | React.HTMLAttributes 102 | >(({ className, ...props }, ref) => ( 103 |
108 | )) 109 | TableCaption.displayName = "TableCaption" 110 | 111 | export { 112 | Table, 113 | TableHeader, 114 | TableBody, 115 | TableFooter, 116 | TableHead, 117 | TableRow, 118 | TableCell, 119 | TableCaption, 120 | } 121 | -------------------------------------------------------------------------------- /templates/guard-self-host-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "account:Get*" 8 | ], 9 | "Resource": "*" 10 | }, 11 | { 12 | "Effect": "Allow", 13 | "Action": [ 14 | "apigateway:GET" 15 | ], 16 | "Resource": "*" 17 | }, 18 | { 19 | "Effect": "Allow", 20 | "Action": [ 21 | "cloudtrail:GetInsightSelectors" 22 | ], 23 | "Resource": "*" 24 | }, 25 | { 26 | "Effect": "Allow", 27 | "Action": [ 28 | "dynamodb:ListTables", 29 | "dynamodb:DescribeTable", 30 | "dynamodb:DescribeContinuousBackups", 31 | "dynamodb:DescribeTimeToLive" 32 | ], 33 | "Resource": "*" 34 | }, 35 | { 36 | "Effect": "Allow", 37 | "Action": [ 38 | "ec2:DescribeInstances", 39 | "ec2:DescribeVolumes", 40 | "ec2:DescribeSecurityGroups", 41 | "ec2:DescribeAddresses", 42 | "ec2:DescribeInstanceAttribute", 43 | "ec2:DescribeVolumesModifications", 44 | "ec2:DescribeInstanceStatus" 45 | ], 46 | "Resource": "*" 47 | }, 48 | { 49 | "Effect": "Allow", 50 | "Action": [ 51 | "ecr:Describe*" 52 | ], 53 | "Resource": "*" 54 | }, 55 | { 56 | "Effect": "Allow", 57 | "Action": [ 58 | "ecs:ListClusters", 59 | "ecs:DescribeTaskDefinition", 60 | "ecs:ListTaskDefinitions", 61 | "ecs:DescribeTasks", 62 | "ecs:DescribeContainerInstances", 63 | "ecs:ListServices", 64 | "ecs:DescribeServices" 65 | ], 66 | "Resource": "*" 67 | }, 68 | { 69 | "Effect": "Allow", 70 | "Action": [ 71 | "glue:GetConnections", 72 | "glue:GetSecurityConfiguration*" 73 | ], 74 | "Resource": "*" 75 | }, 76 | { 77 | "Effect": "Allow", 78 | "Action": [ 79 | "iam:GetRole", 80 | "iam:List*", 81 | "iam:GetPolicy", 82 | "iam:GetPolicyVersion", 83 | "iam:GetAccountSummary", 84 | "iam:GetAccessKeyLastUsed", 85 | "iam:GetLoginProfile" 86 | ], 87 | "Resource": "*" 88 | }, 89 | { 90 | "Effect": "Allow", 91 | "Action": [ 92 | "lambda:ListFunctions", 93 | "lambda:GetFunction*", 94 | "lambda:GetPolicy", 95 | "lambda:GetLayerVersion", 96 | "lambda:ListTags" 97 | ], 98 | "Resource": "*" 99 | }, 100 | { 101 | "Effect": "Allow", 102 | "Action": [ 103 | "logs:FilterLogEvents", 104 | "logs:DescribeLogGroups" 105 | ], 106 | "Resource": "*" 107 | }, 108 | { 109 | "Effect": "Allow", 110 | "Action": [ 111 | "macie2:GetMacieSession" 112 | ], 113 | "Resource": "*" 114 | }, 115 | { 116 | "Effect": "Allow", 117 | "Action": [ 118 | "s3:ListAllMyBuckets", 119 | "s3:Get*" 120 | ], 121 | "Resource": "*" 122 | }, 123 | { 124 | "Effect": "Allow", 125 | "Action": [ 126 | "securityhub:GetFindings" 127 | ], 128 | "Resource": "*" 129 | }, 130 | { 131 | "Effect": "Allow", 132 | "Action": [ 133 | "ssm:GetDocument", 134 | "ssm-incidents:List*" 135 | ], 136 | "Resource": "*" 137 | }, 138 | { 139 | "Effect": "Allow", 140 | "Action": [ 141 | "tag:GetTagKeys" 142 | ], 143 | "Resource": "*" 144 | } 145 | ] 146 | } 147 | -------------------------------------------------------------------------------- /backend/database/postgres/main.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "guarddev/logger" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | _ "github.com/lib/pq" 13 | "golang.org/x/text/cases" 14 | "golang.org/x/text/language" 15 | 16 | "go.opentelemetry.io/otel" 17 | "go.uber.org/zap" 18 | ) 19 | 20 | type DatabaseConnectProps struct { 21 | Logger *logger.LogMiddleware 22 | } 23 | 24 | type Database struct { 25 | Queries 26 | logger *logger.LogMiddleware 27 | } 28 | 29 | func Connect(ctx context.Context, args DatabaseConnectProps) *Database { 30 | tracer := otel.Tracer("postgres/Connect") 31 | ctx, span := tracer.Start(ctx, "Connect") 32 | defer span.End() 33 | 34 | connectRetries := 5 35 | var conn *sql.DB 36 | var err error 37 | var connStr string 38 | 39 | logger := args.Logger.Logger(ctx) 40 | 41 | for connectRetries > 0 { 42 | conn, err, connStr = getConnection(ctx) 43 | if err == nil { 44 | logger.Info("[Postgres] Database client started") 45 | break 46 | } 47 | connectRetries -= 1 48 | sleepTime := 5 49 | logger.Error( 50 | "[Postgres] Could not connect to Postgres. Retrying after sleeping.", 51 | zap.Error(err), 52 | zap.Int("Retries Left", connectRetries), 53 | zap.Int("Sleep Time", sleepTime), 54 | zap.String("Connection String", connStr)) 55 | time.Sleep(time.Second * time.Duration(sleepTime)) 56 | } 57 | 58 | if connectRetries <= 0 { 59 | logger.Error("[Postgres] Failed to Connect to Postgres") 60 | span.RecordError(fmt.Errorf("failed to connect to Postgres")) 61 | os.Exit(1) 62 | } 63 | 64 | queries := New(conn) 65 | return &Database{Queries: *queries, logger: args.Logger} 66 | } 67 | 68 | func getConnection(ctx context.Context) (*sql.DB, error, string) { 69 | tracer := otel.Tracer("postgres/getConnection") 70 | _, span := tracer.Start(ctx, "getConnection") 71 | defer span.End() 72 | 73 | host := os.Getenv("POSTGRES_DB_HOST") 74 | port := os.Getenv("POSTGRES_DB_PORT") 75 | user := os.Getenv("POSTGRES_DB_USER") 76 | password := os.Getenv("POSTGRES_DB_PASS") 77 | dbname := os.Getenv("POSTGRES_DB_NAME") 78 | 79 | sslMode := "disable" 80 | 81 | postgresqlDbInfo := fmt.Sprintf( 82 | "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", 83 | host, port, user, password, dbname, sslMode, 84 | ) 85 | 86 | db, err := sql.Open("postgres", postgresqlDbInfo) 87 | if err != nil { 88 | span.RecordError(err) 89 | return nil, err, postgresqlDbInfo 90 | } 91 | err = db.Ping() 92 | if err != nil { 93 | span.RecordError(err) 94 | return nil, err, postgresqlDbInfo 95 | } 96 | 97 | return db, nil, "" 98 | } 99 | 100 | type SetupNewUserProps struct { 101 | EmailAddr string 102 | FullName string 103 | } 104 | 105 | func (d *Database) SetupNewUser(ctx context.Context, args SetupNewUserProps) (*UserInfo, error) { 106 | tracer := otel.Tracer("postgres/SetupNewUser") 107 | ctx, span := tracer.Start(ctx, "SetupNewUser") 108 | defer span.End() 109 | 110 | fName := args.FullName 111 | emailAddr := args.EmailAddr 112 | 113 | fullName := cases.Title(language.Und).String(strings.ToLower(fName)) 114 | 115 | user, err := d.Queries.AddUser(ctx, AddUserParams{ 116 | Email: emailAddr, 117 | FullName: fullName, 118 | }) 119 | if err != nil { 120 | d.logger.Logger(ctx).Error( 121 | "[Postgres] Could not setup new user", 122 | zap.Error(err), 123 | zap.String("user_email", emailAddr), 124 | zap.String("user_name", fName), 125 | ) 126 | span.RecordError(err) 127 | return nil, fmt.Errorf("could not setup new user") 128 | } 129 | 130 | return &user, err 131 | } 132 | -------------------------------------------------------------------------------- /backend/database/postgres/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.27.0 4 | 5 | package postgres 6 | 7 | import ( 8 | "database/sql" 9 | "database/sql/driver" 10 | "fmt" 11 | "time" 12 | 13 | "github.com/google/uuid" 14 | ) 15 | 16 | type MembershipType string 17 | 18 | const ( 19 | MembershipTypeOWNER MembershipType = "OWNER" 20 | MembershipTypeADMIN MembershipType = "ADMIN" 21 | MembershipTypeMEMBER MembershipType = "MEMBER" 22 | ) 23 | 24 | func (e *MembershipType) Scan(src interface{}) error { 25 | switch s := src.(type) { 26 | case []byte: 27 | *e = MembershipType(s) 28 | case string: 29 | *e = MembershipType(s) 30 | default: 31 | return fmt.Errorf("unsupported scan type for MembershipType: %T", src) 32 | } 33 | return nil 34 | } 35 | 36 | type NullMembershipType struct { 37 | MembershipType MembershipType 38 | Valid bool // Valid is true if MembershipType is not NULL 39 | } 40 | 41 | // Scan implements the Scanner interface. 42 | func (ns *NullMembershipType) Scan(value interface{}) error { 43 | if value == nil { 44 | ns.MembershipType, ns.Valid = "", false 45 | return nil 46 | } 47 | ns.Valid = true 48 | return ns.MembershipType.Scan(value) 49 | } 50 | 51 | // Value implements the driver Valuer interface. 52 | func (ns NullMembershipType) Value() (driver.Value, error) { 53 | if !ns.Valid { 54 | return nil, nil 55 | } 56 | return string(ns.MembershipType), nil 57 | } 58 | 59 | type AccountConnection struct { 60 | ConnectionID int64 61 | ProjectID int64 62 | ExternalID uuid.UUID 63 | AccountID string 64 | Created time.Time 65 | } 66 | 67 | type Project struct { 68 | ProjectID int64 69 | TeamID int64 70 | ProjectSlug string 71 | ProjectName string 72 | Created time.Time 73 | } 74 | 75 | type Scan struct { 76 | ScanID uuid.UUID 77 | ProjectID int64 78 | ScanCompleted bool 79 | Regions []string 80 | Services []string 81 | ServiceCount int32 82 | RegionCount int32 83 | ResourceCost int32 84 | Created time.Time 85 | } 86 | 87 | type ScanItem struct { 88 | ScanItemID int64 89 | ScanID uuid.UUID 90 | Service string 91 | Region string 92 | ResourceCost int32 93 | Findings []string 94 | Summary string 95 | Remedy string 96 | Created time.Time 97 | } 98 | 99 | type ScanItemEntry struct { 100 | ScanItemEntryID int64 101 | ScanItemID int64 102 | Findings []string 103 | Title string 104 | Summary string 105 | Remedy string 106 | Commands []string 107 | ResourceCost int32 108 | Created time.Time 109 | } 110 | 111 | type SubscriptionPlan struct { 112 | ID int64 113 | TeamID int64 114 | StripeSubscriptionID sql.NullString 115 | ResourcesIncluded int32 116 | ResourcesUsed int32 117 | Created time.Time 118 | } 119 | 120 | type Team struct { 121 | TeamID int64 122 | TeamSlug string 123 | TeamName string 124 | StripeCustomerID sql.NullString 125 | Created time.Time 126 | } 127 | 128 | type TeamInvite struct { 129 | TeamInviteID int64 130 | InviteCode string 131 | TeamID int64 132 | InviteeEmail string 133 | Created time.Time 134 | } 135 | 136 | type TeamMembership struct { 137 | TeamMembershipID int64 138 | TeamID int64 139 | UserID int64 140 | MembershipType MembershipType 141 | Created time.Time 142 | } 143 | 144 | type UserInfo struct { 145 | UserID int64 146 | Email string 147 | FullName string 148 | ExternalID uuid.UUID 149 | Created time.Time 150 | } 151 | -------------------------------------------------------------------------------- /backend/gqlgen.yml: -------------------------------------------------------------------------------- 1 | # Where are all the schema files located? globs are supported eg src/**/*.graphqls 2 | schema: 3 | - graph/*.graphqls 4 | 5 | # Where should the generated server code go? 6 | exec: 7 | filename: graph/generated.go 8 | package: graph 9 | 10 | # Uncomment to enable federation 11 | # federation: 12 | # filename: graph/federation.go 13 | # package: graph 14 | # version: 2 15 | # options 16 | # computed_requires: true 17 | 18 | # Where should any generated models go? 19 | model: 20 | filename: graph/model/models_gen.go 21 | package: model 22 | 23 | # Where should the resolver implementations go? 24 | resolver: 25 | layout: follow-schema 26 | dir: graph 27 | package: graph 28 | filename_template: "{name}.resolvers.go" 29 | # Optional: turn on to not generate template comments above resolvers 30 | # omit_template_comment: false 31 | 32 | # Optional: turn on use ` + "`" + `gqlgen:"fieldName"` + "`" + ` tags in your models 33 | # struct_tag: json 34 | 35 | # Optional: turn on to use []Thing instead of []*Thing 36 | omit_slice_element_pointers: true 37 | 38 | # Optional: turn on to omit Is() methods to interface and unions 39 | # omit_interface_checks : true 40 | 41 | # Optional: turn on to skip generation of ComplexityRoot struct content and Complexity function 42 | # omit_complexity: false 43 | 44 | # Optional: turn on to not generate any file notice comments in generated files 45 | # omit_gqlgen_file_notice: false 46 | 47 | # Optional: turn on to exclude the gqlgen version in the generated file notice. No effect if `omit_gqlgen_file_notice` is true. 48 | # omit_gqlgen_version_in_file_notice: false 49 | 50 | # Optional: turn off to make struct-type struct fields not use pointers 51 | # e.g. type Thing struct { FieldA OtherThing } instead of { FieldA *OtherThing } 52 | struct_fields_always_pointers: false 53 | 54 | # Optional: turn off to make resolvers return values instead of pointers for structs 55 | resolvers_always_return_pointers: false 56 | 57 | # Optional: turn on to return pointers instead of values in unmarshalInput 58 | # return_pointers_in_unmarshalinput: false 59 | 60 | # Optional: wrap nullable input fields with Omittable 61 | # nullable_input_omittable: true 62 | 63 | # Optional: set to speed up generation time by not performing a final validation pass. 64 | # skip_validation: true 65 | 66 | # Optional: set to skip running `go mod tidy` when generating server code 67 | # skip_mod_tidy: true 68 | 69 | # Optional: if this is set to true, argument directives that 70 | # decorate a field with a null value will still be called. 71 | # 72 | # This enables argumment directives to not just mutate 73 | # argument values but to set them even if they're null. 74 | call_argument_directives_with_null: true 75 | 76 | # gqlgen will search for any type names in the schema in these go packages 77 | # if they match it will use them, otherwise it will generate them. 78 | autobind: 79 | - "guarddev/database/postgres" 80 | 81 | # This section declares type mapping between the GraphQL and go type systems 82 | # 83 | # The first line in each type will be used as defaults for resolver arguments and 84 | # modelgen, the others will be allowed when binding to fields. Configure them to 85 | # your liking 86 | models: 87 | ID: 88 | model: 89 | - github.com/99designs/gqlgen/graphql.ID 90 | - github.com/99designs/gqlgen/graphql.Int 91 | - github.com/99designs/gqlgen/graphql.Int64 92 | - github.com/99designs/gqlgen/graphql.Int32 93 | Int: 94 | model: 95 | - github.com/99designs/gqlgen/graphql.Int 96 | - github.com/99designs/gqlgen/graphql.Int64 97 | - github.com/99designs/gqlgen/graphql.Int32 98 | 99 | Int64: 100 | model: 101 | - github.com/99designs/gqlgen/graphql.Int64 102 | -------------------------------------------------------------------------------- /frontend/app/console/[teamSlug]/project/[projectSlug]/scan/[scanId]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useQuery } from "@apollo/client"; 4 | import { GET_SCANS } from "./gql"; 5 | import { GetScansQuery } from "@/gql/graphql"; 6 | import { useEffect, useState } from "react"; 7 | import { Skeleton } from "@/components/ui/skeleton"; 8 | import { DataTable } from "./data_table"; 9 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; 10 | import { Button } from "@/components/ui/button"; 11 | import { ArrowLeft } from "lucide-react"; 12 | import Link from "next/link"; 13 | import { usePathname } from "next/navigation"; 14 | 15 | export type SCAN_ITEMS = GetScansQuery["teams"][0]["projects"][0]["scans"][0]["scanItems"][0]; 16 | 17 | const ScanPage = ({ params }: { params: { teamSlug: string; projectSlug: string; scanId: string } }) => { 18 | const { teamSlug, projectSlug, scanId } = params; 19 | const pathname = usePathname(); 20 | const parentPath = pathname.split('/scan/')[0]; 21 | 22 | const [scanCompleted, setScanComplete] = useState(false); 23 | 24 | const { data, loading } = useQuery(GET_SCANS, { variables: { teamSlug, projectSlug, scanId }, pollInterval: scanCompleted ? 0 : 5000 }); 25 | 26 | const scanData = data?.teams[0].projects[0].scans[0]; 27 | const scans = scanData?.scanItems; 28 | const scanCompl = scanData?.scanCompleted; 29 | 30 | useEffect(() => { 31 | if (scanCompl) { 32 | setScanComplete(true); 33 | } 34 | }, [data]) 35 | 36 | const LoadingSkeleton = () => ( 37 |
38 |
39 | {/* Skeleton for the input field */} 40 |
41 |
42 | 43 | 44 | 45 | {['Service', 'Region', 'Summary', 'Findings'].map((header) => ( 46 | 47 | 48 | 49 | ))} 50 | 51 | 52 | 53 | {[...Array(10)].map((_, index) => ( 54 | 55 | {[...Array(4)].map((_, cellIndex) => ( 56 | 57 | 58 | 59 | ))} 60 | 61 | ))} 62 | 63 |
64 |
65 |
66 | 67 | 68 |
69 |
70 | ); 71 | 72 | return ( 73 |
74 | {!loading ? 75 |
76 | 77 |
78 | 89 |
90 | 91 | 92 |
93 | : 94 |
95 | 96 |
97 | } 98 | 99 |
100 | ); 101 | }; 102 | 103 | export default ScanPage; 104 | -------------------------------------------------------------------------------- /frontend/app/console/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useQuery } from "@apollo/client"; 4 | import Installation from "./Installation"; 5 | import { GetExternalIdQuery, GetTeamsQuery } from "@/gql/graphql"; 6 | import { GET_EXTERNAL_ID, GET_TEAMS } from "./gql"; 7 | import { useEffect } from "react"; 8 | import { useRouter } from "next/navigation"; 9 | 10 | const ConsolePage = () => { 11 | const { data: team, refetch } = useQuery(GET_TEAMS, { variables: { teamSlug: undefined } }); 12 | const { data } = useQuery(GET_EXTERNAL_ID); 13 | const externalId = data?.getExternalId; 14 | 15 | const router = useRouter(); 16 | useEffect(() => { 17 | if (team?.teams.length) { 18 | router.push(`/console/${team?.teams[0].teamSlug}/project/${team?.teams[0].projects[0].projectSlug}`) 19 | } 20 | }, [team]); 21 | 22 | return ( 23 |
24 |
25 | {team?.teams.length === 0 && ( 26 |
27 | {/* Header */} 28 |
29 |

30 | {process.env.NEXT_PUBLIC_SELF_HOSTING ? "Set Up Your IAM User" : "Connect your AWS Account"} 31 |

32 |
33 |

34 | {process.env.NEXT_PUBLIC_SELF_HOSTING 35 | ? "To allow Guard to scan your AWS account, you need to create an IAM User with read-only permissions. This IAM User will allow Guard to securely access your AWS resources for security scans without modifying them." 36 | : "To allow Guard to scan your AWS account, you'll need to add a read-only permission using AWS CloudFormation. This will allow Guard to securely access your AWS resources for security scans without modifying them."} 37 |

38 |
39 | 40 | 41 | 42 |

43 | Scanning your account will not incur any charges to your AWS account. 44 |

45 |
46 |
47 |
48 | 49 | {/* Installation Steps */} 50 |
51 | 52 |
53 |
54 | )} 55 |
56 |
57 | ); 58 | } 59 | 60 | export default ConsolePage; 61 | -------------------------------------------------------------------------------- /frontend/gql/fragment-masking.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; 3 | import { FragmentDefinitionNode } from 'graphql'; 4 | import { Incremental } from './graphql'; 5 | 6 | 7 | export type FragmentType> = TDocumentType extends DocumentTypeDecoration< 8 | infer TType, 9 | any 10 | > 11 | ? [TType] extends [{ ' $fragmentName'?: infer TKey }] 12 | ? TKey extends string 13 | ? { ' $fragmentRefs'?: { [key in TKey]: TType } } 14 | : never 15 | : never 16 | : never; 17 | 18 | // return non-nullable if `fragmentType` is non-nullable 19 | export function useFragment( 20 | _documentNode: DocumentTypeDecoration, 21 | fragmentType: FragmentType> 22 | ): TType; 23 | // return nullable if `fragmentType` is undefined 24 | export function useFragment( 25 | _documentNode: DocumentTypeDecoration, 26 | fragmentType: FragmentType> | undefined 27 | ): TType | undefined; 28 | // return nullable if `fragmentType` is nullable 29 | export function useFragment( 30 | _documentNode: DocumentTypeDecoration, 31 | fragmentType: FragmentType> | null 32 | ): TType | null; 33 | // return nullable if `fragmentType` is nullable or undefined 34 | export function useFragment( 35 | _documentNode: DocumentTypeDecoration, 36 | fragmentType: FragmentType> | null | undefined 37 | ): TType | null | undefined; 38 | // return array of non-nullable if `fragmentType` is array of non-nullable 39 | export function useFragment( 40 | _documentNode: DocumentTypeDecoration, 41 | fragmentType: Array>> 42 | ): Array; 43 | // return array of nullable if `fragmentType` is array of nullable 44 | export function useFragment( 45 | _documentNode: DocumentTypeDecoration, 46 | fragmentType: Array>> | null | undefined 47 | ): Array | null | undefined; 48 | // return readonly array of non-nullable if `fragmentType` is array of non-nullable 49 | export function useFragment( 50 | _documentNode: DocumentTypeDecoration, 51 | fragmentType: ReadonlyArray>> 52 | ): ReadonlyArray; 53 | // return readonly array of nullable if `fragmentType` is array of nullable 54 | export function useFragment( 55 | _documentNode: DocumentTypeDecoration, 56 | fragmentType: ReadonlyArray>> | null | undefined 57 | ): ReadonlyArray | null | undefined; 58 | export function useFragment( 59 | _documentNode: DocumentTypeDecoration, 60 | fragmentType: FragmentType> | Array>> | ReadonlyArray>> | null | undefined 61 | ): TType | Array | ReadonlyArray | null | undefined { 62 | return fragmentType as any; 63 | } 64 | 65 | 66 | export function makeFragmentData< 67 | F extends DocumentTypeDecoration, 68 | FT extends ResultOf 69 | >(data: FT, _fragment: F): FragmentType { 70 | return data as FragmentType; 71 | } 72 | export function isFragmentReady( 73 | queryNode: DocumentTypeDecoration, 74 | fragmentNode: TypedDocumentNode, 75 | data: FragmentType, any>> | null | undefined 76 | ): data is FragmentType { 77 | const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ 78 | ?.deferredFields; 79 | 80 | if (!deferredFields) return true; 81 | 82 | const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; 83 | const fragName = fragDef?.name?.value; 84 | 85 | const fields = (fragName && deferredFields[fragName]) || []; 86 | return fields.length > 0 && fields.every(field => data && field in data); 87 | } 88 | -------------------------------------------------------------------------------- /backend/awsmiddleware/scanner.go: -------------------------------------------------------------------------------- 1 | package awsmiddleware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "guarddev/awsmiddleware/dynamodbscanner" 7 | "guarddev/awsmiddleware/ec2scanner" 8 | "guarddev/awsmiddleware/ecsscanner" 9 | "guarddev/awsmiddleware/iamscanner" 10 | "guarddev/awsmiddleware/lambdascanner" 11 | "guarddev/awsmiddleware/s3scanner" 12 | "guarddev/database/postgres" 13 | 14 | "go.opentelemetry.io/otel" 15 | "go.opentelemetry.io/otel/attribute" 16 | "go.uber.org/zap" 17 | ) 18 | 19 | type ScanResults struct { 20 | ScanItem postgres.ScanItem 21 | ScanItemEntries []postgres.CreateNewScanItemEntryParams 22 | } 23 | 24 | func (a *AWSMiddleware) StartScan(ctx context.Context, accessKey, secretKey, sessionToken string, regions, services []string, accountId string) ([]ScanResults, error) { 25 | tracer := otel.Tracer("awsmiddleware/StartScan") 26 | ctx, span := tracer.Start(ctx, "StartScan") 27 | defer span.End() 28 | 29 | span.SetAttributes( 30 | attribute.StringSlice("aws.regions", regions), 31 | attribute.StringSlice("aws.services", services), 32 | attribute.String("aws.account_id", accountId), 33 | ) 34 | 35 | var results []ScanResults 36 | 37 | logger := a.logger.Logger(ctx) 38 | 39 | for _, region := range regions { 40 | for _, service := range services { 41 | logger.Info("[AWSMiddleware/StartScan] Scanning Service", zap.String("Service", service), zap.String("Region", region)) 42 | result, scanItems, err := a.scanService(ctx, accessKey, secretKey, sessionToken, region, service, accountId) 43 | if err != nil { 44 | span.RecordError(err) 45 | return nil, fmt.Errorf("error scanning %s in %s: %v", service, region, err) 46 | } 47 | results = append(results, ScanResults{ 48 | ScanItem: result, 49 | ScanItemEntries: scanItems, 50 | }) 51 | } 52 | } 53 | 54 | return results, nil 55 | } 56 | 57 | func (a *AWSMiddleware) scanService(ctx context.Context, accessKey, secretKey, sessionToken, region, service, accountId string) (postgres.ScanItem, []postgres.CreateNewScanItemEntryParams, error) { 58 | tracer := otel.Tracer("awsmiddleware/scanService") 59 | ctx, span := tracer.Start(ctx, "scanService") 60 | defer span.End() 61 | 62 | span.SetAttributes( 63 | attribute.String("aws.region", region), 64 | attribute.String("aws.service", service), 65 | attribute.String("aws.account_id", accountId), 66 | ) 67 | 68 | result := postgres.ScanItem{ 69 | Service: service, 70 | Region: region, 71 | } 72 | 73 | var scanItemEntries []postgres.CreateNewScanItemEntryParams = []postgres.CreateNewScanItemEntryParams{} 74 | 75 | var err error 76 | var findings []string 77 | 78 | switch service { 79 | case "s3": 80 | scanner := s3scanner.NewS3Scanner(accessKey, secretKey, sessionToken, region, accountId, a.logger, a.model) 81 | findings, scanItemEntries, err = scanner.ScanS3(ctx, region) 82 | case "ec2": 83 | scanner := ec2scanner.NewEC2Scanner(accessKey, secretKey, sessionToken, region, accountId, a.logger, a.model) 84 | findings, scanItemEntries, err = scanner.ScanEC2(ctx, region) 85 | case "ecs": 86 | scanner := ecsscanner.NewECSScanner(accessKey, secretKey, sessionToken, region, accountId, a.logger, a.model) 87 | findings, scanItemEntries, err = scanner.ScanECS(ctx, region) 88 | case "lambda": 89 | scanner := lambdascanner.NewLambdaScanner(accessKey, secretKey, sessionToken, region, accountId, a.logger, a.model) 90 | findings, scanItemEntries, err = scanner.ScanLambda(ctx, region) 91 | case "dynamodb": 92 | scanner := dynamodbscanner.NewDynamoDBScanner(accessKey, secretKey, sessionToken, region, accountId, a.logger, a.model) 93 | findings, scanItemEntries, err = scanner.ScanDynamoDB(ctx, region) 94 | case "iam": 95 | scanner := iamscanner.NewIAMScanner(accessKey, secretKey, sessionToken, region, accountId, a.logger, a.model) 96 | findings, scanItemEntries, err = scanner.ScanIAM(ctx, region) 97 | default: 98 | err = fmt.Errorf("unsupported service: %s", service) 99 | } 100 | 101 | if err != nil { 102 | span.RecordError(err) 103 | return result, nil, err 104 | } 105 | 106 | result.Findings = findings 107 | span.SetAttributes(attribute.Int("findings.count", len(findings))) 108 | 109 | return result, scanItemEntries, nil 110 | } 111 | -------------------------------------------------------------------------------- /backend/awsmiddleware/main.go: -------------------------------------------------------------------------------- 1 | package awsmiddleware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "guarddev/logger" 7 | "guarddev/modelapi" 8 | "os" 9 | 10 | "github.com/aws/aws-sdk-go/aws" 11 | "github.com/aws/aws-sdk-go/aws/credentials" 12 | "github.com/aws/aws-sdk-go/aws/session" 13 | "github.com/aws/aws-sdk-go/service/sts" 14 | "go.opentelemetry.io/otel" 15 | "go.opentelemetry.io/otel/attribute" 16 | ) 17 | 18 | type AWSMiddleware struct { 19 | stsClient *sts.STS 20 | logger *logger.LogMiddleware 21 | model modelapi.ModelAPI 22 | selfHosted bool 23 | } 24 | 25 | type AssumeRoleProps struct { 26 | AWSAccountID string 27 | ExternalID string 28 | } 29 | 30 | type AssumeRoleResult struct { 31 | AccessKeyID string 32 | SecretAccessKey string 33 | SessionToken string 34 | } 35 | 36 | func Connect(logger *logger.LogMiddleware, model modelapi.ModelAPI) *AWSMiddleware { 37 | tracer := otel.Tracer("awsmiddleware/Connect") 38 | _, span := tracer.Start(context.Background(), "Connect") 39 | defer span.End() 40 | 41 | AWS_REGION := os.Getenv("AWS_REGION") 42 | selfHosting := os.Getenv("SELF_HOSTING") != "" 43 | 44 | span.SetAttributes( 45 | attribute.String("aws.region", AWS_REGION), 46 | ) 47 | 48 | var sess *session.Session 49 | var stsClient *sts.STS 50 | 51 | if selfHosting { 52 | // Self-Hosting: Use provided credentials directly 53 | AWS_ACCESS_KEY_ID := os.Getenv("AWS_ACCESS_KEY_ID") 54 | AWS_SECRET_ACCESS_KEY := os.Getenv("AWS_SECRET_ACCESS_KEY") 55 | 56 | sess = session.Must(session.NewSession(&aws.Config{ 57 | Region: aws.String(AWS_REGION), 58 | Credentials: credentials.NewStaticCredentials(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, ""), 59 | })) 60 | } else { 61 | // Production: Create a session that will use STS 62 | sess = session.Must(session.NewSession(&aws.Config{ 63 | Region: aws.String(AWS_REGION), 64 | })) 65 | stsClient = sts.New(sess) 66 | } 67 | 68 | return &AWSMiddleware{ 69 | stsClient: stsClient, 70 | logger: logger, 71 | model: model, 72 | selfHosted: selfHosting, 73 | } 74 | } 75 | 76 | func (a *AWSMiddleware) AssumeRole(ctx context.Context, props AssumeRoleProps) (*AssumeRoleResult, error) { 77 | tracer := otel.Tracer("awsmiddleware/AssumeRole") 78 | ctx, span := tracer.Start(ctx, "AssumeRole") 79 | defer span.End() 80 | 81 | if a.selfHosted { 82 | // Self-Hosting: Direct access is used, no role assumption necessary 83 | return nil, fmt.Errorf("AssumeRole is not applicable in self-hosting mode") 84 | } 85 | 86 | roleArn := fmt.Sprintf("arn:aws:iam::%s:role/GuardSecurityScanRole", props.AWSAccountID) 87 | 88 | span.SetAttributes( 89 | attribute.String("aws.account_id", props.AWSAccountID), 90 | attribute.String("aws.role_arn", roleArn), 91 | ) 92 | 93 | input := &sts.AssumeRoleInput{ 94 | RoleArn: aws.String(roleArn), 95 | RoleSessionName: aws.String("GuardSession"), 96 | ExternalId: aws.String(props.ExternalID), 97 | } 98 | 99 | result, err := a.stsClient.AssumeRoleWithContext(ctx, input) 100 | if err != nil { 101 | span.RecordError(err) 102 | return nil, fmt.Errorf("error assuming role: %v", err) 103 | } 104 | 105 | return &AssumeRoleResult{ 106 | AccessKeyID: *result.Credentials.AccessKeyId, 107 | SecretAccessKey: *result.Credentials.SecretAccessKey, 108 | SessionToken: *result.Credentials.SessionToken, 109 | }, nil 110 | } 111 | 112 | // GetAccountID retrieves the AWS account ID for the given access key and secret key 113 | func (a *AWSMiddleware) GetAccountID(accessKey, secretKey string) (string, error) { 114 | // Create a new AWS session with provided credentials 115 | sess, err := session.NewSession(&aws.Config{ 116 | Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""), 117 | Region: aws.String("us-west-2"), // Region is required, but it doesn’t matter for STS calls 118 | }) 119 | if err != nil { 120 | return "", fmt.Errorf("failed to create session, %w", err) 121 | } 122 | 123 | // Create an STS client 124 | svc := sts.New(sess) 125 | 126 | // Call GetCallerIdentity to get account information 127 | result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{}) 128 | if err != nil { 129 | return "", fmt.Errorf("failed to get caller identity, %w", err) 130 | } 131 | 132 | // Return the Account ID 133 | return *result.Account, nil 134 | } 135 | -------------------------------------------------------------------------------- /backend/auth/main.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "strings" 10 | 11 | "github.com/clerkinc/clerk-sdk-go/clerk" 12 | "go.opentelemetry.io/otel" 13 | "go.opentelemetry.io/otel/attribute" 14 | ) 15 | 16 | var userCtxKey = &contextKey{"userId"} 17 | 18 | type contextKey struct { 19 | name string 20 | } 21 | 22 | func Middleware() func(http.Handler) http.Handler { 23 | return func(next http.Handler) http.Handler { 24 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | tracer := otel.Tracer("auth/Middleware") 26 | ctx, span := tracer.Start(r.Context(), "AuthMiddleware") 27 | defer span.End() 28 | 29 | selfHosting := os.Getenv("SELF_HOSTING") != "" 30 | if selfHosting { 31 | firstName := "Self" 32 | lastName := "Hosted" 33 | emailID := "mock_email_id" 34 | email := "support@guard.dev" 35 | user := clerk.User{ 36 | ID: "mock_user_id", 37 | FirstName: &firstName, 38 | LastName: &lastName, 39 | PrimaryEmailAddressID: &emailID, 40 | EmailAddresses: []clerk.EmailAddress{ 41 | { 42 | ID: emailID, 43 | EmailAddress: email, 44 | }, 45 | }, 46 | } 47 | ctx = AttachContext(ctx, &user) 48 | r = r.WithContext(ctx) 49 | next.ServeHTTP(w, r) 50 | return 51 | } 52 | 53 | vercelEnv := r.Header.Get("vercel_env") 54 | clientKey := os.Getenv("CLERK_SECRET_KEY") 55 | 56 | if vercelEnv == "preview" { 57 | clientKey = os.Getenv("CLERK_SECRET_KEY_ALTERNATIVE") 58 | } 59 | 60 | if clientKey == "" { 61 | log.Fatalln("ERROR: CANNOT FIND CLERK CLIENT KEY") 62 | } 63 | 64 | client, _ := clerk.NewClient(clientKey) 65 | header := r.Header.Get("Authorization") 66 | 67 | // User is unauthenticated. 68 | if header == "" { 69 | span.AddEvent("Unauthenticated user, no Authorization header") 70 | next.ServeHTTP(w, r) 71 | return 72 | } 73 | 74 | sessionToken := strings.Split(header, " ")[1] 75 | sessClaims, err := client.VerifyToken(sessionToken) 76 | if err != nil { 77 | span.RecordError(err) 78 | http.Error(w, "Invalid Authorization Token", http.StatusForbidden) 79 | return 80 | } 81 | 82 | user, err := client.Users().Read(sessClaims.Claims.Subject) 83 | if err != nil { 84 | span.RecordError(err) 85 | http.Error(w, "Malformed Authorization Token", http.StatusForbidden) 86 | return 87 | } 88 | 89 | span.SetAttributes( 90 | attribute.String("user.id", user.ID), 91 | attribute.String("user.email", user.EmailAddresses[0].EmailAddress), 92 | ) 93 | 94 | ctx = AttachContext(ctx, user) 95 | r = r.WithContext(ctx) 96 | next.ServeHTTP(w, r) 97 | }) 98 | } 99 | } 100 | 101 | func AttachContext(ctx context.Context, user *clerk.User) context.Context { 102 | return context.WithValue(ctx, userCtxKey, user) 103 | } 104 | 105 | func FromContext(ctx context.Context) *clerk.User { 106 | raw, _ := ctx.Value(userCtxKey).(*clerk.User) 107 | return raw 108 | } 109 | 110 | func EmailFromContext(ctx context.Context) (string, error) { 111 | tracer := otel.Tracer("auth/EmailFromContext") 112 | ctx, span := tracer.Start(ctx, "EmailFromContext") 113 | defer span.End() 114 | 115 | user := FromContext(ctx) 116 | return getEmail(ctx, user) 117 | } 118 | 119 | func getEmail(ctx context.Context, user *clerk.User) (string, error) { 120 | tracer := otel.Tracer("auth/getEmail") 121 | _, span := tracer.Start(ctx, "getEmail") 122 | defer span.End() 123 | 124 | if user == nil { 125 | err := fmt.Errorf("not logged in") 126 | span.RecordError(err) 127 | return "", err 128 | } 129 | for _, emailAddr := range user.EmailAddresses { 130 | if emailAddr.ID == *user.PrimaryEmailAddressID { 131 | return emailAddr.EmailAddress, nil 132 | } 133 | } 134 | return user.EmailAddresses[0].EmailAddress, nil 135 | } 136 | 137 | func FullnameFromContext(ctx context.Context) (string, error) { 138 | tracer := otel.Tracer("auth/FullnameFromContext") 139 | ctx, span := tracer.Start(ctx, "FullnameFromContext") 140 | defer span.End() 141 | 142 | user := FromContext(ctx) 143 | if user == nil { 144 | err := fmt.Errorf("not logged in") 145 | span.RecordError(err) 146 | return "", err 147 | } 148 | return fmt.Sprintf("%s %s", *user.FirstName, *user.LastName), nil 149 | } 150 | -------------------------------------------------------------------------------- /frontend/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { Cross2Icon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /backend/database/postgres/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TYPE IF EXISTS membership_type CASCADE; 2 | CREATE TYPE membership_type AS ENUM ('OWNER', 'ADMIN', 'MEMBER'); 3 | 4 | DROP TABLE IF EXISTS user_info CASCADE; 5 | CREATE TABLE user_info ( 6 | user_id BIGSERIAL PRIMARY KEY NOT NULL, 7 | email TEXT UNIQUE NOT NULL, 8 | full_name TEXT NOT NULL, 9 | external_id UUID NOT NULL DEFAULT gen_random_uuid(), -- New column for storing the External ID 10 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 11 | ); 12 | 13 | DROP TABLE IF EXISTS team CASCADE; 14 | CREATE TABLE team ( 15 | team_id BIGSERIAL PRIMARY KEY NOT NULL, 16 | team_slug TEXT UNIQUE NOT NULL, 17 | team_name TEXT NOT NULL, 18 | stripe_customer_id TEXT UNIQUE, 19 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 20 | ); 21 | 22 | DROP TABLE IF EXISTS team_membership CASCADE; 23 | CREATE TABLE team_membership ( 24 | team_membership_id BIGSERIAL PRIMARY KEY NOT NULL, 25 | team_id BIGINT REFERENCES team (team_id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 26 | user_id BIGINT REFERENCES user_info (user_id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 27 | membership_type MEMBERSHIP_TYPE NOT NULL, 28 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 29 | UNIQUE (team_id, user_id) 30 | ); 31 | 32 | DROP TABLE IF EXISTS team_invite CASCADE; 33 | CREATE TABLE team_invite ( 34 | team_invite_id BIGSERIAL PRIMARY KEY NOT NULL, 35 | invite_code TEXT UNIQUE NOT NULL, 36 | team_id BIGINT REFERENCES team (team_id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 37 | invitee_email TEXT NOT NULL, 38 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 39 | UNIQUE (team_id, invitee_email) 40 | ); 41 | 42 | DROP TABLE IF EXISTS project CASCADE; 43 | CREATE TABLE project ( 44 | project_id BIGSERIAL PRIMARY KEY NOT NULL, 45 | team_id BIGINT REFERENCES team (team_id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 46 | project_slug TEXT UNIQUE NOT NULL, 47 | project_name TEXT NOT NULL, 48 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 49 | ); 50 | 51 | DROP TABLE IF EXISTS account_connection CASCADE; 52 | CREATE TABLE account_connection ( 53 | connection_id BIGSERIAL PRIMARY KEY NOT NULL, 54 | project_id BIGINT REFERENCES project (project_id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 55 | external_id UUID NOT NULL, 56 | account_id TEXT NOT NULL, 57 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 58 | ); 59 | 60 | DROP TABLE IF EXISTS scan CASCADE; 61 | CREATE TABLE scan ( 62 | scan_id UUID PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), 63 | project_id BIGINT REFERENCES project (project_id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 64 | scan_completed BOOLEAN NOT NULL DEFAULT false, 65 | regions TEXT[], 66 | services TEXT[], 67 | service_count INT NOT NULL DEFAULT 0, 68 | region_count INT NOT NULL DEFAULT 0, 69 | resource_cost INT NOT NULL DEFAULT 0, -- Total resource cost for the scan 70 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 71 | ); 72 | 73 | DROP TABLE IF EXISTS scan_item CASCADE; 74 | CREATE TABLE scan_item ( 75 | scan_item_id BIGSERIAL PRIMARY KEY NOT NULL, 76 | scan_id UUID REFERENCES scan (scan_id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 77 | service TEXT NOT NULL, 78 | region TEXT NOT NULL, 79 | resource_cost INT NOT NULL DEFAULT 0, -- Total resource cost for each scan item 80 | findings TEXT[], 81 | summary TEXT NOT NULL, 82 | remedy TEXT NOT NULL, 83 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 84 | ); 85 | 86 | DROP TABLE IF EXISTS scan_item_entry CASCADE; 87 | CREATE TABLE scan_item_entry ( 88 | scan_item_entry_id BIGSERIAL PRIMARY KEY NOT NULL, 89 | scan_item_id BIGINT REFERENCES scan_item (scan_item_id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 90 | findings TEXT[], 91 | title TEXT NOT NULL, 92 | summary TEXT NOT NULL, 93 | remedy TEXT NOT NULL, 94 | commands TEXT[], 95 | resource_cost INT NOT NULL DEFAULT 1, -- Resource cost for each scan item entry 96 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 97 | ); 98 | 99 | DROP TABLE IF EXISTS subscription_plan CASCADE; 100 | CREATE TABLE subscription_plan ( 101 | id BIGSERIAL PRIMARY KEY NOT NULL, 102 | team_id BIGINT REFERENCES team (team_id) ON DELETE CASCADE UNIQUE NOT NULL, 103 | stripe_subscription_id TEXT UNIQUE, 104 | resources_included INT NOT NULL DEFAULT 0, -- Included resources per plan 105 | resources_used INT NOT NULL DEFAULT 0, -- Tracks total resources used 106 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 107 | ); 108 | -------------------------------------------------------------------------------- /backend/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "guarddev/auth" 6 | "guarddev/awsmiddleware" 7 | "guarddev/database/postgres" 8 | "guarddev/graph" 9 | "guarddev/logger" 10 | "guarddev/modelapi" 11 | "guarddev/modelapi/anthropicapi" 12 | "guarddev/modelapi/geminiapi" 13 | "guarddev/modelapi/grokapi" 14 | "guarddev/paymentsmiddleware" 15 | "log" 16 | "net/http" 17 | "os" 18 | 19 | "github.com/99designs/gqlgen/graphql/playground" 20 | "github.com/go-chi/chi" 21 | "github.com/joho/godotenv" 22 | "github.com/rs/cors" 23 | "go.uber.org/zap" 24 | 25 | "github.com/hyperdxio/opentelemetry-logs-go/exporters/otlp/otlplogs" 26 | sdk "github.com/hyperdxio/opentelemetry-logs-go/sdk/logs" 27 | "github.com/hyperdxio/otel-config-go/otelconfig" 28 | ) 29 | 30 | const defaultPort = "8080" 31 | 32 | func main() { 33 | port := os.Getenv("PORT") 34 | if port == "" { 35 | port = defaultPort 36 | } 37 | 38 | godotenv.Load() 39 | production := os.Getenv("PRODUCTION") != "" 40 | selfhosting := os.Getenv("SELF_HOSTING") != "" 41 | 42 | otelShutdown, err := otelconfig.ConfigureOpenTelemetry() 43 | if err != nil { 44 | log.Fatalf("Error setting up OTel SDK - %e", err) 45 | } 46 | defer otelShutdown() 47 | ctx := context.Background() 48 | 49 | logExporter, _ := otlplogs.NewExporter(ctx) 50 | loggerProvider := sdk.NewLoggerProvider(sdk.WithBatcher(logExporter)) 51 | defer loggerProvider.Shutdown(ctx) 52 | 53 | logMiddleware := logger.Connect(logger.LoggerConnectProps{Production: false}) 54 | postgresClient := postgres.Connect(ctx, postgres.DatabaseConnectProps{Logger: logMiddleware}) 55 | 56 | Logger := logMiddleware.Logger(ctx) 57 | 58 | // Initialize the selected model based on environment variable 59 | var selectedModel modelapi.ModelAPI 60 | modelType := os.Getenv("MODEL_TYPE") 61 | switch modelType { 62 | case "anthropic": 63 | Logger.Info("[ModelAPI] Initializing Anthropic model") 64 | selectedModel = anthropicapi.Connect(ctx, anthropicapi.AnthropicConnectProps{ 65 | Logger: logMiddleware, 66 | }) 67 | case "grok": 68 | Logger.Info("[ModelAPI] Initializing Grok model") 69 | selectedModel = grokapi.Connect(ctx, grokapi.GrokConnectProps{ 70 | Logger: logMiddleware, 71 | }) 72 | default: 73 | if modelType == "" { 74 | Logger.Info("[ModelAPI] No model type specified, defaulting to Gemini") 75 | } else { 76 | Logger.Info("[ModelAPI] Unknown model type, defaulting to Gemini", zap.String("specified_type", modelType)) 77 | } 78 | selectedModel = geminiapi.Connect(ctx, geminiapi.GeminiConnectProps{ 79 | Logger: logMiddleware, 80 | }) 81 | } 82 | 83 | var payments *paymentsmiddleware.Payments = nil 84 | 85 | if !selfhosting { 86 | payments = paymentsmiddleware.Connect(paymentsmiddleware.PaymentsConnectProps{ 87 | Logger: logMiddleware, 88 | Database: postgresClient, 89 | }) 90 | } 91 | 92 | awsMiddleware := awsmiddleware.Connect(logMiddleware, selectedModel) 93 | 94 | srv := graph.Connnect(ctx, graph.GraphConnectProps{ 95 | Logger: logMiddleware, 96 | Database: postgresClient, 97 | AWSMiddleware: awsMiddleware, 98 | Gemini: selectedModel, // Note: This might need to be updated if the graph package expects specifically Gemini 99 | Payments: payments, 100 | }) 101 | 102 | // Start listening for connections 103 | graphRouter := getGraphqlSrv() 104 | graphRouter.Handle("/", srv) 105 | 106 | if !production { 107 | graphRouter.Handle("/playground", playground.Handler("GraphQL Playground", "/graph")) 108 | Logger.Info("[Graph] Connect to http://localhost:" + port + "/graph for GraphQL server") 109 | Logger.Info("[Graph] Connect to http://localhost:" + port + "/graph/playground for GraphQL playground") 110 | } else { 111 | Logger.Info("[Graph] Connect to https://api.guard.dev/graph for GraphQL server") 112 | } 113 | 114 | router := chi.NewRouter() 115 | router.Mount("/graph", graphRouter) 116 | 117 | if !selfhosting { 118 | router.Post("/payments", payments.HandleStripeWebhook) 119 | } 120 | 121 | log.Fatal(http.ListenAndServe(":"+port, router)) 122 | } 123 | 124 | func getGraphqlSrv() *chi.Mux { 125 | frontendAllowedOrigins := []string{ 126 | "http://localhost:3000", 127 | "https://www.guard.dev", 128 | "https://guard.dev", 129 | } 130 | 131 | graphRouter := chi.NewRouter() 132 | graphRouter.Use(cors.New(cors.Options{ 133 | AllowedOrigins: frontendAllowedOrigins, 134 | AllowCredentials: true, 135 | AllowedHeaders: []string{"Content-Type", "Authorization", "vercel_env"}, 136 | Debug: false, 137 | }).Handler) 138 | graphRouter.Use(auth.Middleware()) 139 | 140 | return graphRouter 141 | } 142 | -------------------------------------------------------------------------------- /frontend/components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SheetPrimitive from "@radix-ui/react-dialog" 5 | import { Cross2Icon } from "@radix-ui/react-icons" 6 | import { cva, type VariantProps } from "class-variance-authority" 7 | 8 | import { cn } from "@/lib/utils" 9 | 10 | const Sheet = SheetPrimitive.Root 11 | 12 | const SheetTrigger = SheetPrimitive.Trigger 13 | 14 | const SheetClose = SheetPrimitive.Close 15 | 16 | const SheetPortal = SheetPrimitive.Portal 17 | 18 | const SheetOverlay = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef 21 | >(({ className, ...props }, ref) => ( 22 | 30 | )) 31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName 32 | 33 | const sheetVariants = cva( 34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", 35 | { 36 | variants: { 37 | side: { 38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", 39 | bottom: 40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", 41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", 42 | right: 43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", 44 | }, 45 | }, 46 | defaultVariants: { 47 | side: "right", 48 | }, 49 | } 50 | ) 51 | 52 | interface SheetContentProps 53 | extends React.ComponentPropsWithoutRef, 54 | VariantProps {} 55 | 56 | const SheetContent = React.forwardRef< 57 | React.ElementRef, 58 | SheetContentProps 59 | >(({ side = "right", className, children, ...props }, ref) => ( 60 | 61 | 62 | 67 | 68 | 69 | Close 70 | 71 | {children} 72 | 73 | 74 | )) 75 | SheetContent.displayName = SheetPrimitive.Content.displayName 76 | 77 | const SheetHeader = ({ 78 | className, 79 | ...props 80 | }: React.HTMLAttributes) => ( 81 |
88 | ) 89 | SheetHeader.displayName = "SheetHeader" 90 | 91 | const SheetFooter = ({ 92 | className, 93 | ...props 94 | }: React.HTMLAttributes) => ( 95 |
102 | ) 103 | SheetFooter.displayName = "SheetFooter" 104 | 105 | const SheetTitle = React.forwardRef< 106 | React.ElementRef, 107 | React.ComponentPropsWithoutRef 108 | >(({ className, ...props }, ref) => ( 109 | 114 | )) 115 | SheetTitle.displayName = SheetPrimitive.Title.displayName 116 | 117 | const SheetDescription = React.forwardRef< 118 | React.ElementRef, 119 | React.ComponentPropsWithoutRef 120 | >(({ className, ...props }, ref) => ( 121 | 126 | )) 127 | SheetDescription.displayName = SheetPrimitive.Description.displayName 128 | 129 | export { 130 | Sheet, 131 | SheetPortal, 132 | SheetOverlay, 133 | SheetTrigger, 134 | SheetClose, 135 | SheetContent, 136 | SheetHeader, 137 | SheetFooter, 138 | SheetTitle, 139 | SheetDescription, 140 | } 141 | -------------------------------------------------------------------------------- /backend/go.mod: -------------------------------------------------------------------------------- 1 | module guarddev 2 | 3 | go 1.22.5 4 | 5 | toolchain go1.22.7 6 | 7 | require ( 8 | github.com/99designs/gqlgen v0.17.54 9 | github.com/aws/aws-sdk-go v1.55.5 10 | github.com/clerkinc/clerk-sdk-go v1.49.1 11 | github.com/go-chi/chi v1.5.5 12 | github.com/google/generative-ai-go v0.18.0 13 | github.com/google/uuid v1.6.0 14 | github.com/hyperdxio/opentelemetry-go/otelzap v0.2.1 15 | github.com/hyperdxio/opentelemetry-logs-go v0.4.2 16 | github.com/hyperdxio/otel-config-go v1.12.3 17 | github.com/joho/godotenv v1.5.1 18 | github.com/lib/pq v1.10.9 19 | github.com/rs/cors v1.11.1 20 | github.com/stretchr/testify v1.9.0 21 | github.com/stripe/stripe-go/v81 v81.0.0 22 | github.com/vektah/gqlparser/v2 v2.5.16 23 | go.opentelemetry.io/otel v1.30.0 24 | go.opentelemetry.io/otel/trace v1.30.0 25 | go.uber.org/zap v1.27.0 26 | golang.org/x/sync v0.8.0 27 | golang.org/x/text v0.18.0 28 | google.golang.org/api v0.199.0 29 | ) 30 | 31 | require ( 32 | cloud.google.com/go v0.115.1 // indirect 33 | cloud.google.com/go/ai v0.8.0 // indirect 34 | cloud.google.com/go/auth v0.9.5 // indirect 35 | cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect 36 | cloud.google.com/go/compute/metadata v0.5.2 // indirect 37 | cloud.google.com/go/longrunning v0.5.7 // indirect 38 | github.com/agnivade/levenshtein v1.1.1 // indirect 39 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 40 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect 41 | github.com/davecgh/go-spew v1.1.1 // indirect 42 | github.com/felixge/httpsnoop v1.0.4 // indirect 43 | github.com/go-jose/go-jose/v3 v3.0.0 // indirect 44 | github.com/go-logr/logr v1.4.2 // indirect 45 | github.com/go-logr/stdr v1.2.2 // indirect 46 | github.com/go-ole/go-ole v1.2.6 // indirect 47 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 48 | github.com/google/s2a-go v0.1.8 // indirect 49 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 50 | github.com/googleapis/gax-go/v2 v2.13.0 // indirect 51 | github.com/gorilla/websocket v1.5.0 // indirect 52 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect 53 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 54 | github.com/jmespath/go-jmespath v0.4.0 // indirect 55 | github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect 56 | github.com/mitchellh/mapstructure v1.5.0 // indirect 57 | github.com/pmezard/go-difflib v1.0.0 // indirect 58 | github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect 59 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 60 | github.com/sethvargo/go-envconfig v0.9.0 // indirect 61 | github.com/shirou/gopsutil/v3 v3.23.8 // indirect 62 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 63 | github.com/sosodev/duration v1.3.1 // indirect 64 | github.com/tklauser/go-sysconf v0.3.12 // indirect 65 | github.com/tklauser/numcpus v0.6.1 // indirect 66 | github.com/urfave/cli/v2 v2.27.4 // indirect 67 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 68 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 69 | go.opencensus.io v0.24.0 // indirect 70 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect 71 | go.opentelemetry.io/contrib/instrumentation/host v0.44.0 // indirect 72 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 73 | go.opentelemetry.io/contrib/instrumentation/runtime v0.44.0 // indirect 74 | go.opentelemetry.io/contrib/propagators/b3 v1.19.0 // indirect 75 | go.opentelemetry.io/contrib/propagators/ot v1.19.0 // indirect 76 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.41.0 // indirect 77 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.41.0 // indirect 78 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.41.0 // indirect 79 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 // indirect 80 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.18.0 // indirect 81 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 // indirect 82 | go.opentelemetry.io/otel/metric v1.30.0 // indirect 83 | go.opentelemetry.io/otel/sdk v1.30.0 // indirect 84 | go.opentelemetry.io/otel/sdk/metric v1.30.0 // indirect 85 | go.opentelemetry.io/proto/otlp v1.0.0 // indirect 86 | go.uber.org/multierr v1.11.0 // indirect 87 | golang.org/x/crypto v0.27.0 // indirect 88 | golang.org/x/mod v0.20.0 // indirect 89 | golang.org/x/net v0.29.0 // indirect 90 | golang.org/x/oauth2 v0.23.0 // indirect 91 | golang.org/x/sys v0.25.0 // indirect 92 | golang.org/x/time v0.6.0 // indirect 93 | golang.org/x/tools v0.24.0 // indirect 94 | google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect 95 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect 96 | google.golang.org/grpc v1.67.0 // indirect 97 | google.golang.org/protobuf v1.34.2 // indirect 98 | gopkg.in/yaml.v3 v3.0.1 // indirect 99 | ) 100 | -------------------------------------------------------------------------------- /templates/guard-scan-role.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: | 3 | This template creates the GuardSecurityScanRole IAM role in your AWS account 4 | with the necessary read-only permissions for Guard to perform security scans. 5 | The role will allow Guard to assume the role using AWS Security Token Service (STS). 6 | Parameters: 7 | ExternalId: 8 | Description: | 9 | The External ID Guard will use to assume this role. DO NOT CHANGE THIS. 10 | Type: String 11 | 12 | Resources: 13 | GuardSecurityScanRole: 14 | Type: AWS::IAM::Role 15 | Properties: 16 | RoleName: GuardSecurityScanRole 17 | AssumeRolePolicyDocument: 18 | Version: '2012-10-17' 19 | Statement: 20 | - Effect: Allow 21 | Principal: 22 | AWS: "arn:aws:iam::591907732013:root" 23 | Action: 'sts:AssumeRole' 24 | Condition: 25 | StringEquals: 26 | 'sts:ExternalId': !Ref ExternalId 27 | MaxSessionDuration: 21600 28 | Policies: 29 | - PolicyName: GuardPermissions 30 | PolicyDocument: 31 | Version: '2012-10-17' 32 | Statement: 33 | - Effect: Allow 34 | Action: 35 | - 'account:Get*' 36 | Resource: "*" 37 | - Effect: Allow 38 | Action: 39 | - 'apigateway:GET' 40 | Resource: "*" 41 | - Effect: Allow 42 | Action: 43 | - 'cloudtrail:GetInsightSelectors' 44 | Resource: "*" 45 | - Effect: Allow 46 | Action: 47 | - 'dynamodb:ListTables' 48 | - 'dynamodb:DescribeTable' 49 | - 'dynamodb:DescribeContinuousBackups' 50 | - 'dynamodb:DescribeTimeToLive' 51 | Resource: "*" 52 | - Effect: Allow 53 | Action: 54 | - 'ec2:DescribeInstances' 55 | - 'ec2:DescribeVolumes' 56 | - 'ec2:DescribeSecurityGroups' 57 | - 'ec2:DescribeAddresses' 58 | - 'ec2:DescribeInstanceAttribute' 59 | - 'ec2:DescribeVolumesModifications' 60 | - 'ec2:DescribeInstanceStatus' 61 | Resource: "*" 62 | - Effect: Allow 63 | Action: 64 | - 'ecr:Describe*' 65 | Resource: "*" 66 | - Effect: Allow 67 | Action: 68 | - 'ecs:ListClusters' 69 | - 'ecs:DescribeTaskDefinition' 70 | - 'ecs:ListTaskDefinitions' 71 | - 'ecs:DescribeTasks' 72 | - 'ecs:DescribeContainerInstances' 73 | - 'ecs:ListServices' 74 | - 'ecs:DescribeServices' 75 | Resource: "*" 76 | - Effect: Allow 77 | Action: 78 | - 'glue:GetConnections' 79 | - 'glue:GetSecurityConfiguration*' 80 | Resource: "*" 81 | - Effect: Allow 82 | Action: 83 | - 'iam:GetRole' 84 | - 'iam:List*' 85 | - 'iam:GetPolicy' 86 | - 'iam:GetPolicyVersion' 87 | - 'iam:GetAccountSummary' 88 | - 'iam:GetAccessKeyLastUsed' 89 | - 'iam:GetLoginProfile' 90 | Resource: "*" 91 | - Effect: Allow 92 | Action: 93 | - 'lambda:ListFunctions' 94 | - 'lambda:GetFunction*' 95 | - 'lambda:GetPolicy' 96 | - 'lambda:GetLayerVersion' 97 | - 'lambda:ListTags' 98 | Resource: "*" 99 | - Effect: Allow 100 | Action: 101 | - 'logs:FilterLogEvents' 102 | - 'logs:DescribeLogGroups' 103 | Resource: "*" 104 | - Effect: Allow 105 | Action: 106 | - 'macie2:GetMacieSession' 107 | Resource: "*" 108 | - Effect: Allow 109 | Action: 110 | - 's3:ListAllMyBuckets' 111 | - 's3:Get*' 112 | Resource: "*" 113 | - Effect: Allow 114 | Action: 115 | - 'securityhub:GetFindings' 116 | Resource: "*" 117 | - Effect: Allow 118 | Action: 119 | - 'ssm:GetDocument' 120 | - 'ssm-incidents:List*' 121 | Resource: "*" 122 | - Effect: Allow 123 | Action: 124 | - 'tag:GetTagKeys' 125 | Resource: "*" 126 | Tags: 127 | - Key: "Service" 128 | Value: "https://www.guard.dev" 129 | - Key: "Support" 130 | Value: "support@guard.dev" 131 | - Key: "CloudFormation" 132 | Value: "true" 133 | - Key: "Name" 134 | Value: "GuardSecurityScanRole" 135 | 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Guard.dev - Open Source Cloud Security Tool 2 | 3 | Guard.dev is an open-source, AI-powered cloud security tool designed to scan and secure your cloud environment by identifying misconfigurations and vulnerabilities across AWS. Using advanced large language models (LLMs), Guard.dev provides actionable insights and command-level fixes to enhance cloud security with minimal setup and maintenance. 4 | 5 | ## Key Features 6 | 7 | - **AWS Coverage**: Currently supports AWS services such as IAM, EC2, S3, Lambda, DynamoDB, and ECS, with more services coming soon. 8 | - **AI-Powered Remediation**: Automatically generates suggested command-line fixes and best practices for identified misconfigurations. 9 | - **Real-Time Scanning**: Continuously monitors cloud environments for the latest vulnerabilities and configuration issues. 10 | - **Extensible & Open Source**: Fully open-sourced to allow customization, integration, and community contributions. 11 | - **Flexible Deployment**: Deployable via Docker Compose for a quick and easy setup. 12 | 13 | ## Installation 14 | 15 | ### Prebuilt Docker Image 16 | 17 | 1. **Pull and Run the Docker Image**: 18 | 19 | ```bash 20 | docker run -d \ 21 | -p 3000:3000 \ 22 | -p 8080:8080 \ 23 | -e GEMINI_SECRET_KEY=your_gemini_secret_key \ 24 | -e AWS_REGION=us-west-2 \ 25 | -e AWS_ACCESS_KEY_ID=your_aws_access_key_id \ 26 | -e AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key \ 27 | ghcr.io/guard-dev/guard:latest 28 | ``` 29 | Attach the following policy to the IAM User: [templates/guard-self-host-policy.json](./templates/guard-self-host-policy.json). 30 | 31 | 2. **Access Guard.dev**: 32 | Open your browser and navigate to `http://localhost:3000`. 33 | 34 | ### Build From Source 35 | 36 | 1. **Clone the Repository**: 37 | 38 | ```bash 39 | git clone https://github.com/guard-dev/guard.git 40 | cd guard 41 | ``` 42 | 43 | 2. **Copy Environment Variables File**: 44 | 45 | ```bash 46 | cp example.env .env 47 | ``` 48 | 49 | - Edit the `.env` file to configure the necessary variables such as `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `GEMINI_SECRET_KEY`, etc. 50 | 51 | 3. **Run with Docker Compose**: 52 | 53 | ```bash 54 | ./start # Ensure that Docker is running and Python3 is installed. 55 | ``` 56 | 57 | 4. **Access Guard.dev**: 58 | Open your browser and navigate to `http://localhost:3000`. 59 | 60 | ## Environment Variable Setup 61 | 62 | To set up Guard.dev for use, you will need to configure the following services: 63 | 64 | ### 1. AWS Account Linking 65 | 66 | For self-hosting Guard.dev, follow these steps to set up an IAM User with the required permissions: 67 | 68 | 1. **Create an IAM User** in your AWS account with **programmatic access**. 69 | 2. **Attach the Policy** to the IAM User: 70 | - Download or copy the policy document from [templates/guard-self-host-policy.json](./templates/guard-self-host-policy.json). 71 | - Attach this policy to the IAM User during or after creation. 72 | 3. **Obtain Access Keys**: 73 | - After creating the IAM User, make sure to securely store the **Access Key ID** and **Secret Access Key**. 74 | 4. **Configure Environment Variables**: 75 | - Set the following environment variables in your hosting environment: 76 | - `AWS_ACCESS_KEY_ID` 77 | - `AWS_SECRET_ACCESS_KEY` 78 | 79 | This IAM User will have the necessary permissions for Guard to perform cloud security scans across your AWS resources. 80 | 81 | ### 2. LLM Scanning with Google Gemini 82 | 83 | Guard.dev leverages Google Gemini for LLM-based scans. 84 | 85 | - Follow the guide at [Google Gemini API Key Setup](https://ai.google.dev/gemini-api/docs/api-key) to obtain an API key. 86 | - Set the API key as an environment variable named `GEMINI_SECRET_KEY`. 87 | - Make sure the API key is properly secured and included in the `.env` file. 88 | 89 | ### Environment Variables 90 | 91 | Update the `.env` file with the following environment variables: 92 | 93 | - `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION` for AWS Integration. 94 | - `GEMINI_SECRET_KEY` for LLM scans. 95 | 96 | ## Usage 97 | 98 | 1. **Authenticate** with your AWS cloud account. 99 | 2. **Run Scans** on specific services or across the entire environment. 100 | 3. **Review Findings** and generated fixes for misconfigurations and vulnerabilities. 101 | 4. **Implement Fixes** using the provided commands or export a summary report. 102 | 103 | ## Supported Services 104 | 105 | ### AWS 106 | 107 | - IAM, EC2, S3, Lambda, DynamoDB, ECS (more services coming soon) 108 | 109 | ### GCP & Azure 110 | 111 | - Support for GCP and Azure is coming soon 112 | 113 | ## Contributing 114 | 115 | We welcome contributions from the community! Please refer to `CONTRIBUTING.md` for guidelines on how to get involved. 116 | 117 | ## License 118 | 119 | Guard.dev is released under the [Server Side Public License (SSPL)](LICENSE). This license allows you to view, modify, and self-host the software, but restricts using it to offer commercial services without open-sourcing your modifications. 120 | 121 | ## Get in Touch 122 | 123 | - **Website**: [www.guard.dev](https://www.guard.dev) 124 | - **Support**: 125 | -------------------------------------------------------------------------------- /frontend/components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { type DialogProps } from "@radix-ui/react-dialog" 5 | import { MagnifyingGlassIcon } from "@radix-ui/react-icons" 6 | import { Command as CommandPrimitive } from "cmdk" 7 | 8 | import { cn } from "@/lib/utils" 9 | import { Dialog, DialogContent } from "@/components/ui/dialog" 10 | 11 | const Command = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )) 24 | Command.displayName = CommandPrimitive.displayName 25 | 26 | interface CommandDialogProps extends DialogProps { } 27 | 28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => { 29 | return ( 30 | 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | ) 38 | } 39 | 40 | const CommandInput = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 |
45 | 46 | 54 |
55 | )) 56 | 57 | CommandInput.displayName = CommandPrimitive.Input.displayName 58 | 59 | const CommandList = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, ...props }, ref) => ( 63 | 68 | )) 69 | 70 | CommandList.displayName = CommandPrimitive.List.displayName 71 | 72 | const CommandEmpty = React.forwardRef< 73 | React.ElementRef, 74 | React.ComponentPropsWithoutRef 75 | >((props, ref) => ( 76 | 81 | )) 82 | 83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName 84 | 85 | const CommandGroup = React.forwardRef< 86 | React.ElementRef, 87 | React.ComponentPropsWithoutRef 88 | >(({ className, ...props }, ref) => ( 89 | 97 | )) 98 | 99 | CommandGroup.displayName = CommandPrimitive.Group.displayName 100 | 101 | const CommandSeparator = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName 112 | 113 | const CommandItem = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 125 | )) 126 | 127 | CommandItem.displayName = CommandPrimitive.Item.displayName 128 | 129 | const CommandShortcut = ({ 130 | className, 131 | ...props 132 | }: React.HTMLAttributes) => { 133 | return ( 134 | 141 | ) 142 | } 143 | CommandShortcut.displayName = "CommandShortcut" 144 | 145 | export { 146 | Command, 147 | CommandDialog, 148 | CommandInput, 149 | CommandList, 150 | CommandEmpty, 151 | CommandGroup, 152 | CommandItem, 153 | CommandShortcut, 154 | CommandSeparator, 155 | } 156 | -------------------------------------------------------------------------------- /backend/database/postgres/query.sql: -------------------------------------------------------------------------------- 1 | 2 | -------------------- UserInfo Queries -------------------- 3 | 4 | -- name: AddUser :one 5 | INSERT INTO user_info (email, full_name) VALUES ($1, $2) RETURNING *; 6 | 7 | -- name: GetUserByEmail :one 8 | SELECT * FROM user_info WHERE email = $1 LIMIT 1; 9 | 10 | -- name: GetUserById :one 11 | SELECT * FROM user_info WHERE user_id = $1 LIMIT 1; 12 | 13 | -- name: GetExternalIdByEmail :one 14 | SELECT external_id FROM user_info WHERE email = $1; 15 | 16 | 17 | 18 | -------------------- Team Queries -------------------- 19 | 20 | -- name: GetTeamByTeamSlug :one 21 | SELECT * FROM team WHERE team_slug = $1 LIMIT 1; 22 | 23 | -- name: GetTeamByTeamId :one 24 | SELECT * FROM team WHERE team_id = $1 LIMIT 1; 25 | 26 | -- name: CreateNewTeam :one 27 | INSERT INTO team (team_slug, team_name) VALUES ($1, $2) RETURNING *; 28 | 29 | -- name: GetTeamsByUserId :many 30 | SELECT team.* 31 | FROM team 32 | JOIN team_membership on team.team_id = team_membership.team_id 33 | WHERE team_membership.user_id = $1 34 | ORDER BY team.created; 35 | 36 | -- name: UpdateTeamStripeCustomerIdByTeamId :one 37 | UPDATE team SET stripe_customer_id = $2 WHERE team_id = $1 RETURNING *; 38 | 39 | -- name: GetTeamByStripeCustomerId :one 40 | SELECT * FROM team WHERE stripe_customer_id = $1; 41 | 42 | 43 | 44 | 45 | 46 | -------------------- TeamMembership Queries -------------------- 47 | 48 | -- name: AddTeamMembership :one 49 | INSERT INTO team_membership (team_id, user_id, membership_type) 50 | VALUES ($1, $2, $3) RETURNING *; 51 | 52 | -- name: GetTeamMembershipByTeamIdUserId :one 53 | SELECT * FROM team_membership WHERE team_id = $1 AND user_id = $2 LIMIT 1; 54 | 55 | -- name: GetTeamMembershipsByTeamId :many 56 | SELECT * FROM team_membership WHERE team_id = $1 ORDER BY created; 57 | 58 | 59 | 60 | 61 | -------------------- Project Queries -------------------- 62 | 63 | -- name: CreateNewProject :one 64 | INSERT INTO project ( 65 | team_id, 66 | project_slug, 67 | project_name 68 | ) VALUES ($1, $2, $3) RETURNING *; 69 | 70 | -- name: GetProjectsByTeamId :many 71 | SELECT * FROM project WHERE team_id = $1; 72 | 73 | -- name: GetProjectByTeamIdAndProjectSlug :one 74 | SELECT * FROM project WHERE team_id = $1 AND project_slug = $2; 75 | 76 | -- name: DeleteProjectByTeamIdAndProjectSlug :one 77 | DELETE FROM project WHERE team_id = $1 AND project_slug = $2 RETURNING *; 78 | 79 | -- name: GetProjectByProjectId :one 80 | SELECT * FROM project WHERE project_id = $1; 81 | 82 | 83 | -------------------- Account Connection Queries -------------------- 84 | 85 | -- name: CreateAccountConnection :one 86 | INSERT INTO account_connection ( 87 | project_id, 88 | external_id, 89 | account_id 90 | ) VALUES ($1, $2, $3) RETURNING *; 91 | 92 | -- name: GetConnectionsByProjectId :many 93 | SELECT * FROM account_connection WHERE project_id = $1; 94 | 95 | 96 | -------------------- Scan Queries -------------------- 97 | 98 | -- name: CreateNewScan :one 99 | INSERT INTO scan (project_id, region_count, service_count, services, regions) VALUES ($1, $2, $3, $4, $5) RETURNING *; 100 | 101 | -- name: IncrementScanResourceCostByScanId :one 102 | UPDATE scan SET resource_cost = resource_cost + $1 WHERE scan_id = $2 RETURNING *; 103 | 104 | 105 | -- name: GetScansByProjectId :many 106 | SELECT * FROM scan WHERE project_id = $1; 107 | 108 | -- name: UpdateScanCompletedStatus :one 109 | UPDATE scan SET scan_completed = $1 WHERE scan_id = $2 RETURNING *; 110 | 111 | -- name: GetScanByScanIdProjectId :one 112 | SELECT * FROM scan WHERE project_id = $1 AND scan_id = $2; 113 | 114 | -------------------- Scan Item Queries -------------------- 115 | 116 | -- name: GetScanItemByScanItemId :one 117 | SELECT * FROM scan_item WHERE scan_id = $1; 118 | 119 | -- name: CreateNewScanItem :one 120 | INSERT INTO scan_item ( 121 | scan_id, 122 | service, 123 | region, 124 | findings, 125 | summary, 126 | remedy 127 | ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; 128 | 129 | -- name: IncrementScanItemResourceCostByScanItemid :one 130 | UPDATE scan_item SET resource_cost = resource_cost + $1 WHERE scan_item_id = $2 RETURNING *; 131 | 132 | -- name: GetScanItemsByScanId :many 133 | SELECT * FROM scan_item WHERE scan_id = $1; 134 | 135 | -- name: GetScanItemByScanIdAndScanItemId :one 136 | SELECT * FROM scan_item WHERE scan_id = $1 AND scan_item_id = $2; 137 | 138 | -------------------- Scan Item Entry Queries -------------------- 139 | 140 | -- name: CreateNewScanItemEntry :one 141 | INSERT INTO scan_item_entry ( 142 | scan_item_id, 143 | findings, 144 | title, 145 | summary, 146 | remedy, 147 | commands 148 | ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; 149 | 150 | -- name: GetScanItemEntriesByScanId :many 151 | SELECT * FROM scan_item_entry WHERE scan_item_id = $1; 152 | 153 | 154 | 155 | -------------------- Subscription Plan Queries -------------------- 156 | 157 | -- name: IncrementSubscriptionResourcesUsedByTeamId :one 158 | UPDATE subscription_plan SET resources_used = resources_used + $1 WHERE team_id = $2 RETURNING *; 159 | 160 | -- name: CreateSubscription :one 161 | INSERT INTO subscription_plan 162 | (team_id, stripe_subscription_id, resources_included) 163 | VALUES ($1, $2, $3) RETURNING *; 164 | 165 | -- name: GetSubscriptionByTeamId :one 166 | SELECT * FROM subscription_plan WHERE team_id = $1 ORDER BY created LIMIT 1; 167 | 168 | -- name: GetSubscriptionByTeamIdSubscriptionId :one 169 | SELECT * FROM subscription_plan WHERE team_id = $1 AND id = $2 LIMIT 1; 170 | 171 | -- name: GetSubscriptionById :one 172 | SELECT * FROM subscription_plan WHERE id = $1 LIMIT 1; 173 | 174 | -- name: GetSubscriptionByStripeSubscriptionId :one 175 | SELECT * FROM subscription_plan WHERE stripe_subscription_id = $1 LIMIT 1; 176 | 177 | -- name: SetSubscriptionStripeIdByTeamId :one 178 | UPDATE subscription_plan SET stripe_subscription_id = $2 WHERE team_id = $1 RETURNING *; 179 | 180 | -- name: DeleteSubscriptionByStripeSubscriptionId :one 181 | DELETE FROM subscription_plan WHERE stripe_subscription_id = $1 RETURNING *; 182 | 183 | -- name: ResetSubscriptionResourcesUsed :one 184 | UPDATE subscription_plan 185 | SET resources_used = 0 186 | WHERE team_id = $1 187 | RETURNING *; 188 | 189 | -------------------------------------------------------------------------------- /frontend/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { 5 | CaretSortIcon, 6 | CheckIcon, 7 | ChevronDownIcon, 8 | ChevronUpIcon, 9 | } from "@radix-ui/react-icons" 10 | import * as SelectPrimitive from "@radix-ui/react-select" 11 | 12 | import { cn } from "@/lib/utils" 13 | 14 | const Select = SelectPrimitive.Root 15 | 16 | const SelectGroup = SelectPrimitive.Group 17 | 18 | const SelectValue = SelectPrimitive.Value 19 | 20 | const SelectTrigger = React.forwardRef< 21 | React.ElementRef, 22 | React.ComponentPropsWithoutRef 23 | >(({ className, children, ...props }, ref) => ( 24 | span]:line-clamp-1", 28 | className 29 | )} 30 | {...props} 31 | > 32 | {children} 33 | 34 | 35 | 36 | 37 | )) 38 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 39 | 40 | const SelectScrollUpButton = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | 53 | 54 | )) 55 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 56 | 57 | const SelectScrollDownButton = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, ref) => ( 61 | 69 | 70 | 71 | )) 72 | SelectScrollDownButton.displayName = 73 | SelectPrimitive.ScrollDownButton.displayName 74 | 75 | const SelectContent = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef 78 | >(({ className, children, position = "popper", ...props }, ref) => ( 79 | 80 | 91 | 92 | 99 | {children} 100 | 101 | 102 | 103 | 104 | )) 105 | SelectContent.displayName = SelectPrimitive.Content.displayName 106 | 107 | const SelectLabel = React.forwardRef< 108 | React.ElementRef, 109 | React.ComponentPropsWithoutRef 110 | >(({ className, ...props }, ref) => ( 111 | 116 | )) 117 | SelectLabel.displayName = SelectPrimitive.Label.displayName 118 | 119 | const SelectItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | SelectItem.displayName = SelectPrimitive.Item.displayName 140 | 141 | const SelectSeparator = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef 144 | >(({ className, ...props }, ref) => ( 145 | 150 | )) 151 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 152 | 153 | export { 154 | Select, 155 | SelectGroup, 156 | SelectValue, 157 | SelectTrigger, 158 | SelectContent, 159 | SelectLabel, 160 | SelectItem, 161 | SelectSeparator, 162 | SelectScrollUpButton, 163 | SelectScrollDownButton, 164 | } 165 | -------------------------------------------------------------------------------- /frontend/components/PricingTable.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { Button } from "@/components/ui/button"; 5 | 6 | interface PricingTableProps { 7 | handleCheckout?: (lookupString: string) => any; 8 | }; 9 | 10 | const PricingTable: React.FC = ({ handleCheckout }) => { 11 | const plans = [ 12 | { 13 | name: 'Open Source', 14 | price: 'Free', 15 | extraPrice: '-', 16 | resources: 'Unlimited Resources', 17 | support: 'Community Support', 18 | features: [], 19 | cta: 'Self Host', 20 | lookupKey: '', 21 | href: "https://www.github.com/guard-dev/guard" 22 | }, 23 | { 24 | name: 'Basic', 25 | price: '$150/mo', 26 | extraPrice: '$6 per 100 extra resources', 27 | resources: '1,000 Resources Included', 28 | support: 'Standard Support', 29 | features: [], 30 | cta: 'Get Started', 31 | lookupKey: 'guard_basic_monthly', 32 | href: "/console" 33 | }, 34 | { 35 | name: 'Pro', 36 | price: '$400/mo', 37 | extraPrice: '$5 per 100 extra resources', 38 | resources: '5,000 Resources Included', 39 | support: 'Priority Support', 40 | features: [], 41 | cta: 'Get Started', 42 | lookupKey: 'guard_pro_monthly', 43 | href: "/console" 44 | }, 45 | { 46 | name: 'Enterprise', 47 | price: 'Contact Us', 48 | extraPrice: '', 49 | resources: 'Unlimited Resources', 50 | support: 'Dedicated Support', 51 | features: [], 52 | cta: 'Contact Us', 53 | lookupKey: '', 54 | href: "https://www.cal.com/guard" 55 | }, 56 | ]; 57 | 58 | 59 | return ( 60 |
61 |

Pricing Plans

62 |
63 | {plans.map((plan) => ( 64 |
69 |
70 |

{plan.name}

71 |
72 | {plan.price} 73 |
74 | {plan.extraPrice !== '-' && plan.extraPrice !== '' && ( 75 |
76 | Extra: {plan.extraPrice} 77 |
78 | )} 79 |
80 | 81 |
82 |
83 |
84 | {plan.resources} 85 |
86 |
87 |
88 | {plan.support} 89 |
90 |
91 | 92 | {handleCheckout && plan.lookupKey ? ( 93 | 103 | ) : ( 104 | 110 | 119 | 120 | )} 121 |
122 | ))} 123 |
124 |
125 | ); 126 | }; 127 | 128 | export default PricingTable; 129 | -------------------------------------------------------------------------------- /frontend/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | //import PricingTable from "@/components/PricingTable"; 3 | import { IconCloudSecurity, IconAI, IconRealTime, IconActionable } from '@/components/Icons'; 4 | import Navbar from "./console/Navbar"; 5 | import { FooterSection } from "./console/Footer"; 6 | 7 | export default function Home() { 8 | return ( 9 |
10 | 11 | 12 | {/* Hero Section */} 13 |
14 |
15 | 16 |
17 |
18 | 19 | {/* Features Section */} 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 | 28 | {/* Pricing Section 29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 | */} 37 | 38 |
39 | 40 |
41 |
42 | ); 43 | } 44 | 45 | const HeroSection = () => { 46 | return ( 47 |
48 |
49 |

50 | guard 51 |

52 |

53 | AI-Powered Cloud Security, Simplified. 54 |

55 |

56 | Detect AWS misconfigurations in real-time and fix them with AI-driven, actionable insights. 57 |

58 |
59 | 60 | 77 | 78 |
79 |
80 |