diff --git a/.github/workflows/code-generation-check.yml b/.github/workflows/code-generation-check.yml new file mode 100644 index 00000000000..3ea1103df95 --- /dev/null +++ b/.github/workflows/code-generation-check.yml @@ -0,0 +1,90 @@ +# Copyright 2025 Specter Ops, Inc. +# +# Licensed under the Apache License, Version 2.0 +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +name: Code Generation Check + +on: + push: + branches: + - BED-6802 + +jobs: + code-generation-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout source code for this repository + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + check-latest: true + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install Yarn + run: | + npm install --global yarn + + - name: Run stbernard deps + run: | + go tool stbernard deps + + - name: Run stbernard modsync + run: | + go tool stbernard modsync + + - name: Run stbernard generate + run: | + go tool stbernard generate + + - name: Run stbernard license + run: | + go tool stbernard license + + - name: Check for uncommitted changes + id: show + run: | + git update-index --skip-worktree cmd/ui/public/mockServiceWorker.js + go tool stbernard show -no-version + + - name: Fail with helpful message + if: failure() + run: | + echo "::error::Code generation check failed!" + echo "" + echo "==========================================" + echo " CODE GENERATION CHECK FAILED" + echo "==========================================" + echo "" + echo "This PR has uncommitted generated code or dependency changes." + echo "" + echo "Please run the following command locally and commit the changes:" + echo "" + echo " just prepare-for-codereview" + echo "" + echo "Then push the updated changes to this PR." + echo "" + echo "==========================================" + exit 1 diff --git a/.vscode/launch.json b/.vscode/launch.json index 07be5d1abb2..0e1533fbe99 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,81 +1,94 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "antlr-debug", + "request": "launch", + "name": "Debug Cypher Grammar", + "input": "${file}", + "grammar": "${workspaceFolder}/packages/go/cypher/grammar/Cypher.g4", + "visualParseTree": true, + }, + { + "name": "Launch Go file", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${file}", + }, + { + "name": "Debug with Docker", + "type": "go", + "host": "localhost", + "mode": "remote", + "port": 3456, + "request": "attach", + "substitutePath": [ { - "type": "antlr-debug", - "request": "launch", - "name": "Debug Cypher Grammar", - "input": "${file}", - "grammar": "${workspaceFolder}/packages/go/cypher/grammar/Cypher.g4", - "visualParseTree": true + "from": "${workspaceFolder}", + "to": "/bloodhound", }, - { - "name": "Launch Go file", - "type": "go", - "request": "launch", - "mode": "debug", - "program": "${file}" - }, - { - "name": "Debug with Docker", - "type": "go", - "host": "localhost", - "mode": "remote", - "port": 3456, - "request": "attach", - "substitutePath": [ - { - "from": "${workspaceFolder}", - "to": "/bloodhound" - } - ] - }, - { - "name": "Debug API Locally", - "type": "go", - "request": "launch", - "mode": "debug", - "program": "github.com/specterops/bloodhound/cmd/api/src", - "cwd": "${workspaceFolder}", - "args": [ - "-configfile", - "${workspaceFolder}/local-harnesses/build.config.json" - ] - }, - { - "name": "Run Package Integration Tests", - "type": "go", - "request": "launch", - "mode": "test", - "program": "${fileDirname}", - "buildFlags": "--tags=integration,serial_integration", - "showLog": true, - "env": { - "INTEGRATION_CONFIG_PATH": "${workspaceFolder}/local-harnesses/integration.config.json" - } - }, - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "skipFiles": ["/**"], - "program": "${workspaceFolder}/start", - "outFiles": ["${workspaceFolder}/**/*.js"] - }, - { - "name": "Graphify", - "type": "go", - "request": "launch", - "mode": "debug", - "program": "packages/go/graphify", - "cwd": "${workspaceFolder}", - "args": ["bh-graphify", "-path=${workspaceFolder}/cmd/api/src/test/fixtures/fixtures/v6/ingest", "-outpath=${workspaceFolder}/tmp/"], - "env": { - "SB_PG_CONNECTION":"user=bloodhound password=bloodhoundcommunityedition dbname=bloodhound host=localhost port=65432" - } - } - ] + ], + }, + { + "name": "Debug API Locally", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "github.com/specterops/bloodhound/cmd/api/src", + "cwd": "${workspaceFolder}", + "args": [ + "-configfile", + "${workspaceFolder}/local-harnesses/build.config.json", + ], + }, + { + "name": "Run Package Integration Tests", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${fileDirname}", + "buildFlags": "--tags=integration,serial_integration", + "showLog": true, + "env": { + "INTEGRATION_CONFIG_PATH": "${workspaceFolder}/local-harnesses/integration.config.json", + }, + }, + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/start", + "outFiles": ["${workspaceFolder}/**/*.js"], + }, + { + "name": "Debug St Bernard License", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "packages/go/stbernard", + "cwd": "${workspaceFolder}", + "args": ["license"], + }, + { + "name": "Graphify", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "packages/go/graphify", + "cwd": "${workspaceFolder}", + "args": [ + "bh-graphify", + "-path=${workspaceFolder}/cmd/api/src/test/fixtures/fixtures/v6/ingest", + "-outpath=${workspaceFolder}/tmp/", + ], + "env": { + "SB_PG_CONNECTION": "user=bloodhound password=bloodhoundcommunityedition dbname=bloodhound host=localhost port=65432", + }, + }, + ], } diff --git a/packages/go/stbernard/command/license/internal/cmd.go b/packages/go/stbernard/command/license/internal/cmd.go index 37f26d6d63d..e5962d65820 100644 --- a/packages/go/stbernard/command/license/internal/cmd.go +++ b/packages/go/stbernard/command/license/internal/cmd.go @@ -118,8 +118,11 @@ func Run(env environment.Environment) error { }) // ignore directories and paths that are in the ignore list - if info.IsDir() && (slices.Contains(ignoreDir, info.Name()) || ignorePath) { + if info.IsDir() && slices.Contains(ignoreDir, info.Name()) { return filepath.SkipDir + } else if ignorePath { + // Shortcut out without skipping directory (support specific file ignores) + return nil } // ignore files that are in the ignore list diff --git a/packages/go/stbernard/command/show/show.go b/packages/go/stbernard/command/show/show.go index 3c63fc48739..3c1af718988 100644 --- a/packages/go/stbernard/command/show/show.go +++ b/packages/go/stbernard/command/show/show.go @@ -34,7 +34,8 @@ const ( ) type command struct { - env environment.Environment + env environment.Environment + noVersion bool } type repository struct { @@ -65,6 +66,8 @@ func (s *command) Name() string { func (s *command) Parse(cmdIndex int) error { cmd := flag.NewFlagSet(Name, flag.ExitOnError) + cmd.BoolVar(&s.noVersion, "no-version", false, "Disable version tag checking (for environments without git tags)") + cmd.Usage = func() { w := flag.CommandLine.Output() fmt.Fprintf(w, "%s\n\nUsage: %s %s [OPTIONS]\n\nOptions:\n", Usage, filepath.Base(os.Args[0]), Name) @@ -93,7 +96,11 @@ func (s *command) Run() error { for _, repo := range repos { fmt.Printf("Repository Report For %s\n", repo.path) fmt.Printf("Current HEAD: %s\n", repo.sha) - fmt.Printf("Detected version: %s\n", repo.version) + if s.noVersion { + fmt.Println("Detected version: (skipped)") + } else { + fmt.Printf("Detected version: %s\n", repo.version) + } if !repo.clean { fmt.Println("CHANGES DETECTED - please commit outstanding changes and run the command again") return fmt.Errorf("changes detected in git repository - please commit outstanding changes and run the command again") @@ -123,18 +130,27 @@ func (s *command) submodulesCheck(paths []string) ([]repository, error) { func (s *command) repositoryCheck(cwd string) (repository, error) { var repo repository - if sha, err := git.FetchCurrentCommitSHA(cwd, s.env); err != nil { + sha, err := git.FetchCurrentCommitSHA(cwd, s.env) + if err != nil { return repo, fmt.Errorf("fetching current commit sha: %w", err) - } else if version, err := git.ParseLatestVersionFromTags(cwd, s.env); err != nil { - return repo, fmt.Errorf("parsing version: %w", err) - } else if clean, err := git.CheckClean(cwd, s.env); err != nil { - return repo, fmt.Errorf("checking repository clean: %w", err) - } else { - repo.path = cwd - repo.sha = sha + } + + repo.path = cwd + repo.sha = sha + + if !s.noVersion { + version, err := git.ParseLatestVersionFromTags(cwd, s.env) + if err != nil { + return repo, fmt.Errorf("parsing version: %w", err) + } repo.version = version - repo.clean = clean + } - return repo, nil + clean, err := git.CheckClean(cwd, s.env) + if err != nil { + return repo, fmt.Errorf("checking repository clean: %w", err) } + repo.clean = clean + + return repo, nil } diff --git a/packages/go/stbernard/git/git.go b/packages/go/stbernard/git/git.go index eab5d2e94e1..355ea3e08cf 100644 --- a/packages/go/stbernard/git/git.go +++ b/packages/go/stbernard/git/git.go @@ -95,19 +95,20 @@ func CheckClean(cwd string, env environment.Environment) (bool, error) { diffIndexPlan := cmdrunner.ExecutionPlan{ Command: "git", - Args: []string{"diff-index", "--quiet", "HEAD", "--"}, + Args: []string{"--no-pager", "diff"}, Path: cwd, Env: env.Slice(), SuppressErrors: true, } result, err := cmdrunner.Run(context.TODO(), diffIndexPlan) if err != nil { - // Failure was due to dirty workspace - if errors.Is(err, cmdrunner.ErrCmdExecutionFailed) && result.ReturnCode == 1 { - return false, nil - } else { - return false, fmt.Errorf("git diff-index: %w", err) - } + return false, fmt.Errorf("git diff: %w", err) + } + + if len(result.StandardOutput.Bytes()) > 0 { + slog.Info("Repository is dirty") + fmt.Fprint(os.Stdout, result.StandardOutput.String()) + return false, nil } slog.Info(fmt.Sprintf("Finished checking repository clean for %s", cwd)) diff --git a/packages/go/stbernard/workspace/workspace.go b/packages/go/stbernard/workspace/workspace.go index a5379fd0c5e..dbfbad94cbe 100644 --- a/packages/go/stbernard/workspace/workspace.go +++ b/packages/go/stbernard/workspace/workspace.go @@ -20,8 +20,10 @@ import ( "context" "errors" "fmt" + "log/slog" "os" "path/filepath" + "strings" "github.com/specterops/bloodhound/packages/go/stbernard/cmdrunner" "github.com/specterops/bloodhound/packages/go/stbernard/environment" @@ -87,6 +89,14 @@ func FindPaths(env environment.Environment) (WorkspacePaths, error) { return WorkspacePaths{}, fmt.Errorf("parsing yarn workspace: %w", err) } + slog.Info( + "Detected Workspace", + slog.String("root", cwd), + slog.String("submodules", strings.Join(subPaths, "|")), + slog.String("assets", yarnWorkspaces.AssetsDir), + slog.String("yarn_workspaces", strings.Join(yarnWorkspaces.Workspaces, "|")), + ) + return WorkspacePaths{ Root: cwd, Coverage: path,