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 |
84 |
85 |
86 | Back to All Scans
87 |
88 |
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 |
handleCheckout(plan.lookupKey)}
100 | >
101 | {plan.cta}
102 |
103 | ) : (
104 |
110 |
117 | {plan.cta}
118 |
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 |
18 |
19 | {/* Features Section */}
20 |
27 |
28 | {/* Pricing Section
29 |
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 |
88 |
89 |
90 | );
91 | }
92 |
93 | const FeaturesSection = () => {
94 | const features = [
95 | {
96 | icon: ,
97 | title: "Cloud Security Scanning",
98 | description: "Comprehensive scanning of your AWS resources to identify potential security risks and misconfigurations."
99 | },
100 | {
101 | icon: ,
102 | title: "AI-Powered Analysis",
103 | description: "Leverage advanced LLMs to process and analyze your cloud infrastructure for deeper insights."
104 | },
105 | {
106 | icon: ,
107 | title: "Real-Time Detection",
108 | description: "Instantly identify vulnerabilities and security issues as they arise in your AWS environment."
109 | },
110 | {
111 | icon: ,
112 | title: "Actionable Insights",
113 | description: "Receive clear, actionable recommendations to improve your cloud security posture."
114 | }
115 | ];
116 |
117 | return (
118 |
119 |
Features
120 |
121 | {features.map((feature, index) => (
122 |
123 | {feature.icon}
124 |
{feature.title}
125 |
{feature.description}
126 |
127 | ))}
128 |
129 |
130 | );
131 | }
132 |
--------------------------------------------------------------------------------
/backend/awsmiddleware/dynamodbscanner/scanner.go:
--------------------------------------------------------------------------------
1 | package dynamodbscanner
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "guarddev/database/postgres"
7 |
8 | "go.opentelemetry.io/otel"
9 | "go.uber.org/zap"
10 | )
11 |
12 | func (scanner *DynamoDBScanner) ScanDynamoDB(ctx context.Context, region string) ([]string, []postgres.CreateNewScanItemEntryParams, error) {
13 | tracer := otel.Tracer("dynamodbscanner/ScanDynamoDB")
14 | ctx, span := tracer.Start(ctx, "ScanDynamoDB")
15 | defer span.End()
16 |
17 | var allFindings []string
18 |
19 | tables, err := scanner.ListTables(ctx)
20 | if err != nil {
21 | span.RecordError(err)
22 | return nil, nil, err
23 | }
24 |
25 | // Categorize findings by type
26 | var encryptionFindings, pitrFindings, autoScalingFindings, ttlFindings, unusedIndexesFindings []string
27 |
28 | for _, tableName := range tables {
29 | // Check Table Encryption
30 | encryptionResult, err := scanner.CheckTableEncryption(ctx, *tableName)
31 | if err != nil {
32 | encryptionFindings = append(encryptionFindings, fmt.Sprintf("Error checking encryption for table %s: %v", *tableName, err))
33 | } else {
34 | encryptionFindings = append(encryptionFindings, encryptionResult...)
35 | }
36 |
37 | // Check Point-in-Time Recovery (PITR)
38 | pitrResult, err := scanner.CheckPointInTimeRecovery(ctx, *tableName)
39 | if err != nil {
40 | pitrFindings = append(pitrFindings, fmt.Sprintf("Error checking point-in-time recovery for table %s: %v", *tableName, err))
41 | } else {
42 | pitrFindings = append(pitrFindings, pitrResult...)
43 | }
44 |
45 | // Check Auto Scaling
46 | autoScalingResult, err := scanner.CheckAutoScaling(ctx, *tableName)
47 | if err != nil {
48 | autoScalingFindings = append(autoScalingFindings, fmt.Sprintf("Error checking auto scaling for table %s: %v", *tableName, err))
49 | } else {
50 | autoScalingFindings = append(autoScalingFindings, autoScalingResult...)
51 | }
52 |
53 | // Check TTL Settings
54 | ttlResult, err := scanner.CheckTTLStatus(ctx, *tableName)
55 | if err != nil {
56 | ttlFindings = append(ttlFindings, fmt.Sprintf("Error checking TTL for table %s: %v", *tableName, err))
57 | } else {
58 | ttlFindings = append(ttlFindings, ttlResult...)
59 | }
60 |
61 | // Check Unused Indexes
62 | unusedIndexesResult, err := scanner.CheckUnusedIndexes(ctx, *tableName)
63 | if err != nil {
64 | unusedIndexesFindings = append(unusedIndexesFindings, fmt.Sprintf("Error checking unused indexes for table %s: %v", *tableName, err))
65 | } else {
66 | unusedIndexesFindings = append(unusedIndexesFindings, unusedIndexesResult...)
67 | }
68 | }
69 |
70 | // Process and summarize categorized findings using LLM
71 | scanItems, err := scanner.processAndSummarizeDynamoDBFindings(
72 | ctx,
73 | "dynamodb",
74 | region,
75 | encryptionFindings,
76 | pitrFindings,
77 | autoScalingFindings,
78 | ttlFindings,
79 | unusedIndexesFindings,
80 | )
81 | if err != nil {
82 | span.RecordError(err)
83 | return nil, nil, err
84 | }
85 |
86 | // Append all categorized findings to allFindings
87 | allFindings = append(allFindings, encryptionFindings...)
88 | allFindings = append(allFindings, pitrFindings...)
89 | allFindings = append(allFindings, autoScalingFindings...)
90 | allFindings = append(allFindings, ttlFindings...)
91 | allFindings = append(allFindings, unusedIndexesFindings...)
92 |
93 | return allFindings, scanItems, nil
94 | }
95 |
96 | func (scanner *DynamoDBScanner) processAndSummarizeDynamoDBFindings(
97 | ctx context.Context,
98 | service string,
99 | region string,
100 | encryptionFindings []string,
101 | pitrFindings []string,
102 | autoScalingFindings []string,
103 | ttlFindings []string,
104 | unusedIndexesFindings []string,
105 | ) ([]postgres.CreateNewScanItemEntryParams, error) {
106 | tracer := otel.Tracer("dynamodbscanner/processAndSummarizeDynamoDBFindings")
107 | ctx, span := tracer.Start(ctx, "processAndSummarizeDynamoDBFindings")
108 | defer span.End()
109 |
110 | scanItems := []postgres.CreateNewScanItemEntryParams{}
111 |
112 | if len(encryptionFindings) > 0 {
113 | encryptionSummary, err := scanner.Gemini.SummarizeFindings(ctx, service, region, scanner.accountId, encryptionFindings)
114 | if err != nil {
115 | span.RecordError(err)
116 | return nil, fmt.Errorf("error summarizing encryption findings: %v", err)
117 | }
118 | scanItems = append(scanItems, postgres.CreateNewScanItemEntryParams{
119 | Findings: encryptionFindings,
120 | Title: "Encryption Settings",
121 | Summary: encryptionSummary.Summary,
122 | Remedy: encryptionSummary.Remedies,
123 | Commands: encryptionSummary.Commands,
124 | })
125 | scanner.logger.Logger(ctx).Info("Encryption Settings", zap.Any("Encryption Summary", encryptionSummary))
126 | }
127 |
128 | if len(pitrFindings) > 0 {
129 | pitrSummary, err := scanner.Gemini.SummarizeFindings(ctx, service, region, scanner.accountId, pitrFindings)
130 | if err != nil {
131 | span.RecordError(err)
132 | return nil, fmt.Errorf("error summarizing PITR findings: %v", err)
133 | }
134 | scanItems = append(scanItems, postgres.CreateNewScanItemEntryParams{
135 | Findings: pitrFindings,
136 | Title: "Point-in-Time Recovery (PITR)",
137 | Summary: pitrSummary.Summary,
138 | Remedy: pitrSummary.Remedies,
139 | Commands: pitrSummary.Commands,
140 | })
141 | scanner.logger.Logger(ctx).Info("Point-in-Time Recovery (PITR)", zap.Any("PITR Summary", pitrSummary))
142 | }
143 |
144 | if len(autoScalingFindings) > 0 {
145 | autoScalingSummary, err := scanner.Gemini.SummarizeFindings(ctx, service, region, scanner.accountId, autoScalingFindings)
146 | if err != nil {
147 | span.RecordError(err)
148 | return nil, fmt.Errorf("error summarizing auto scaling findings: %v", err)
149 | }
150 | scanItems = append(scanItems, postgres.CreateNewScanItemEntryParams{
151 | Findings: autoScalingFindings,
152 | Title: "Auto Scaling Settings",
153 | Summary: autoScalingSummary.Summary,
154 | Remedy: autoScalingSummary.Remedies,
155 | Commands: autoScalingSummary.Commands,
156 | })
157 | scanner.logger.Logger(ctx).Info("Auto Scaling Settings", zap.Any("Auto Scaling Summary", autoScalingSummary))
158 | }
159 |
160 | if len(ttlFindings) > 0 {
161 | ttlSummary, err := scanner.Gemini.SummarizeFindings(ctx, service, region, scanner.accountId, ttlFindings)
162 | if err != nil {
163 | span.RecordError(err)
164 | return nil, fmt.Errorf("error summarizing TTL findings: %v", err)
165 | }
166 | scanItems = append(scanItems, postgres.CreateNewScanItemEntryParams{
167 | Findings: ttlFindings,
168 | Title: "Time-to-Live (TTL) Settings",
169 | Summary: ttlSummary.Summary,
170 | Remedy: ttlSummary.Remedies,
171 | Commands: ttlSummary.Commands,
172 | })
173 | scanner.logger.Logger(ctx).Info("Time-to-Live (TTL) Settings", zap.Any("TTL Summary", ttlSummary))
174 | }
175 |
176 | if len(unusedIndexesFindings) > 0 {
177 | unusedIndexesSummary, err := scanner.Gemini.SummarizeFindings(ctx, service, region, scanner.accountId, unusedIndexesFindings)
178 | if err != nil {
179 | span.RecordError(err)
180 | return nil, fmt.Errorf("error summarizing unused indexes findings: %v", err)
181 | }
182 | scanItems = append(scanItems, postgres.CreateNewScanItemEntryParams{
183 | Findings: unusedIndexesFindings,
184 | Title: "Unused Indexes",
185 | Summary: unusedIndexesSummary.Summary,
186 | Remedy: unusedIndexesSummary.Remedies,
187 | Commands: unusedIndexesSummary.Commands,
188 | })
189 | scanner.logger.Logger(ctx).Info("Unused Indexes", zap.Any("Unused Indexes Summary", unusedIndexesSummary))
190 | }
191 |
192 | return scanItems, nil
193 | }
194 |
--------------------------------------------------------------------------------
/backend/awsmiddleware/dynamodbscanner/main.go:
--------------------------------------------------------------------------------
1 | package dynamodbscanner
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "guarddev/logger"
7 | "guarddev/modelapi"
8 |
9 | "github.com/aws/aws-sdk-go/aws"
10 | "github.com/aws/aws-sdk-go/aws/credentials"
11 | "github.com/aws/aws-sdk-go/aws/session"
12 | "github.com/aws/aws-sdk-go/service/dynamodb"
13 | "go.opentelemetry.io/otel"
14 | "go.opentelemetry.io/otel/attribute"
15 | )
16 |
17 | type DynamoDBScanner struct {
18 | dynamodbClient *dynamodb.DynamoDB
19 | logger *logger.LogMiddleware
20 | Gemini modelapi.ModelAPI
21 | accountId string
22 | }
23 |
24 | // Initialize a DynamoDB scanner with assumed credentials
25 | func NewDynamoDBScanner(accessKey, secretKey, sessionToken, region string, accountId string, logger *logger.LogMiddleware, gemini modelapi.ModelAPI) *DynamoDBScanner {
26 | sess := session.Must(session.NewSession(&aws.Config{
27 | Region: aws.String(region),
28 | Credentials: credentials.NewStaticCredentials(
29 | accessKey,
30 | secretKey,
31 | sessionToken,
32 | ),
33 | }))
34 | return &DynamoDBScanner{
35 | dynamodbClient: dynamodb.New(sess),
36 | logger: logger,
37 | Gemini: gemini,
38 | accountId: accountId,
39 | }
40 | }
41 |
42 | // List all DynamoDB tables
43 | func (scanner *DynamoDBScanner) ListTables(ctx context.Context) ([]*string, error) {
44 | tracer := otel.Tracer("dynamodbscanner/ListTables")
45 | ctx, span := tracer.Start(ctx, "ListTables")
46 | defer span.End()
47 |
48 | // List DynamoDB tables
49 | result, err := scanner.dynamodbClient.ListTables(&dynamodb.ListTablesInput{})
50 | if err != nil {
51 | span.RecordError(err)
52 | return nil, fmt.Errorf("error listing DynamoDB tables: %v", err)
53 | }
54 |
55 | span.SetAttributes(attribute.Int("table_count", len(result.TableNames)))
56 | return result.TableNames, nil
57 | }
58 |
59 | // Check if table encryption is enabled
60 | func (scanner *DynamoDBScanner) CheckTableEncryption(ctx context.Context, tableName string) ([]string, error) {
61 | tracer := otel.Tracer("dynamodbscanner/CheckTableEncryption")
62 | ctx, span := tracer.Start(ctx, "CheckTableEncryption")
63 | defer span.End()
64 |
65 | var findings []string
66 |
67 | result, err := scanner.dynamodbClient.DescribeTable(&dynamodb.DescribeTableInput{
68 | TableName: aws.String(tableName),
69 | })
70 | if err != nil {
71 | span.RecordError(err)
72 | return nil, fmt.Errorf("error describing table %s: %v", tableName, err)
73 | }
74 |
75 | if result.Table.SSEDescription == nil || aws.StringValue(result.Table.SSEDescription.Status) != "ENABLED" {
76 | findings = append(findings, fmt.Sprintf("WARNING: DynamoDB table %s does not have encryption enabled.", tableName))
77 | }
78 |
79 | if result.Table.SSEDescription != nil {
80 | span.SetAttributes(attribute.String("table_encryption", aws.StringValue(result.Table.SSEDescription.Status)))
81 | }
82 | return findings, nil
83 | }
84 |
85 | // Check if Point-in-Time Recovery (PITR) is enabled
86 | func (scanner *DynamoDBScanner) CheckPointInTimeRecovery(ctx context.Context, tableName string) ([]string, error) {
87 | tracer := otel.Tracer("dynamodbscanner/CheckPointInTimeRecovery")
88 | ctx, span := tracer.Start(ctx, "CheckPointInTimeRecovery")
89 | defer span.End()
90 |
91 | var findings []string
92 |
93 | result, err := scanner.dynamodbClient.DescribeContinuousBackups(&dynamodb.DescribeContinuousBackupsInput{
94 | TableName: aws.String(tableName),
95 | })
96 | if err != nil {
97 | span.RecordError(err)
98 | return nil, fmt.Errorf("error describing continuous backups for table %s: %v", tableName, err)
99 | }
100 |
101 | if result.ContinuousBackupsDescription.PointInTimeRecoveryDescription == nil || aws.StringValue(result.ContinuousBackupsDescription.PointInTimeRecoveryDescription.PointInTimeRecoveryStatus) != "ENABLED" {
102 | findings = append(findings, fmt.Sprintf("WARNING: Point-in-time recovery is not enabled for DynamoDB table %s.", tableName))
103 | }
104 |
105 | if result.ContinuousBackupsDescription.PointInTimeRecoveryDescription != nil {
106 | span.SetAttributes(attribute.String("pitr_status", aws.StringValue(result.ContinuousBackupsDescription.PointInTimeRecoveryDescription.PointInTimeRecoveryStatus)))
107 | }
108 | return findings, nil
109 | }
110 |
111 | // Check if Auto Scaling is enabled for a table
112 | func (scanner *DynamoDBScanner) CheckAutoScaling(ctx context.Context, tableName string) ([]string, error) {
113 | tracer := otel.Tracer("dynamodbscanner/CheckAutoScaling")
114 | ctx, span := tracer.Start(ctx, "CheckAutoScaling")
115 | defer span.End()
116 |
117 | var findings []string
118 |
119 | // Describe auto scaling settings for the table
120 | result, err := scanner.dynamodbClient.DescribeTable(&dynamodb.DescribeTableInput{
121 | TableName: aws.String(tableName),
122 | })
123 | if err != nil {
124 | span.RecordError(err)
125 | return nil, fmt.Errorf("error describing table %s: %v", tableName, err)
126 | }
127 |
128 | if result.Table.ProvisionedThroughput != nil && aws.Int64Value(result.Table.ProvisionedThroughput.ReadCapacityUnits) > 0 {
129 | findings = append(findings, fmt.Sprintf("Auto Scaling is enabled for DynamoDB table %s with Read Capacity Units: %d, Write Capacity Units: %d.", tableName, aws.Int64Value(result.Table.ProvisionedThroughput.ReadCapacityUnits), aws.Int64Value(result.Table.ProvisionedThroughput.WriteCapacityUnits)))
130 | } else {
131 | findings = append(findings, fmt.Sprintf("WARNING: Auto Scaling is not enabled for DynamoDB table %s.", tableName))
132 | }
133 |
134 | span.SetAttributes(attribute.Bool("auto_scaling_enabled", len(findings) == 0))
135 | return findings, nil
136 | }
137 |
138 | // Check for unused indexes in a DynamoDB table
139 | func (scanner *DynamoDBScanner) CheckUnusedIndexes(ctx context.Context, tableName string) ([]string, error) {
140 | tracer := otel.Tracer("dynamodbscanner/CheckUnusedIndexes")
141 | ctx, span := tracer.Start(ctx, "CheckUnusedIndexes")
142 | defer span.End()
143 |
144 | var findings []string
145 |
146 | result, err := scanner.dynamodbClient.DescribeTable(&dynamodb.DescribeTableInput{
147 | TableName: aws.String(tableName),
148 | })
149 | if err != nil {
150 | span.RecordError(err)
151 | return nil, fmt.Errorf("error describing table %s: %v", tableName, err)
152 | }
153 |
154 | if len(result.Table.GlobalSecondaryIndexes) == 0 {
155 | findings = append(findings, fmt.Sprintf("No Global Secondary Indexes found for DynamoDB table %s.", tableName))
156 | } else {
157 | for _, gsi := range result.Table.GlobalSecondaryIndexes {
158 | if aws.StringValue(gsi.IndexStatus) != "ACTIVE" {
159 | findings = append(findings, fmt.Sprintf("WARNING: Global Secondary Index %s on table %s is not active or unused.", aws.StringValue(gsi.IndexName), tableName))
160 | }
161 | }
162 | }
163 |
164 | span.SetAttributes(attribute.Int("gsi_count", len(result.Table.GlobalSecondaryIndexes)))
165 | return findings, nil
166 | }
167 |
168 | // Check TTL (Time to Live) status on a table
169 | func (scanner *DynamoDBScanner) CheckTTLStatus(ctx context.Context, tableName string) ([]string, error) {
170 | tracer := otel.Tracer("dynamodbscanner/CheckTTLStatus")
171 | ctx, span := tracer.Start(ctx, "CheckTTLStatus")
172 | defer span.End()
173 |
174 | var findings []string
175 |
176 | result, err := scanner.dynamodbClient.DescribeTimeToLive(&dynamodb.DescribeTimeToLiveInput{
177 | TableName: aws.String(tableName),
178 | })
179 | if err != nil {
180 | span.RecordError(err)
181 | return nil, fmt.Errorf("error describing TTL for table %s: %v", tableName, err)
182 | }
183 |
184 | if result.TimeToLiveDescription == nil || aws.StringValue(result.TimeToLiveDescription.TimeToLiveStatus) != "ENABLED" {
185 | findings = append(findings, fmt.Sprintf("WARNING: TTL is not enabled for DynamoDB table %s.", tableName))
186 | }
187 |
188 | if result.TimeToLiveDescription != nil {
189 | span.SetAttributes(attribute.String("ttl_status", aws.StringValue(result.TimeToLiveDescription.TimeToLiveStatus)))
190 | }
191 | return findings, nil
192 | }
193 |
--------------------------------------------------------------------------------
/backend/awsmiddleware/lambdascanner/scanner.go:
--------------------------------------------------------------------------------
1 | package lambdascanner
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "guarddev/database/postgres"
7 |
8 | "github.com/aws/aws-sdk-go/aws"
9 | "go.opentelemetry.io/otel"
10 | "go.opentelemetry.io/otel/attribute"
11 | "go.uber.org/zap"
12 | )
13 |
14 | func (scanner *LambdaScanner) ScanLambda(ctx context.Context, region string) ([]string, []postgres.CreateNewScanItemEntryParams, error) {
15 | tracer := otel.Tracer("lambdascanner/ScanLambda")
16 | ctx, span := tracer.Start(ctx, "ScanLambda")
17 | defer span.End()
18 |
19 | var allFindings []string
20 |
21 | functions, err := scanner.ListFunctions(ctx)
22 | if err != nil {
23 | span.RecordError(err)
24 | return nil, nil, err
25 | }
26 |
27 | span.SetAttributes(attribute.Int("function_count", len(functions)))
28 |
29 | // Categorize findings by type
30 | var iamRoleFindings, envVariableFindings, cloudWatchLogFindings, timeoutFindings, vpcFindings []string
31 |
32 | for _, function := range functions {
33 | allFindings = append(allFindings, fmt.Sprintf("Found Lambda Function: %s", aws.StringValue(function.FunctionName)))
34 |
35 | // Check IAM role attached to Lambda function
36 | iamFindings, err := scanner.CheckFunctionIAMRole(ctx, function)
37 | if err != nil {
38 | span.RecordError(err)
39 | iamRoleFindings = append(iamRoleFindings, fmt.Sprintf("Error checking IAM role for function %s: %v", aws.StringValue(function.FunctionName), err))
40 | } else {
41 | iamRoleFindings = append(iamRoleFindings, iamFindings...)
42 | }
43 |
44 | // Check for sensitive environment variables in Lambda function
45 | envFindings := scanner.CheckSensitiveEnvironmentVariables(ctx, function)
46 | envVariableFindings = append(envVariableFindings, envFindings...)
47 |
48 | // Check if CloudWatch Logs is properly configured
49 | logFindings, err := scanner.CheckCloudWatchLogs(ctx, function)
50 | if err != nil {
51 | span.RecordError(err)
52 | cloudWatchLogFindings = append(cloudWatchLogFindings, fmt.Sprintf("Error checking CloudWatch logs for function %s: %v", aws.StringValue(function.FunctionName), err))
53 | } else {
54 | cloudWatchLogFindings = append(cloudWatchLogFindings, logFindings...)
55 | }
56 |
57 | // Check Lambda function timeout configuration
58 | timeoutConfigFindings := scanner.CheckFunctionTimeout(ctx, function)
59 | timeoutFindings = append(timeoutFindings, timeoutConfigFindings...)
60 |
61 | // Check if Lambda function is configured in a VPC
62 | vpcConfigFindings := scanner.CheckVpcConfiguration(ctx, function)
63 | vpcFindings = append(vpcFindings, vpcConfigFindings...)
64 | }
65 |
66 | // Process and summarize categorized findings using LLM
67 | scanItems, err := scanner.processAndSummarizeFindings(
68 | ctx,
69 | "lambda",
70 | region,
71 | scanner.accountId,
72 | iamRoleFindings,
73 | envVariableFindings,
74 | cloudWatchLogFindings,
75 | timeoutFindings,
76 | vpcFindings,
77 | )
78 | if err != nil {
79 | span.RecordError(err)
80 | return nil, nil, err
81 | }
82 |
83 | // Append all categorized findings to allFindings
84 | allFindings = append(allFindings, iamRoleFindings...)
85 | allFindings = append(allFindings, envVariableFindings...)
86 | allFindings = append(allFindings, cloudWatchLogFindings...)
87 | allFindings = append(allFindings, timeoutFindings...)
88 | allFindings = append(allFindings, vpcFindings...)
89 |
90 | span.SetAttributes(attribute.Int("total_findings", len(allFindings)))
91 |
92 | return allFindings, scanItems, nil
93 | }
94 |
95 | func (scanner *LambdaScanner) processAndSummarizeFindings(
96 | ctx context.Context,
97 | service string,
98 | region string,
99 | accountId string,
100 | iamRoleFindings []string,
101 | envVariableFindings []string,
102 | cloudWatchLogFindings []string,
103 | timeoutFindings []string,
104 | vpcFindings []string,
105 | ) ([]postgres.CreateNewScanItemEntryParams, error) {
106 | tracer := otel.Tracer("lambdascanner/processAndSummarizeFindings")
107 | ctx, span := tracer.Start(ctx, "processAndSummarizeFindings")
108 | defer span.End()
109 |
110 | scanItems := []postgres.CreateNewScanItemEntryParams{}
111 |
112 | // Summarize IAM Role findings
113 | if len(iamRoleFindings) > 0 {
114 | iamRoleSummary, err := scanner.Gemini.SummarizeFindings(ctx, service, region, accountId, iamRoleFindings)
115 | if err != nil {
116 | span.RecordError(err)
117 | return nil, fmt.Errorf("error summarizing IAM role findings: %v", err)
118 | }
119 | scanItems = append(scanItems, postgres.CreateNewScanItemEntryParams{
120 | Findings: iamRoleFindings,
121 | Title: "Lambda IAM Role Findings",
122 | Summary: iamRoleSummary.Summary,
123 | Remedy: iamRoleSummary.Remedies,
124 | Commands: iamRoleSummary.Commands,
125 | })
126 | scanner.logger.Logger(ctx).Info("Lambda IAM Role Findings", zap.Any("Summary", iamRoleSummary))
127 | }
128 |
129 | // Summarize Environment Variable findings
130 | if len(envVariableFindings) > 0 {
131 | envVarSummary, err := scanner.Gemini.SummarizeFindings(ctx, service, region, accountId, envVariableFindings)
132 | if err != nil {
133 | span.RecordError(err)
134 | return nil, fmt.Errorf("error summarizing environment variable findings: %v", err)
135 | }
136 | scanItems = append(scanItems, postgres.CreateNewScanItemEntryParams{
137 | Findings: envVariableFindings,
138 | Title: "Sensitive Environment Variable Findings",
139 | Summary: envVarSummary.Summary,
140 | Remedy: envVarSummary.Remedies,
141 | Commands: envVarSummary.Commands,
142 | })
143 | scanner.logger.Logger(ctx).Info("Sensitive Environment Variable Findings", zap.Any("Summary", envVarSummary))
144 | }
145 |
146 | // Summarize CloudWatch Log findings
147 | if len(cloudWatchLogFindings) > 0 {
148 | cloudWatchSummary, err := scanner.Gemini.SummarizeFindings(ctx, service, region, accountId, cloudWatchLogFindings)
149 | if err != nil {
150 | span.RecordError(err)
151 | return nil, fmt.Errorf("error summarizing CloudWatch log findings: %v", err)
152 | }
153 | scanItems = append(scanItems, postgres.CreateNewScanItemEntryParams{
154 | Findings: cloudWatchLogFindings,
155 | Title: "CloudWatch Log Findings",
156 | Summary: cloudWatchSummary.Summary,
157 | Remedy: cloudWatchSummary.Remedies,
158 | Commands: cloudWatchSummary.Commands,
159 | })
160 | scanner.logger.Logger(ctx).Info("CloudWatch Log Findings", zap.Any("Summary", cloudWatchSummary))
161 | }
162 |
163 | // Summarize Timeout Configuration findings
164 | if len(timeoutFindings) > 0 {
165 | timeoutSummary, err := scanner.Gemini.SummarizeFindings(ctx, service, region, accountId, timeoutFindings)
166 | if err != nil {
167 | span.RecordError(err)
168 | return nil, fmt.Errorf("error summarizing timeout configuration findings: %v", err)
169 | }
170 | scanItems = append(scanItems, postgres.CreateNewScanItemEntryParams{
171 | Findings: timeoutFindings,
172 | Title: "Timeout Configuration Findings",
173 | Summary: timeoutSummary.Summary,
174 | Remedy: timeoutSummary.Remedies,
175 | Commands: timeoutSummary.Commands,
176 | })
177 | scanner.logger.Logger(ctx).Info("Timeout Configuration Findings", zap.Any("Summary", timeoutSummary))
178 | }
179 |
180 | // Summarize VPC Configuration findings
181 | if len(vpcFindings) > 0 {
182 | vpcSummary, err := scanner.Gemini.SummarizeFindings(ctx, service, region, accountId, vpcFindings)
183 | if err != nil {
184 | span.RecordError(err)
185 | return nil, fmt.Errorf("error summarizing VPC configuration findings: %v", err)
186 | }
187 | scanItems = append(scanItems, postgres.CreateNewScanItemEntryParams{
188 | Findings: vpcFindings,
189 | Title: "VPC Configuration Findings",
190 | Summary: vpcSummary.Summary,
191 | Remedy: vpcSummary.Remedies,
192 | Commands: vpcSummary.Commands,
193 | })
194 | scanner.logger.Logger(ctx).Info("VPC Configuration Findings", zap.Any("Summary", vpcSummary))
195 | }
196 |
197 | span.SetAttributes(attribute.Int("scan_items_count", len(scanItems)))
198 |
199 | return scanItems, nil
200 | }
201 |
--------------------------------------------------------------------------------