diff --git a/docs/_data/bearer_scan.yaml b/docs/_data/bearer_scan.yaml index 3f6ad94e4..cd19aad1f 100644 --- a/docs/_data/bearer_scan.yaml +++ b/docs/_data/bearer_scan.yaml @@ -4,6 +4,10 @@ usage: bearer scan [flags] options: - name: api-key usage: Use your Bearer API Key to send the report to Bearer. + - name: branch + usage: The name of the branch being scanned. + - name: commit + usage: The hash of the commit being scanned. - name: config-file default_value: bearer.yml usage: Load configuration from the specified path. @@ -19,6 +23,16 @@ options: - name: debug-profile default_value: "false" usage: Generate profiling data for debugging + - name: default-branch + usage: The name of the default branch. + - name: diff + default_value: "false" + usage: | + Only report differences in findings relative to a base branch. + - name: diff-base-branch + usage: The name of the base branch to use for diff scanning. + - name: diff-base-commit + usage: The hash of the base commit to use for diff scanning. - name: disable-default-rules default_value: "false" usage: Disables all default and built-in rules. @@ -56,6 +70,13 @@ options: shorthand: f usage: | Specify report format (json, yaml, sarif, gitlab-sast, rdjson, html) + - name: github-api-url + usage: A non-standard URL to use for the Github API + - name: github-repository + usage: | + The owner and name of the repository on Github. eg. Bearer/bearer + - name: github-token + usage: An access token for the Github API. - name: help shorthand: h default_value: "false" @@ -97,6 +118,8 @@ options: - name: report default_value: security usage: Specify the type of report (security, privacy, dataflow). + - name: repository-url + usage: The remote URL of the repository. - name: scanner default_value: "[sast]" usage: | diff --git a/docs/_data/examples/ci/gitlab/diff-reviewdog.yaml b/docs/_data/examples/ci/gitlab/diff-reviewdog.yaml index a1eee6565..a233fd37e 100644 --- a/docs/_data/examples/ci/gitlab/diff-reviewdog.yaml +++ b/docs/_data/examples/ci/gitlab/diff-reviewdog.yaml @@ -1,10 +1,7 @@ bearer_mr: - variables: - DIFF_BASE_BRANCH: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME - DIFF_BASE_COMMIT: $CI_MERGE_REQUEST_DIFF_BASE_SHA script: - curl -sfL https://raw.githubusercontent.com/Bearer/bearer/main/contrib/install.sh | sh -s -- -b /usr/local/bin - curl -sfL https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh | sh -s -- -b /usr/local/bin - - bearer scan . --format=rdjson --output=rd.json || export BEARER_EXIT=$? + - bearer scan . --diff --format=rdjson --output=rd.json || export BEARER_EXIT=$? - cat rd.json | reviewdog -f=rdjson -reporter=gitlab-mr-discussion - exit $BEARER_EXIT diff --git a/docs/_data/examples/ci/gitlab/diff.yaml b/docs/_data/examples/ci/gitlab/diff.yaml index 6b801f6c2..02bcea650 100644 --- a/docs/_data/examples/ci/gitlab/diff.yaml +++ b/docs/_data/examples/ci/gitlab/diff.yaml @@ -2,7 +2,4 @@ bearer_mr: image: name: bearer/bearer entrypoint: [""] - variables: - DIFF_BASE_BRANCH: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME - DIFF_BASE_COMMIT: $CI_MERGE_REQUEST_DIFF_BASE_SHA - script: bearer scan . + script: bearer scan --diff . diff --git a/docs/guides/configure-scan.md b/docs/guides/configure-scan.md index 414efdbb5..b117aabed 100644 --- a/docs/guides/configure-scan.md +++ b/docs/guides/configure-scan.md @@ -33,12 +33,13 @@ When scanning a Git repository, you can choose to only report new findings that have been introduced, relative to a base branch. Any findings that already existed in the base branch will not be reported. -Use the `DIFF_BASE_BRANCH` environment variable to enable differential scanning, -and to specify the base branch to use for comparison. +Use the `--diff` flag to enable differential scanning. The repository's default +branch will be used as the base branch for comparison. You can override this by +setting the `BEARER_DIFF_BASE_BRANCH` environment variable. ```bash git checkout my-feature -DIFF_BASE_BRANCH=main bearer scan . +BEARER_DIFF_BASE_BRANCH=base-branch bearer scan --diff . ``` If the base branch is not available in the git repository, it's head will be diff --git a/docs/guides/gitlab.md b/docs/guides/gitlab.md index d95ded83e..722d7a2ac 100644 --- a/docs/guides/gitlab.md +++ b/docs/guides/gitlab.md @@ -31,8 +31,8 @@ These changes set the format to `gitlab-sast` and write an artifact that GitLab ### Gitlab Merge Request Diff When Bearer CLI is being used to check a merge request, you can tell the Bearer -CLI to only report findings introduced within the merge request by setting the -`DIFF_BASE_BRANCH` variable. +CLI to only report findings introduced within the merge request by adding the +`--diff` flag. {% yamlExample "ci/gitlab/diff" %} diff --git a/e2e/flags/.snapshots/TestMetadataFlags-help-scan b/e2e/flags/.snapshots/TestMetadataFlags-help-scan index ef1822a6f..610ec4e95 100644 --- a/e2e/flags/.snapshots/TestMetadataFlags-help-scan +++ b/e2e/flags/.snapshots/TestMetadataFlags-help-scan @@ -24,6 +24,7 @@ Rule Flags Scan Flags --context string Expand context of schema classification e.g., --context=health, to include data types particular to health --data-subject-mapping string Override default data subject mapping by providing a path to a custom mapping JSON file + --diff Only report differences in findings relative to a base branch. --disable-domain-resolution Do not attempt to resolve detected domains during classification (default true) --domain-resolution-timeout duration Set timeout when attempting to resolve detected domains during classification, e.g. --domain-resolution-timeout=3s (default 3s) --exit-code int Force a given exit code for the scan command. Set this to 0 (success) to always return a success exit code despite any findings from the scan. (default -1) diff --git a/e2e/flags/.snapshots/TestMetadataFlags-scan-help b/e2e/flags/.snapshots/TestMetadataFlags-scan-help index ef1822a6f..610ec4e95 100644 --- a/e2e/flags/.snapshots/TestMetadataFlags-scan-help +++ b/e2e/flags/.snapshots/TestMetadataFlags-scan-help @@ -24,6 +24,7 @@ Rule Flags Scan Flags --context string Expand context of schema classification e.g., --context=health, to include data types particular to health --data-subject-mapping string Override default data subject mapping by providing a path to a custom mapping JSON file + --diff Only report differences in findings relative to a base branch. --disable-domain-resolution Do not attempt to resolve detected domains during classification (default true) --domain-resolution-timeout duration Set timeout when attempting to resolve detected domains during classification, e.g. --domain-resolution-timeout=3s (default 3s) --exit-code int Force a given exit code for the scan command. Set this to 0 (success) to always return a success exit code despite any findings from the scan. (default -1) diff --git a/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-context-flag b/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-context-flag index fe449fef0..569fdfd06 100644 --- a/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-context-flag +++ b/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-context-flag @@ -25,6 +25,7 @@ Rule Flags Scan Flags --context string Expand context of schema classification e.g., --context=health, to include data types particular to health --data-subject-mapping string Override default data subject mapping by providing a path to a custom mapping JSON file + --diff Only report differences in findings relative to a base branch. --disable-domain-resolution Do not attempt to resolve detected domains during classification (default true) --domain-resolution-timeout duration Set timeout when attempting to resolve detected domains during classification, e.g. --domain-resolution-timeout=3s (default 3s) --exit-code int Force a given exit code for the scan command. Set this to 0 (success) to always return a success exit code despite any findings from the scan. (default -1) diff --git a/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-format-flag-privacy b/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-format-flag-privacy index bedaf0560..6fd4d119a 100644 --- a/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-format-flag-privacy +++ b/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-format-flag-privacy @@ -25,6 +25,7 @@ Rule Flags Scan Flags --context string Expand context of schema classification e.g., --context=health, to include data types particular to health --data-subject-mapping string Override default data subject mapping by providing a path to a custom mapping JSON file + --diff Only report differences in findings relative to a base branch. --disable-domain-resolution Do not attempt to resolve detected domains during classification (default true) --domain-resolution-timeout duration Set timeout when attempting to resolve detected domains during classification, e.g. --domain-resolution-timeout=3s (default 3s) --exit-code int Force a given exit code for the scan command. Set this to 0 (success) to always return a success exit code despite any findings from the scan. (default -1) diff --git a/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-format-flag-security b/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-format-flag-security index c075e817b..3b64e1d89 100644 --- a/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-format-flag-security +++ b/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-format-flag-security @@ -25,6 +25,7 @@ Rule Flags Scan Flags --context string Expand context of schema classification e.g., --context=health, to include data types particular to health --data-subject-mapping string Override default data subject mapping by providing a path to a custom mapping JSON file + --diff Only report differences in findings relative to a base branch. --disable-domain-resolution Do not attempt to resolve detected domains during classification (default true) --domain-resolution-timeout duration Set timeout when attempting to resolve detected domains during classification, e.g. --domain-resolution-timeout=3s (default 3s) --exit-code int Force a given exit code for the scan command. Set this to 0 (success) to always return a success exit code despite any findings from the scan. (default -1) diff --git a/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-report-flag b/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-report-flag index 3438d31fd..ecf89e381 100644 --- a/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-report-flag +++ b/e2e/flags/.snapshots/TestReportFlagsShouldFail-invalid-report-flag @@ -25,6 +25,7 @@ Rule Flags Scan Flags --context string Expand context of schema classification e.g., --context=health, to include data types particular to health --data-subject-mapping string Override default data subject mapping by providing a path to a custom mapping JSON file + --diff Only report differences in findings relative to a base branch. --disable-domain-resolution Do not attempt to resolve detected domains during classification (default true) --domain-resolution-timeout duration Set timeout when attempting to resolve detected domains during classification, e.g. --domain-resolution-timeout=3s (default 3s) --exit-code int Force a given exit code for the scan command. Set this to 0 (success) to always return a success exit code despite any findings from the scan. (default -1) diff --git a/go.mod b/go.mod index 73d30ec83..9484bf0ce 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/gertd/go-pluralize v0.2.1 github.com/gitsight/go-vcsurl v1.0.1 github.com/go-enry/go-enry/v2 v2.8.4 - github.com/go-git/go-git/v5 v5.10.0 github.com/google/go-github v17.0.0+incompatible github.com/google/uuid v1.4.0 github.com/hhatto/gocloc v0.5.2 @@ -69,51 +68,36 @@ require ( ) require ( - dario.cat/mergo v1.0.0 // indirect github.com/BobuSumisu/aho-corasick v1.0.3 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect - github.com/acomagu/bufpipe v1.0.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/cloudflare/circl v1.3.3 // indirect - github.com/cyphar/filepath-securejoin v0.2.4 // indirect - github.com/emirpasic/gods v1.18.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/gorilla/mux v1.8.0 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sergi/go-diff v1.3.1 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.2.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect - github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect go.opentelemetry.io/otel v1.19.0 // indirect go.opentelemetry.io/otel/metric v1.19.0 // indirect @@ -121,11 +105,9 @@ require ( go.opentelemetry.io/otel/trace v1.19.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.15.0 // indirect golang.org/x/tools v0.14.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect ) require ( diff --git a/go.sum b/go.sum index f2267e722..a111fe973 100644 --- a/go.sum +++ b/go.sum @@ -36,31 +36,18 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8a+4nPE9g= github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= -github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -72,7 +59,6 @@ github.com/bradleyjkemp/cupaloy v2.3.0+incompatible/go.mod h1:Au1Xw1sgaJ5iSFktEh github.com/buildkite/terminal v3.2.0+incompatible h1:08p6611HADinUwK0oyxCaAsnFXVDU4GlTW1TdXVP+5s= github.com/buildkite/terminal v3.2.0+incompatible/go.mod h1:iQavkS6X0wlozOmO2rxHYt/9mE5Ij2XTk6yGcclx6hk= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= @@ -89,16 +75,12 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= -github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -111,10 +93,6 @@ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -141,20 +119,10 @@ github.com/gitleaks/go-gitdiff v0.9.0 h1:SHAU2l0ZBEo8g82EeFewhVy81sb7JCxW76oSPtR github.com/gitleaks/go-gitdiff v0.9.0/go.mod h1:pKz0X4YzCKZs30BL+weqBIG7mx0jl4tF1uXV9ZyNvrA= github.com/gitsight/go-vcsurl v1.0.1 h1:wkijKsbVg9R2IBP97U7wOANeIW9WJJKkBwS9XqllzWo= github.com/gitsight/go-vcsurl v1.0.1/go.mod h1:qRFdKDa/0Lh9MT0xE+qQBYZ/01+mY1H40rZUHR24X9U= -github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= -github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-enry/go-enry/v2 v2.8.4 h1:QrY3hx/RiqCJJRbdU0MOcjfTM1a586J0WSooqdlJIhs= github.com/go-enry/go-enry/v2 v2.8.4/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.10.0 h1:F0x3xXrAWmhwtzoCokU4IMPcBdncG+HAAqi9FcOOjbQ= -github.com/go-git/go-git/v5 v5.10.0/go.mod h1:1FOZ/pQnqw24ghP2n7cunVl0ON55BsjPYvhWHvZGhoo= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -271,13 +239,9 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= -github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= @@ -295,8 +259,6 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= -github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= @@ -334,8 +296,6 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= @@ -379,19 +339,14 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/schollz/progressbar/v3 v3.14.1 h1:VD+MJPCr4s3wdhTc7OEJ/Z3dAeBzJ7yKH/P4lC5yRTI= github.com/schollz/progressbar/v3 v3.14.1/go.mod h1:Zc9xXneTzWXF81TGoqL71u0sBPjULtEHYtj/WVgVy8E= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shirou/gopsutil/v3 v3.23.10 h1:/N42opWlYzegYaVkWejXWJpbzKv2JDy3mrgGzKsh9hM= github.com/shirou/gopsutil/v3 v3.23.10/go.mod h1:JIE26kpucQi+innVlAUnIEOSBhBUkirr5b44yr55+WE= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= -github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= github.com/smacker/go-tree-sitter v0.0.0-20230720070738-0d0a9f78d8f8 h1:DxgjlvWYsb80WEN2Zv3WqJFAg2DKjUQJO6URGdf1x6Y= github.com/smacker/go-tree-sitter v0.0.0-20230720070738-0d0a9f78d8f8/go.mod h1:q99oHDsbP0xRwmn7Vmob8gbSMNyvJ83OauXPSuHQuKE= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -409,7 +364,6 @@ github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -433,8 +387,6 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/weppos/publicsuffix-go v0.30.1 h1:8q+QwBS1MY56Zjfk/50ycu33NN8aa1iCCEQwo/71Oos= github.com/weppos/publicsuffix-go v0.30.1/go.mod h1:s41lQh6dIsDWIC1OWh7ChWJXLH0zkJ9KHZVqA7vHyuQ= -github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -488,13 +440,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -567,7 +515,6 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= @@ -611,7 +558,6 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -634,7 +580,6 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -645,8 +590,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -658,7 +601,6 @@ golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= @@ -673,7 +615,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -842,14 +783,11 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/internal/commands/artifact/run.go b/internal/commands/artifact/run.go index b84018bd6..acd3cbf15 100644 --- a/internal/commands/artifact/run.go +++ b/internal/commands/artifact/run.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "os" - "path/filepath" "sort" "strings" "time" @@ -26,10 +25,10 @@ import ( "github.com/bearer/bearer/internal/flag" "github.com/bearer/bearer/internal/report/basebranchfindings" reportoutput "github.com/bearer/bearer/internal/report/output" - "github.com/bearer/bearer/internal/report/output/saas" "github.com/bearer/bearer/internal/report/output/stats" outputtypes "github.com/bearer/bearer/internal/report/output/types" scannerstats "github.com/bearer/bearer/internal/scanner/stats" + "github.com/bearer/bearer/internal/util/file" "github.com/bearer/bearer/internal/util/ignore" ignoretypes "github.com/bearer/bearer/internal/util/ignore/types" outputhandler "github.com/bearer/bearer/internal/util/output" @@ -60,8 +59,6 @@ type Runner interface { Scan(ctx context.Context, opts flag.Options) ([]files.File, *basebranchfindings.Findings, error) // Report a writes a report Report(files []files.File, baseBranchFindings *basebranchfindings.Findings) (bool, error) - // Close closes runner - Close(ctx context.Context) error } type runner struct { @@ -71,26 +68,29 @@ type runner struct { goclocResult *gocloc.Result scanSettings settings.Config stats *scannerstats.Stats + gitContext *gitrepository.Context } // NewRunner initializes Runner that provides scanning functionalities. func NewRunner( ctx context.Context, scanSettings settings.Config, + gitContext *gitrepository.Context, targetPath string, goclocResult *gocloc.Result, stats *scannerstats.Stats, -) Runner { +) (Runner, error) { r := &runner{ scanSettings: scanSettings, targetPath: targetPath, goclocResult: goclocResult, stats: stats, + gitContext: gitContext, } - scanID, err := scanid.Build(scanSettings) + scanID, err := scanid.Build(scanSettings, gitContext) if err != nil { - log.Error().Msgf("failed to build scan id for caching %s", err) + return nil, fmt.Errorf("failed to build scan id for caching: %w", err) } path := os.TempDir() + "/bearer" + scanID @@ -101,13 +101,14 @@ func NewRunner( log.Debug().Msgf("creating report %s", path) if _, err := os.Stat(completedPath); err == nil { - if !scanSettings.Scan.Force && scanSettings.Scan.DiffBaseBranch == "" { + // diff can't use the cache because the base branch scan data is not in the report + if !scanSettings.Scan.Force && !scanSettings.Scan.Diff { // force is not set, and we are not running a diff scan r.reuseDetection = true log.Debug().Msgf("reuse detection for %s", path) r.reportPath = completedPath - return r + return r, nil } else { if _, err = os.Stat(path); err == nil { err := os.Remove(path) @@ -131,18 +132,13 @@ func NewRunner( log.Error().Msgf("failed to create path %s, %s, %#v", path, err.Error(), pathCreated) } - return r + return r, nil } func (r *runner) CacheUsed() bool { return r.reuseDetection } -// Close closes everything -func (r *runner) Close(ctx context.Context) error { - return nil -} - func (r *runner) Scan(ctx context.Context, opts flag.Options) ([]files.File, *basebranchfindings.Findings, error) { if r.reuseDetection { return nil, nil, nil @@ -152,7 +148,7 @@ func (r *runner) Scan(ctx context.Context, opts flag.Options) ([]files.File, *ba outputhandler.StdErrLog(fmt.Sprintf("Scanning target %s", opts.Target)) } - repository, err := gitrepository.New(ctx, r.scanSettings, r.targetPath, opts.DiffBaseBranch) + repository, err := gitrepository.New(ctx, r.scanSettings, r.targetPath, r.gitContext) if err != nil { return nil, nil, fmt.Errorf("git repository error: %w", err) } @@ -176,7 +172,7 @@ func (r *runner) Scan(ctx context.Context, opts flag.Options) ([]files.File, *ba var baseBranchFindings *basebranchfindings.Findings if err := repository.WithBaseBranch(func() error { if !opts.Quiet { - outputhandler.StdErrLog(fmt.Sprintf("\nScanning base branch %s", opts.DiffBaseBranch)) + outputhandler.StdErrLog(fmt.Sprintf("\nScanning base branch %s", r.gitContext.BaseBranch)) } baseBranchFindings, err = r.scanBaseBranch(orchestrator, fileList) @@ -220,7 +216,7 @@ func (r *runner) scanBaseBranch( HasFiles: len(fileList.BaseFiles) != 0, } - reportData, err := reportoutput.GetData(report, r.scanSettings, nil) + reportData, err := reportoutput.GetData(report, r.scanSettings, r.gitContext, nil) if err != nil { return nil, err } @@ -234,7 +230,7 @@ func (r *runner) scanBaseBranch( return result, nil } -func getIgnoredFingerprints(client *api.API, settings settings.Config) ( +func getIgnoredFingerprints(client *api.API, settings settings.Config, gitContext *gitrepository.Context) ( useCloudIgnores bool, ignoredFingerprints map[string]ignoretypes.IgnoredFingerprint, staleIgnoredFingerprintIds []string, @@ -246,15 +242,9 @@ func getIgnoredFingerprints(client *api.API, settings settings.Config) ( } if client != nil && client.Error == nil { - // get ignores from Cloud - vcsInfo, err := saas.GetVCSInfo(settings.Scan.Target) - if err != nil { - return useCloudIgnores, ignoredFingerprints, staleIgnoredFingerprintIds, err - } - useCloudIgnores, ignoredFingerprints, staleIgnoredFingerprintIds, err = ignore.GetIgnoredFingerprintsFromCloud( client, - vcsInfo.FullName, + gitContext.FullName, localIgnoredFingerprints, ) if err != nil { @@ -271,7 +261,7 @@ func getIgnoredFingerprints(client *api.API, settings settings.Config) ( // Run performs artifact scanning func Run(ctx context.Context, opts flag.Options) (err error) { - targetPath, err := filepath.Abs(opts.Target) + targetPath, err := file.CanonicalPath(opts.Target) if err != nil { return fmt.Errorf("failed to get absolute target: %w", err) } @@ -296,6 +286,15 @@ func Run(ctx context.Context, opts flag.Options) (err error) { version_check.DisplayBinaryVersionWarning(versionMeta, opts.ScanOptions.Quiet) } + gitContext, err := gitrepository.NewContext(&opts) + if err != nil { + return fmt.Errorf("failed to get git context: %w", err) + } + + if opts.Diff && gitContext == nil { + return errors.New("--diff option requires a git repository") + } + if !opts.Quiet { outputhandler.StdErrLog("Loading rules") } @@ -308,6 +307,7 @@ func Run(ctx context.Context, opts flag.Options) (err error) { scanSettings.CloudIgnoresUsed, scanSettings.IgnoredFingerprints, scanSettings.StaleIgnoredFingerprintIds, err = getIgnoredFingerprints( opts.GeneralOptions.Client, scanSettings, + gitContext, ) if err != nil { return err @@ -327,14 +327,14 @@ func Run(ctx context.Context, opts flag.Options) (err error) { stats = scannerstats.New() } - gitrepository.ConfigureGithubAuth(scanSettings.Scan.GithubToken) - - r := NewRunner(ctx, scanSettings, targetPath, inputgocloc, stats) - defer r.Close(ctx) + r, err := NewRunner(ctx, scanSettings, gitContext, targetPath, inputgocloc, stats) + if err != nil { + return err + } files, baseBranchFindings, err := r.Scan(ctx, opts) if err != nil { - return fmt.Errorf("scan error: %w", err) + return err } reportFailed, err := r.Report(files, baseBranchFindings) @@ -395,11 +395,11 @@ func (r *runner) Report( outputhandler.StdErrLog("Using cached data") } - reportData, err := reportoutput.GetData(report, r.scanSettings, baseBranchFindings) + reportData, err := reportoutput.GetData(report, r.scanSettings, r.gitContext, baseBranchFindings) if err != nil { return false, err } - reportoutput.UploadReportToCloud(reportData, r.scanSettings) + reportoutput.UploadReportToCloud(reportData, r.scanSettings, r.gitContext) endTime := time.Now() diff --git a/internal/commands/artifact/scanid/scanid.go b/internal/commands/artifact/scanid/scanid.go index 315515460..0dbfef123 100644 --- a/internal/commands/artifact/scanid/scanid.go +++ b/internal/commands/artifact/scanid/scanid.go @@ -5,26 +5,29 @@ import ( "encoding/hex" "encoding/json" "fmt" - "os/exec" - "path/filepath" "sort" "strings" "github.com/google/uuid" - "github.com/rs/zerolog/log" "github.com/bearer/bearer/cmd/bearer/build" + "github.com/bearer/bearer/internal/commands/process/gitrepository" "github.com/bearer/bearer/internal/commands/process/settings" + "github.com/bearer/bearer/internal/util/file" ) -func Build(scanSettings settings.Config) (string, error) { - // we want head as project may contain new changes - cmd := exec.Command("git", "-C", scanSettings.Scan.Target, "rev-parse", "HEAD") - sha, err := cmd.Output() +func Build(scanSettings settings.Config, gitContext *gitrepository.Context) (string, error) { + var sha string + if gitContext != nil && !gitContext.HasUncommittedChanges { + if gitContext.BaseCommitHash == "" { + sha = gitContext.CurrentCommitHash + } else { + sha = gitContext.BaseCommitHash + "_" + gitContext.CurrentCommitHash + } + } - if err != nil { - log.Debug().Msgf("error getting git sha %s", err.Error()) - sha = []byte(uuid.NewString()) + if sha == "" { + sha = uuid.NewString() } configHash, err := hashConfig(scanSettings) @@ -50,13 +53,12 @@ func hashConfig(scanSettings settings.Config) (string, error) { return "", fmt.Errorf("error building scanners hash: %w", err) } - absTarget, err := filepath.Abs(scanSettings.Scan.Target) + absTarget, err := file.CanonicalPath(scanSettings.Scan.Target) if err != nil { return "", fmt.Errorf("error getting absolute path to target: %w", err) } targetHash := md5.Sum([]byte(absTarget)) - baseBranchHash := md5.Sum([]byte(scanSettings.Scan.DiffBaseBranch)) hashBuilder := md5.New() if _, err := hashBuilder.Write(targetHash[:]); err != nil { @@ -68,9 +70,6 @@ func hashConfig(scanSettings settings.Config) (string, error) { if _, err := hashBuilder.Write(scannersHash); err != nil { return "", err } - if _, err := hashBuilder.Write(baseBranchHash[:]); err != nil { - return "", err - } return hex.EncodeToString(hashBuilder.Sum(nil)[:]), nil } diff --git a/internal/commands/ignore.go b/internal/commands/ignore.go index eced3fdf8..c20b2a541 100644 --- a/internal/commands/ignore.go +++ b/internal/commands/ignore.go @@ -11,8 +11,8 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/bearer/bearer/internal/commands/process/gitrepository" "github.com/bearer/bearer/internal/flag" - "github.com/bearer/bearer/internal/report/output/saas" "github.com/bearer/bearer/internal/util/ignore" ignoretypes "github.com/bearer/bearer/internal/util/ignore/types" "github.com/bearer/bearer/internal/util/output" @@ -345,27 +345,26 @@ $ bearer ignore pull /path/to/your_project --api-key=XXXXX`, } } - // get project full name - vcsInfo, err := saas.GetVCSInfo(options.Target) + gitContext, err := gitrepository.NewContext(&options) if err != nil { - return fmt.Errorf("error fetching project info: %s", err) + return fmt.Errorf("failed to get git context: %w", err) } - data, err := options.GeneralOptions.Client.FetchIgnores(vcsInfo.FullName, []string{}) + data, err := options.GeneralOptions.Client.FetchIgnores(gitContext.FullName, []string{}) if err != nil { return fmt.Errorf("cloud error: %s", err) } if !data.ProjectFound { // no project - cmd.Printf("Project %s not found in Cloud. Pull cancelled.", vcsInfo.FullName) + cmd.Printf("Project %s not found in Cloud. Pull cancelled.", gitContext.FullName) return nil } cloudIgnoresCount := len(data.CloudIgnoredFingerprints) if cloudIgnoresCount == 0 { // project found but no ignores - cmd.Printf("No ignores for project %s found in the Cloud. Pull cancelled", vcsInfo.FullName) + cmd.Printf("No ignores for project %s found in the Cloud. Pull cancelled", gitContext.FullName) return nil } diff --git a/internal/commands/process/filelist/files/files.go b/internal/commands/process/filelist/files/files.go index 2d77a567b..cf8d5517c 100644 --- a/internal/commands/process/filelist/files/files.go +++ b/internal/commands/process/filelist/files/files.go @@ -3,14 +3,14 @@ package files import ( "time" - bbftypes "github.com/bearer/bearer/internal/report/basebranchfindings/types" + "github.com/bearer/bearer/internal/git" ) type List struct { Files []File BaseFiles []File Renames map[string]string - Chunks map[string]bbftypes.Chunks + Chunks map[string]git.Chunks } type File struct { diff --git a/internal/commands/process/gitrepository/context.go b/internal/commands/process/gitrepository/context.go new file mode 100644 index 000000000..30731ca1c --- /dev/null +++ b/internal/commands/process/gitrepository/context.go @@ -0,0 +1,275 @@ +package gitrepository + +import ( + "context" + "errors" + "fmt" + "net/url" + "strings" + + "github.com/gitsight/go-vcsurl" + "github.com/google/go-github/github" + "github.com/rs/zerolog/log" + "golang.org/x/oauth2" + "gopkg.in/yaml.v3" + + "github.com/bearer/bearer/internal/flag" + "github.com/bearer/bearer/internal/git" +) + +type Context struct { + RootDir string + Branch, + CurrentBranch, + DefaultBranch, + BaseBranch string + CommitHash, + CurrentCommitHash, + BaseCommitHash string + OriginURL string + ID, + Host, + Owner, + Name, + FullName string + HasUncommittedChanges bool +} + +func NewContext(options *flag.Options) (*Context, error) { + if options.IgnoreGit { + return nil, nil + } + + rootDir, err := git.GetRoot(options.Target) + if rootDir == "" || err != nil { + return nil, err + } + + currentBranch, err := git.GetCurrentBranch(rootDir) + if err != nil { + return nil, fmt.Errorf("error getting current branch name: %w", err) + } + + defaultBranch, err := getDefaultBranch(options, rootDir) + if err != nil { + return nil, fmt.Errorf("error getting default branch name: %w", err) + } + + baseBranch, err := getBaseBranch(options, defaultBranch) + if err != nil { + return nil, fmt.Errorf("error getting base branch name: %w", err) + } + + currentCommitHash, err := git.GetCurrentCommit(rootDir) + if err != nil { + return nil, fmt.Errorf("error getting current commit hash: %w", err) + } + + baseCommitHash, err := getBaseCommitHash(options, rootDir, baseBranch, currentCommitHash) + if err != nil { + return nil, fmt.Errorf("error getting base commit hash: %w", err) + } + + hasUncommittedChanges, err := git.HasUncommittedChanges(rootDir) + if err != nil { + return nil, fmt.Errorf("error checking for uncommitted changes: %w", err) + } + + originURL, err := getOriginURL(options, rootDir) + if err != nil { + return nil, fmt.Errorf("error getting origin url: %w", err) + } + + var id, host, owner, name, fullName string + if originURL != "" { + urlInfo, err := vcsurl.Parse(originURL) + if err != nil { + return nil, fmt.Errorf("couldn't parse origin url: %w", err) + } + + id = urlInfo.ID + host = string(urlInfo.Host) + owner = urlInfo.Username + name = urlInfo.Name + fullName = urlInfo.FullName + } + + context := &Context{ + RootDir: rootDir, + Branch: getBranch(options, currentBranch), + CurrentBranch: currentBranch, + DefaultBranch: defaultBranch, + BaseBranch: baseBranch, + CommitHash: getCommitHash(options, currentCommitHash), + CurrentCommitHash: currentCommitHash, + BaseCommitHash: baseCommitHash, + OriginURL: originURL, + ID: id, + Host: host, + Owner: owner, + Name: name, + FullName: fullName, + HasUncommittedChanges: hasUncommittedChanges, + } + + contextYAML, _ := yaml.Marshal(context) + log.Debug().Msgf("git context:\n%s", contextYAML) + + return context, nil +} + +func getBranch(options *flag.Options, currentBranch string) string { + if options.Branch != "" { + return options.Branch + } + + return currentBranch +} + +func getDefaultBranch(options *flag.Options, rootDir string) (string, error) { + if options.DefaultBranch != "" { + return options.DefaultBranch, nil + } + + return git.GetDefaultBranch(rootDir) +} + +func getBaseBranch(options *flag.Options, defaultBranch string) (string, error) { + if !options.Diff { + return "", nil + } + + if options.DiffBaseBranch != "" { + return options.DiffBaseBranch, nil + } + + if defaultBranch != "" { + log.Debug().Msgf("using default branch %s for diff base branch", defaultBranch) + return defaultBranch, nil + } + + return "", errors.New( + "couldn't determine base branch for diff scanning. " + + "please set the 'BEARER_DIFF_BASE_BRANCH' environment variable", + ) +} + +func getCommitHash(options *flag.Options, currentCommitHash string) string { + if options.Commit != "" { + return options.Commit + } + + return currentCommitHash +} + +func getBaseCommitHash( + options *flag.Options, + rootDir string, + baseBranch string, + currentCommitHash string, +) (string, error) { + if baseBranch == "" { + return "", nil + } + + if options.DiffBaseCommit != "" { + return options.DiffBaseCommit, nil + } + + if hash, err := lookupBaseCommitHashFromGithub(options, baseBranch, currentCommitHash); hash != "" || err != nil { + return hash, err + } + + log.Debug().Msg("finding merge base using local repository") + hash, err := git.GetMergeBase(rootDir, "origin/"+baseBranch, currentCommitHash) + if err != nil { + if !strings.Contains(err.Error(), "Not a valid object name") { + return "", fmt.Errorf("invalid ref: %w", err) + } + } + + if hash != "" { + return hash, nil + } + + log.Debug().Msg("remote ref not found, trying local ref") + hash, err = git.GetMergeBase(rootDir, baseBranch, currentCommitHash) + if err != nil { + return "", fmt.Errorf("invalid ref: %w", err) + } + + if hash != "" { + return hash, nil + } + + return "", fmt.Errorf( + "could not find common ancestor between the current and %s branch. "+ + "please check that the base branch is correct, and that you have "+ + "fetched enough git history to include the latest common ancestor", + baseBranch, + ) +} + +func lookupBaseCommitHashFromGithub(options *flag.Options, baseBranch string, currentCommitHash string) (string, error) { + if options.GithubToken == "" || options.GithubRepository == "" { + return "", nil + } + + log.Debug().Msg("finding merge base using github api") + + splitRepository := strings.SplitN(options.GithubRepository, "/", 2) + if len(splitRepository) != 2 { + return "", fmt.Errorf("invalid github repository name '%s'", options.GithubRepository) + } + + client, err := newGithubClient(options) + if err != nil { + return "", err + } + + comparison, _, err := client.Repositories.CompareCommits( + context.Background(), + splitRepository[0], + splitRepository[1], + baseBranch, + currentCommitHash, + ) + if err != nil { + return "", fmt.Errorf("error calling github compare api: %w", err) + } + + if comparison.MergeBaseCommit == nil { + return "", nil + } + + return *comparison.MergeBaseCommit.SHA, nil +} + +func getOriginURL(options *flag.Options, rootDir string) (string, error) { + if options.OriginURL != "" { + return options.OriginURL, nil + } + + return git.GetOriginURL(rootDir) +} + +func newGithubClient(options *flag.Options) (*github.Client, error) { + tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: options.GithubToken}) + httpClient := oauth2.NewClient(context.Background(), tokenSource) + client := github.NewClient(httpClient) + + if options.GithubAPIURL != "" { + parsedURL, err := url.Parse(options.GithubAPIURL) + if err != nil { + return nil, fmt.Errorf("failed to parse github api url: %w", err) + } + + if !strings.HasSuffix(parsedURL.Path, "/") { + parsedURL.Path += "/" + } + + client.BaseURL = parsedURL + } + + return client, nil +} diff --git a/internal/commands/process/gitrepository/gitrepository.go b/internal/commands/process/gitrepository/gitrepository.go index 1043fc923..d62a8dc2a 100644 --- a/internal/commands/process/gitrepository/gitrepository.go +++ b/internal/commands/process/gitrepository/gitrepository.go @@ -4,99 +4,51 @@ import ( "context" "errors" "fmt" - "net/http" - "net/url" "os" "path/filepath" "strings" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/format/diff" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/transport/client" - githttp "github.com/go-git/go-git/v5/plumbing/transport/http" - "github.com/google/go-github/github" "github.com/hhatto/gocloc" "github.com/rs/zerolog/log" - "golang.org/x/oauth2" "github.com/bearer/bearer/internal/commands/process/filelist/files" "github.com/bearer/bearer/internal/commands/process/filelist/ignore" "github.com/bearer/bearer/internal/commands/process/filelist/timeout" "github.com/bearer/bearer/internal/commands/process/settings" - "github.com/bearer/bearer/internal/report/basebranchfindings" - bbftypes "github.com/bearer/bearer/internal/report/basebranchfindings/types" + "github.com/bearer/bearer/internal/git" + "github.com/bearer/bearer/internal/util/output" ) type Repository struct { ctx context.Context config settings.Config - git *git.Repository - rootPath, targetPath, gitTargetPath string - baseRemoteRefName, - baseLocalRefName plumbing.ReferenceName - headRef *plumbing.Reference - headCommit, - mergeBaseCommit *object.Commit - githubToken string + context *Context } -func New( - ctx context.Context, - config settings.Config, - targetPath string, - baseBranch string, -) (*Repository, error) { - gitRepository, err := git.PlainOpenWithOptions(targetPath, &git.PlainOpenOptions{ - DetectDotGit: true, - }) - if err != nil { - return nil, translateOpenError(baseBranch, err) - } - - worktree, err := gitRepository.Worktree() - if err != nil { - return nil, fmt.Errorf("failed to get worktree: %w", err) +func New(ctx context.Context, config settings.Config, targetPath string, context *Context) (*Repository, error) { + if context == nil { + log.Debug().Msg("no git repository found") + return nil, nil } - rootPath := worktree.Filesystem.Root() - gitTargetPath, err := filepath.Rel(rootPath, targetPath) + gitTargetPath, err := filepath.Rel(context.RootDir, targetPath) if err != nil { return nil, fmt.Errorf("failed to get relative target: %w", err) } - log.Debug().Msgf("git target: [%s/]%s", rootPath, gitTargetPath) - - headRef, err := gitRepository.Head() - if err != nil { - return nil, fmt.Errorf("error getting head ref: %w", err) - } - - headCommit, err := gitRepository.CommitObject(headRef.Hash()) - if err != nil { - return nil, fmt.Errorf("failed to get head commit: %w", err) - } + log.Debug().Msgf("git target: [%s]/%s", context.RootDir, gitTargetPath) repository := &Repository{ - ctx: ctx, - config: config, - git: gitRepository, - rootPath: rootPath, - targetPath: targetPath, - gitTargetPath: gitTargetPath, - baseRemoteRefName: plumbing.NewRemoteReferenceName("origin", baseBranch), - baseLocalRefName: plumbing.NewBranchReferenceName(baseBranch), - headRef: headRef, - headCommit: headCommit, - githubToken: config.Scan.GithubToken, + ctx: ctx, + config: config, + targetPath: targetPath, + gitTargetPath: gitTargetPath, + context: context, } - repository.mergeBaseCommit, err = repository.fetchMergeBaseCommit(baseBranch) - if err != nil { + if err = repository.fetchMergeBaseCommit(); err != nil { return nil, err } @@ -111,168 +63,95 @@ func (repository *Repository) ListFiles( return nil, nil } - headTree, err := repository.headCommit.Tree() - if err != nil { - return nil, fmt.Errorf("failed to get head tree: %w", err) - } - - if repository.mergeBaseCommit == nil { - return repository.getTreeFiles(ignore, goclocResult, headTree) - } - - filePatches, err := repository.getDiffPatch(headTree) - if err != nil { - return nil, err + if repository.context.BaseCommitHash == "" { + return repository.getCurrentFiles(ignore, goclocResult) } - return repository.getDiffFiles(ignore, goclocResult, filePatches) + return repository.getDiffFiles(ignore, goclocResult) } -func (repository *Repository) fetchMergeBaseCommit(baseBranch string) (*object.Commit, error) { - if baseBranch == "" { - return nil, nil - } - - hash, err := repository.lookupMergeBaseHash(baseBranch) - if err != nil { - return nil, fmt.Errorf("error looking up hash: %w", err) - } - - if hash == nil { - return nil, fmt.Errorf( - "could not find common ancestor between the current and %s branch. "+ - "please check that the base branch is correct, and that you have "+ - "fetched enough git history to include the latest common ancestor", - baseBranch, - ) +func (repository *Repository) fetchMergeBaseCommit() error { + hash := repository.context.BaseCommitHash + if hash == "" { + return nil } log.Debug().Msgf("merge base commit: %s", hash) - commit, err := repository.git.CommitObject(*hash) - if err == nil { - return commit, nil - } - - if err != plumbing.ErrObjectNotFound { - return nil, fmt.Errorf("error looking up commit: %w", err) + if isPresent, err := git.CommitPresent(repository.context.RootDir, hash); isPresent || err != nil { + return err } log.Debug().Msgf("merge base commit not present, fetching") - if err := repository.git.FetchContext(repository.ctx, &git.FetchOptions{ - RefSpecs: []config.RefSpec{config.RefSpec( - fmt.Sprintf("+%s:%s", hash.String(), repository.baseRemoteRefName), - )}, - Depth: 1, - Tags: git.NoTags, - }); err != nil { - return nil, fmt.Errorf("error fetching: %w", err) + if err := git.FetchRef(repository.ctx, repository.context.RootDir, hash); err != nil { + return err } log.Debug().Msgf("merge base commit fetched") - return repository.git.CommitObject(*hash) + return nil } -func (repository *Repository) getTreeFiles( +func (repository *Repository) getCurrentFiles( ignore *ignore.FileIgnore, goclocResult *gocloc.Result, - tree *object.Tree, ) (*files.List, error) { + if repository.context.CurrentCommitHash == "" { + return &files.List{}, nil + } + var headFiles []files.File - if err := tree.Files().ForEach(func(f *object.File) error { + gitFiles, err := git.ListTree(repository.context.RootDir, repository.context.CurrentCommitHash) + if err != nil { + return nil, err + } + + for _, file := range gitFiles { if file := repository.fileFor( ignore, goclocResult, - f.Name, + file.Filename, ); file != nil { headFiles = append(headFiles, *file) } - - return nil - }); err != nil { - return nil, err } return &files.List{Files: headFiles}, nil } -func (repository *Repository) getDiffPatch(headTree *object.Tree) ([]diff.FilePatch, error) { - baseTree, err := repository.mergeBaseCommit.Tree() - if err != nil { - return nil, err - } - - changes, err := object.DiffTreeWithOptions( - repository.ctx, - baseTree, - headTree, - object.DefaultDiffTreeOptions, - ) - if err != nil { - return nil, fmt.Errorf("error diffing tree: %w", err) - } - - patch, err := changes.PatchContext(repository.ctx) - if err != nil { - return nil, fmt.Errorf("error getting diff patch: %w", err) - } - - return patch.FilePatches(), nil -} - -func (repository *Repository) translateDiffChunks(gitChunks []diff.Chunk) bbftypes.Chunks { - chunks := basebranchfindings.NewChunks() - for _, chunk := range gitChunks { - var changeType bbftypes.ChangeType - switch chunk.Type() { - case diff.Delete: - changeType = bbftypes.ChunkRemove - case diff.Add: - changeType = bbftypes.ChunkAdd - case diff.Equal: - changeType = bbftypes.ChunkEqual - default: - panic(fmt.Sprintf("unexpected git chunk type %d", chunk.Type())) - } - - chunks.Add(changeType, strings.Count(chunk.Content(), "\n")) - } - - return chunks -} - func (repository *Repository) getDiffFiles( ignore *ignore.FileIgnore, goclocResult *gocloc.Result, - filePatches []diff.FilePatch, ) (*files.List, error) { var baseFiles, headFiles []files.File renames := make(map[string]string) - chunks := make(map[string]bbftypes.Chunks) + chunks := make(map[string]git.Chunks) - for _, filePatch := range filePatches { - fromFile, toFile := filePatch.Files() + filePatches, err := git.Diff(repository.context.RootDir, repository.context.BaseCommitHash) + if err != nil { + return nil, err + } + for _, patch := range filePatches { // we're not interested in removals - if toFile == nil { + if patch.ToPath == "" { continue } - headFile := repository.fileFor(ignore, goclocResult, toFile.Path()) + headFile := repository.fileFor(ignore, goclocResult, patch.ToPath) if headFile == nil { continue } headFiles = append(headFiles, *headFile) - if fromFile == nil { + if patch.FromPath == "" { continue } - relativeFromPath, err := filepath.Rel(repository.gitTargetPath, fromFile.Path()) + relativeFromPath, err := filepath.Rel(repository.gitTargetPath, patch.FromPath) if err != nil { return nil, err } @@ -285,7 +164,7 @@ func (repository *Repository) getDiffFiles( renames[relativeFromPath] = headFile.FilePath } - chunks[headFile.FilePath] = repository.translateDiffChunks(filePatch.Chunks()) + chunks[headFile.FilePath] = patch.Chunks } return &files.List{ @@ -297,48 +176,38 @@ func (repository *Repository) getDiffFiles( } func (repository *Repository) WithBaseBranch(body func() error) error { - if repository == nil || repository.mergeBaseCommit == nil { + if repository == nil || !repository.config.Scan.Diff { return nil } - worktree, err := repository.git.Worktree() - if err != nil { - return fmt.Errorf("error getting git worktree: %w", err) + if repository.context.HasUncommittedChanges { + return errors.New("uncommitted changes found in your repository. commit or stash changes your changes and retry") } - status, err := worktree.Status() - if err != nil { - return err - } - if !status.IsClean() { - return errors.New("uncommitted changes found in worktree. commit or stash changes your changes and retry") + if err := git.Switch(repository.context.RootDir, repository.context.BaseCommitHash, true); err != nil { + return fmt.Errorf("error switching to base branch: %w", err) } - defer repository.restoreHead(worktree) + err := body() + + if restoreErr := repository.restoreCurrent(); restoreErr != nil { + wrappedErr := fmt.Errorf("error restoring to current commit: %w", restoreErr) + if err == nil { + return wrappedErr + } - if err := worktree.Checkout(&git.CheckoutOptions{ - Hash: repository.mergeBaseCommit.Hash, - }); err != nil { - return fmt.Errorf("error checking out base branch: %w", err) + output.StdErrLog(wrappedErr.Error()) } - return body() + return err } -func (repository *Repository) restoreHead(worktree *git.Worktree) { - checkoutOptions := &git.CheckoutOptions{} - if repository.headRef.Name().IsBranch() { - checkoutOptions.Branch = repository.headRef.Name() - } else { - checkoutOptions.Hash = repository.headRef.Hash() +func (repository *Repository) restoreCurrent() error { + if repository.context.CurrentBranch == "" { + return git.Switch(repository.context.RootDir, repository.context.CurrentCommitHash, true) } - if err := worktree.Checkout(checkoutOptions); err != nil { - log.Error().Msgf( - "error restoring git worktree. your worktree may not have been restored to it's original state! %s", - err, - ) - } + return git.Switch(repository.context.RootDir, repository.context.CurrentBranch, false) } func (repository *Repository) fileFor( @@ -373,168 +242,3 @@ func (repository *Repository) fileFor( FilePath: relativePath, } } - -func translateOpenError(baseBranch string, err error) error { - if err != git.ErrRepositoryNotExists { - return err - } - - log.Debug().Msg("no git repository found") - - if baseBranch != "" { - return errors.New("base branch specified but no git repository found") - } - - return nil -} - -func (repository *Repository) lookupMergeBaseHash(baseBranch string) (*plumbing.Hash, error) { - if hash := repository.lookupMergeBaseRefFromVariable(); hash != nil { - return hash, nil - } - - if hash, err := repository.lookupMergeBaseRefFromGithub(baseBranch); hash != nil || err != nil { - return hash, err - } - - log.Debug().Msg("finding merge base using local repository") - - ref, err := repository.git.Reference(repository.baseRemoteRefName, true) - if err != nil && err != plumbing.ErrReferenceNotFound { - if err == plumbing.ErrReferenceNotFound { - return nil, nil - } - - return nil, fmt.Errorf("invalid ref: %w", err) - } - - if err == plumbing.ErrReferenceNotFound { - log.Debug().Msg("remote ref not found, trying local ref") - ref, err = repository.git.Reference(repository.baseLocalRefName, true) - if err != nil { - if err == plumbing.ErrReferenceNotFound { - return nil, nil - } - - return nil, fmt.Errorf("invalid ref: %w", err) - } - } - - baseCommit, err := repository.git.CommitObject(ref.Hash()) - if err != nil { - if err == plumbing.ErrObjectNotFound { - return nil, nil - } - - return nil, fmt.Errorf("error looking up base commit: %w", err) - } - - commonAncestors, err := repository.headCommit.MergeBase(baseCommit) - if err != nil { - if err == plumbing.ErrObjectNotFound { - return nil, nil - } - - return nil, fmt.Errorf("error computing merge base: %w", err) - } - if len(commonAncestors) == 0 { - return nil, nil - } - - return &commonAncestors[0].Hash, nil -} - -func (repository *Repository) lookupMergeBaseRefFromVariable() *plumbing.Hash { - sha := os.Getenv("DIFF_BASE_COMMIT") - if sha == "" { - return nil - } - - hash := plumbing.NewHash(sha) - return &hash -} - -func (repository *Repository) lookupMergeBaseRefFromGithub(baseBranch string) (*plumbing.Hash, error) { - if repository.githubToken == "" { - return nil, nil - } - - githubRepository := os.Getenv("GITHUB_REPOSITORY") - if githubRepository == "" { - return nil, nil - } - - log.Debug().Msg("finding merge base using github api") - - splitRepository := strings.SplitN(githubRepository, "/", 2) - if len(splitRepository) != 2 { - return nil, fmt.Errorf("invalid github repository name '%s'", githubRepository) - } - - client, err := repository.newGithubClient() - if err != nil { - return nil, err - } - - comparison, _, err := client.Repositories.CompareCommits( - context.Background(), - splitRepository[0], - splitRepository[1], - baseBranch, - repository.headRef.Hash().String(), - ) - if err != nil { - return nil, fmt.Errorf("error calling github compare api: %w", err) - } - - if comparison.MergeBaseCommit == nil { - return nil, nil - } - - hash := plumbing.NewHash(*comparison.MergeBaseCommit.SHA) - return &hash, nil -} - -func (repository *Repository) newGithubClient() (*github.Client, error) { - tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: repository.githubToken}) - httpClient := oauth2.NewClient(context.Background(), tokenSource) - client := github.NewClient(httpClient) - - if githubAPIURL := os.Getenv("GITHUB_API_URL"); githubAPIURL != "" { - parsedURL, err := url.Parse(githubAPIURL) - if err != nil { - return nil, fmt.Errorf("failed to parse github api url: %w", err) - } - - if !strings.HasSuffix(parsedURL.Path, "/") { - parsedURL.Path += "/" - } - - client.BaseURL = parsedURL - } - - return client, nil -} - -type GithubTransport struct { - githubToken string -} - -func (transport *GithubTransport) RoundTrip(req *http.Request) (*http.Response, error) { - req.SetBasicAuth("x-access-token", transport.githubToken) - - return http.DefaultTransport.RoundTrip(req) -} - -func ConfigureGithubAuth(githubToken string) { - if githubToken == "" { - return - } - - githubClient := githttp.NewClient(&http.Client{ - Transport: &GithubTransport{githubToken: githubToken}, - }) - - client.InstallProtocol("http", githubClient) - client.InstallProtocol("https", githubClient) -} diff --git a/internal/commands/process/orchestrator/pool/process.go b/internal/commands/process/orchestrator/pool/process.go index 90191d93b..cbd30561f 100644 --- a/internal/commands/process/orchestrator/pool/process.go +++ b/internal/commands/process/orchestrator/pool/process.go @@ -90,6 +90,7 @@ func newProcess(options *ProcessOptions, id string) (*Process, error) { func (process *Process) start(config settings.Config) error { if err := process.command.Start(); err != nil { + close(process.exitChannel) return err } diff --git a/internal/commands/process/settings/settings.go b/internal/commands/process/settings/settings.go index 959cfbeb0..33d06aa0f 100644 --- a/internal/commands/process/settings/settings.go +++ b/internal/commands/process/settings/settings.go @@ -369,7 +369,7 @@ func FromOptions(opts flag.Options, versionMeta *version_check.VersionMeta) (Con BearerRulesVersion: result.BearerRulesVersion, } - if config.Scan.DiffBaseBranch != "" { + if config.Scan.Diff { if !slices.Contains([]string{flag.ReportSecurity, flag.ReportSaaS}, config.Report.Report) { return Config{}, errors.New("diff base branch is only supported for the security report") } diff --git a/internal/commands/scan.go b/internal/commands/scan.go index 8425cf876..988056347 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -34,6 +34,7 @@ var ScanFlags = flag.Flags{ flag.ReportFlagGroup, flag.RuleFlagGroup, flag.ScanFlagGroup, + flag.RepositoryFlagGroup, flag.GeneralFlagGroup, } diff --git a/internal/detectors/dotnet/dotnet.go b/internal/detectors/dotnet/dotnet.go index af6832342..31d8e83d9 100644 --- a/internal/detectors/dotnet/dotnet.go +++ b/internal/detectors/dotnet/dotnet.go @@ -80,12 +80,7 @@ func (detector *detector) ProcessFile(file *file.FileInfo, dir *file.Path, repor } func isProject(path string) (bool, error) { - handleDir, err := isDir(path) - if err != nil { - return false, err - } - - if handleDir { + if file.IsDir(path) { fileInfos, err := os.ReadDir(path) if err != nil { return false, err @@ -127,12 +122,3 @@ func matchFilepath(filename string) (bool, error) { return false, nil } - -func isDir(path string) (bool, error) { - fileInfo, err := os.Stat(path) - if err != nil { - return false, err - } - - return fileInfo.IsDir(), nil -} diff --git a/internal/flag/options.go b/internal/flag/options.go index f125144eb..9db2c1969 100644 --- a/internal/flag/options.go +++ b/internal/flag/options.go @@ -42,6 +42,9 @@ type Flag struct { // Deprecated represents if the flag is deprecated Deprecated bool + + // Additional environment variables to read the value from, in addition to the default + EnvironmentVariables []string } type flagGroupBase struct { @@ -62,6 +65,7 @@ type Options struct { ReportOptions RuleOptions ScanOptions + RepositoryOptions GeneralOptions IgnoreAddOptions IgnoreShowOptions @@ -110,7 +114,11 @@ func bind(cmd *cobra.Command, flag *Flag) error { // viper.SetEnvPrefix("bearer") // replacer := strings.NewReplacer("-", "_") // viper.SetEnvKeyReplacer(replacer) - if err := viper.BindEnv(flag.ConfigName, strings.ToUpper("bearer_"+strings.ReplaceAll(flag.Name, "-", "_"))); err != nil { + arguments := append( + []string{flag.ConfigName, strings.ToUpper("bearer_" + strings.ReplaceAll(flag.Name, "-", "_"))}, + flag.EnvironmentVariables..., + ) + if err := viper.BindEnv(arguments...); err != nil { return err } diff --git a/internal/flag/repository_flags.go b/internal/flag/repository_flags.go new file mode 100644 index 000000000..ae483efd5 --- /dev/null +++ b/internal/flag/repository_flags.go @@ -0,0 +1,141 @@ +package flag + +type repositoryFlagGroup struct{ flagGroupBase } + +var RepositoryFlagGroup = &repositoryFlagGroup{flagGroupBase{name: "Repository"}} + +var ( + RepositoryURLFlag = RepositoryFlagGroup.add(Flag{ + Name: "repository-url", + ConfigName: "repository.url", + Value: "", + Usage: "The remote URL of the repository.", + EnvironmentVariables: []string{ + "ORIGIN_URL", // legacy + "CI_REPOSITORY_URL", // gitlab + }, + DisableInConfig: true, + Hide: true, + }) + BranchFlag = RepositoryFlagGroup.add(Flag{ + Name: "branch", + ConfigName: "repository.branch", + Value: "", + Usage: "The name of the branch being scanned.", + EnvironmentVariables: []string{ + "CURRENT_BRANCH", // legacy + "CI_COMMIT_REF_NAME", // gitlab + }, + DisableInConfig: true, + Hide: true, + }) + CommitFlag = RepositoryFlagGroup.add(Flag{ + Name: "commit", + ConfigName: "repository.commit", + Value: "", + Usage: "The hash of the commit being scanned.", + EnvironmentVariables: []string{ + "SHA", // legacy + "CI_COMMIT_SHA", // gitlab + }, + DisableInConfig: true, + Hide: true, + }) + DefaultBranchFlag = RepositoryFlagGroup.add(Flag{ + Name: "default-branch", + ConfigName: "repository.default-branch", + Value: "", + Usage: "The name of the default branch.", + EnvironmentVariables: []string{ + "DEFAULT_BRANCH", // legacy + "CI_DEFAULT_BRANCH", // gitlab + }, + DisableInConfig: true, + Hide: true, + }) + DiffBaseBranchFlag = RepositoryFlagGroup.add(Flag{ + Name: "diff-base-branch", + ConfigName: "repository.diff-base-branch", + Value: "", + Usage: "The name of the base branch to use for diff scanning.", + EnvironmentVariables: []string{ + "DIFF_BASE_BRANCH", // legacy + "CI_MERGE_REQUEST_TARGET_BRANCH_NAME", // gitlab + }, + DisableInConfig: true, + Hide: true, + }) + DiffBaseCommitFlag = RepositoryFlagGroup.add(Flag{ + Name: "diff-base-commit", + ConfigName: "repository.diff-base-commit", + Value: "", + Usage: "The hash of the base commit to use for diff scanning.", + EnvironmentVariables: []string{ + "DIFF_BASE_COMMIT", // legacy + "CI_MERGE_REQUEST_DIFF_BASE_SHA", // gitlab + }, + DisableInConfig: true, + Hide: true, + }) + GithubTokenFlag = RepositoryFlagGroup.add(Flag{ + Name: "github-token", + ConfigName: "repository.github-token", + Value: "", + Usage: "An access token for the Github API.", + EnvironmentVariables: []string{ + "GITHUB_TOKEN", // github + }, + DisableInConfig: true, + Hide: true, + }) + GithubRepositoryFlag = RepositoryFlagGroup.add(Flag{ + Name: "github-repository", + ConfigName: "repository.github-repository", + Value: "", + Usage: "The owner and name of the repository on Github. eg. Bearer/bearer", + EnvironmentVariables: []string{ + "GITHUB_REPOSITORY", // github + }, + DisableInConfig: true, + Hide: true, + }) + GithubAPIURLFlag = RepositoryFlagGroup.add(Flag{ + Name: "github-api-url", + ConfigName: "repository.github-api-url", + Value: "", + Usage: "A non-standard URL to use for the Github API", + EnvironmentVariables: []string{ + "GITHUB_API_URL", // github + }, + DisableInConfig: true, + Hide: true, + }) +) + +type RepositoryOptions struct { + OriginURL string + Branch string + Commit string + DefaultBranch string + DiffBaseBranch string + DiffBaseCommit string + GithubToken string + GithubRepository string + GithubAPIURL string +} + +func (repositoryFlagGroup) SetOptions(options *Options, args []string) error { + options.RepositoryOptions = RepositoryOptions{ + OriginURL: getString(RepositoryURLFlag), + Branch: getString(BranchFlag), + Commit: getString(CommitFlag), + DefaultBranch: getString(DefaultBranchFlag), + DiffBaseBranch: getString(DiffBaseBranchFlag), + DiffBaseCommit: getString(DiffBaseCommitFlag), + GithubToken: getString(GithubTokenFlag), + GithubRepository: getString(GithubRepositoryFlag), + GithubAPIURL: getString(GithubAPIURLFlag), + } + + return nil +} diff --git a/internal/flag/scan_flags.go b/internal/flag/scan_flags.go index ff893ae40..2c631e201 100644 --- a/internal/flag/scan_flags.go +++ b/internal/flag/scan_flags.go @@ -107,6 +107,13 @@ var ( Value: -1, Usage: "Force a given exit code for the scan command. Set this to 0 (success) to always return a success exit code despite any findings from the scan.", }) + DiffFlag = ScanFlagGroup.add(Flag{ + Name: "diff", + ConfigName: "scan.diff", + Value: false, + Usage: "Only report differences in findings relative to a base branch.", + DisableInConfig: true, + }) ) type ScanOptions struct { @@ -124,8 +131,7 @@ type ScanOptions struct { Scanner []string `mapstructure:"scanner" json:"scanner" yaml:"scanner"` Parallel int `mapstructure:"parallel" json:"parallel" yaml:"parallel"` ExitCode int `mapstructure:"exit-code" json:"exit-code" yaml:"exit-code"` - DiffBaseBranch string `mapstructure:"diff_base_branch" json:"diff_base_branch" yaml:"diff_base_branch"` - GithubToken string `mapstructure:"github_token" json:"github_token" yaml:"github_token"` + Diff bool `mapstructure:"diff" json:"diff" yaml:"diff"` } func (scanFlagGroup) SetOptions(options *Options, args []string) error { @@ -151,6 +157,9 @@ func (scanFlagGroup) SetOptions(options *Options, args []string) error { } } + // DIFF_BASE_BRANCH is used for backwards compatibilty + diff := getBool(DiffFlag) || os.Getenv("DIFF_BASE_BRANCH") != "" + options.ScanOptions = ScanOptions{ SkipPath: getStringSlice(SkipPathFlag), DisableDomainResolution: getBool(DisableDomainResolutionFlag), @@ -166,8 +175,7 @@ func (scanFlagGroup) SetOptions(options *Options, args []string) error { Scanner: scanners, Parallel: viper.GetInt(ParallelFlag.ConfigName), ExitCode: viper.GetInt(ExitCodeFlag.ConfigName), - DiffBaseBranch: os.Getenv("DIFF_BASE_BRANCH"), - GithubToken: os.Getenv("GITHUB_TOKEN"), + Diff: diff, } return nil diff --git a/internal/git/branch.go b/internal/git/branch.go new file mode 100644 index 000000000..80949858a --- /dev/null +++ b/internal/git/branch.go @@ -0,0 +1,49 @@ +package git + +import ( + "context" + "strings" +) + +func GetDefaultBranch(dir string) (string, error) { + name, err := getRevParseAbbrevRef(dir, "origin/HEAD") + if err != nil { + if strings.Contains(err.Error(), "unknown revision") { + return "", nil + } + + return "", err + } + + return strings.TrimPrefix(name, "origin/"), nil +} + +// GetCurrentBranch gets the branch name. It is blank when detached. +func GetCurrentBranch(dir string) (string, error) { + name, err := getRevParseAbbrevRef(dir, "HEAD") + if err != nil { + if strings.Contains(err.Error(), "unknown revision") { + return "", nil + } + + return "", err + } + + if name == "HEAD" { + return "", nil + } + + return name, nil +} + +func getRevParseAbbrevRef(dir string, ref string) (name string, err error) { + output, err := captureCommandBasic( + context.TODO(), + dir, + "rev-parse", + "--abbrev-ref", + ref, + ) + + return strings.TrimSpace(output), err +} diff --git a/internal/git/checkout.go b/internal/git/checkout.go index d63661c01..9bf6d4cfd 100644 --- a/internal/git/checkout.go +++ b/internal/git/checkout.go @@ -1,103 +1,19 @@ package git -import "fmt" +import ( + "context" +) -func checkout(rootDir, ref string, filenames []string) error { - cmd := logAndBuildCommand( - "-c", - "advice.detachedHead=false", - "checkout", - ref, - "--pathspec-from-file=-", - "--pathspec-file-nul", - ) - cmd.Dir = rootDir - - stdin, err := cmd.StdinPipe() - if err != nil { - return err - } - - logWriter := &debugLogWriter{} - cmd.Stdout = logWriter - cmd.Stderr = logWriter - - if err := cmd.Start(); err != nil { - killProcess(cmd) - return err - } - - for _, filename := range filenames { - _, err := stdin.Write([]byte(filename)) - if err != nil { - killProcess(cmd) - return err - } - - _, err = stdin.Write([]byte{0}) - if err != nil { - killProcess(cmd) - return err - } - } - - if err := stdin.Close(); err != nil { - killProcess(cmd) - return err - } - - if err := cmd.Wait(); err != nil { - killProcess(cmd) - return newError(err, logWriter.AllOutput()) - } - - // Using pathspec with checkout doesn't update the HEAD ref so do it manually - return basicCommand(rootDir, "update-ref", "HEAD", ref) -} +func Switch(rootDir, ref string, detach bool) error { + args := []string{"switch"} -func fetchBlobsForRange(rootDir, firstCommitSHA, lastCommitSHA string, filenames []string) error { - objectIDs, err := getObjectIDsForRangeFiles(rootDir, firstCommitSHA, lastCommitSHA, filenames) - if err != nil { - return err + if detach { + args = append(args, "--detach") } - return fetchBlobs(rootDir, objectIDs) -} - -// Fetches the given list of objects/blobs. -// -// There's no command in git that does this directly but we can get the desired -// behaviour by creating a pack and throwing it away -func fetchBlobs(rootDir string, objectIDs []string) error { - cmd := logAndBuildCommand("pack-objects", "--progress", "--stdout") - cmd.Dir = rootDir - - stdin, err := cmd.StdinPipe() - if err != nil { - return err - } - - logWriter := &debugLogWriter{} - cmd.Stderr = logWriter - - if err := cmd.Start(); err != nil { - killProcess(cmd) - return err - } - - for _, objectID := range objectIDs { - fmt.Fprintln(stdin, objectID) - } - - if err := stdin.Close(); err != nil { - killProcess(cmd) - return err - } - - if err := cmd.Wait(); err != nil { - killProcess(cmd) - return newError(err, logWriter.AllOutput()) - } - - return nil + return basicCommand( + context.TODO(), + rootDir, + append(args, ref)..., + ) } diff --git a/internal/git/clone.go b/internal/git/clone.go deleted file mode 100644 index fba296860..000000000 --- a/internal/git/clone.go +++ /dev/null @@ -1,159 +0,0 @@ -package git - -import ( - "errors" - "fmt" - "net/url" - "os" - "slices" - "time" - - "github.com/rs/zerolog/log" -) - -func CloneAndGetTree(token string, url *url.URL, branchName string) (*Tree, error) { - tempDir, err := os.MkdirTemp("", "tree") - if err != nil { - return nil, fmt.Errorf("failed to create temp dir: %s", err) - } - defer os.RemoveAll(tempDir) - - if err := cloneTree(tempDir, urlWithCredentials(url, token), branchName); err != nil { - return nil, fmt.Errorf("failed to clone: %s", err) - } - - return GetTree(tempDir) -} - -func CloneRangeAndCheckoutFiles( - token string, - url *url.URL, - branchName string, - previousCommit *CommitIdentifier, - commit CommitIdentifier, - filenames []string, - targetDir string, -) (bool, error) { - isRange, err := cloneTreeRange(urlWithCredentials(url, token), branchName, previousCommit, commit, targetDir) - if err != nil { - return false, err - } - - firstCommitSHA := commit.SHA - if isRange { - firstCommitSHA = previousCommit.SHA - } - - treeFiles, err := listTree(targetDir, commit.SHA) - if err != nil { - return false, err - } - - filenames = appendMailmap(filenames, treeFiles) - - if err := fetchBlobsForRange(targetDir, firstCommitSHA, commit.SHA, filenames); err != nil { - return false, err - } - - // Remove remote to avoid accidentally fetching more data - if err := removeRemote(targetDir); err != nil { - return false, err - } - - if err := checkout(targetDir, commit.SHA, filenames); err != nil { - return false, err - } - - return isRange, nil -} - -func appendMailmap(filenames []string, treeFiles []TreeFile) []string { - if slices.Contains(filenames, mailmapFilename) { - return filenames - } - - for _, treeFile := range treeFiles { - if treeFile.Filename == mailmapFilename { - return append(filenames, mailmapFilename) - } - } - - return filenames -} - -func cloneTree(targetDir string, url *url.URL, branchName string) error { - return basicCommand( - "", - "clone", - "--depth=1", - "--branch", - branchName, - "--no-checkout", - "--no-tags", - "--filter=blob:none", - "--progress", - url.String(), - targetDir, - ) -} - -func cloneTreeRange( - url *url.URL, - branchName string, - previousCommit *CommitIdentifier, - commit CommitIdentifier, - targetDir string, -) (bool, error) { - if previousCommit == nil { - if err := cloneTreeSince(url, branchName, commit, targetDir); err != nil { - return false, err - } - } else { - if err := cloneTreeSince(url, branchName, *previousCommit, targetDir); err != nil { - return false, err - } - } - - if !commitPresent(targetDir, commit.SHA) { - return false, errors.New("target commit not found") - } - - if previousCommit != nil && !commitPresent(targetDir, previousCommit.SHA) { - log.Debug().Msg("previous commit is missing, possible re-written history") - // Fallback to non range clone - return false, nil - } - - return previousCommit != nil, nil -} - -func cloneTreeSince(url *url.URL, branchName string, commit CommitIdentifier, targetDir string) error { - cmd := logAndBuildCommand( - "clone", - "--shallow-since="+commit.Timestamp.Format(time.RFC3339), - "--branch", - branchName, - "--single-branch", - "--no-checkout", - "--no-tags", - "--filter=blob:none", - "--progress", - url.String(), - targetDir, - ) - - logWriter := &debugLogWriter{} - cmd.Stdout = logWriter - cmd.Stderr = logWriter - - if err := cmd.Run(); err != nil { - killProcess(cmd) - return newError(err, logWriter.AllOutput()) - } - - return nil -} - -func removeRemote(rootDir string) error { - return basicCommand(rootDir, "remote", "remove", "origin") -} diff --git a/internal/git/commit_list.go b/internal/git/commit_list.go deleted file mode 100644 index ecb43debf..000000000 --- a/internal/git/commit_list.go +++ /dev/null @@ -1,194 +0,0 @@ -package git - -import ( - "bufio" - "fmt" - "regexp" - "strings" - "time" -) - -var coAuthorValidPattern = regexp.MustCompile(`<.*>`) - -type CommitInfo struct { - CommitIdentifier - Committer string `json:"committer" yaml:"committer"` - Author string `json:"author" yaml:"author"` - CoAuthors []string `json:"co_authors" yaml:"co_authors"` -} - -func GetCommitList(rootDir, firstCommitSHA, lastCommitSHA string) ([]CommitInfo, error) { - separator := "---" - result := []CommitInfo{} - - cmd := logAndBuildCommand( - "log", - "--first-parent", - "--format=%H %cI%n%cN <%cE>%n%aN <%aE>%n%(trailers:unfold,valueonly,key=Co-authored-by)"+separator, - lastCommitSHA, - "--", - ) - cmd.Dir = rootDir - - logWriter := &debugLogWriter{} - cmd.Stderr = logWriter - - stdout, err := cmd.StdoutPipe() - if err != nil { - killProcess(cmd) - return nil, err - } - - if err := cmd.Start(); err != nil { - killProcess(cmd) - return nil, err - } - - scanner := bufio.NewScanner(stdout) - info := CommitInfo{} - n := 0 - - for scanner.Scan() { - line := scanner.Text() - if line == separator { - result = append(result, info) - - if info.SHA == firstCommitSHA { - break - } - - info = CommitInfo{} - n = 0 - continue - } - - switch n { - case 0: - splitLine := strings.SplitN(line, " ", 2) - - parsedTimestamp, err := time.Parse(time.RFC3339, splitLine[1]) - if err != nil { - return nil, err - } - - info.SHA = splitLine[0] - info.Timestamp = parsedTimestamp.UTC() - case 1: - info.Committer = line - case 2: - info.Author = line - default: - info.CoAuthors = append(info.CoAuthors, line) - } - - n++ - } - - if err := scanner.Err(); err != nil { - killProcess(cmd) - return nil, err - } - - stdout.Close() - - if err := cmd.Wait(); err != nil { - killProcess(cmd) - return nil, newError(err, logWriter.AllOutput()) - } - - if err := translateCoAuthors(rootDir, result); err != nil { - return nil, err - } - - return result, nil -} - -func translateCoAuthors(rootDir string, commitList []CommitInfo) error { - translatedCoAuthors := make(map[string]string) - toTranslate := []string{} - - for _, commitInfo := range commitList { - for _, author := range commitInfo.CoAuthors { - translated := author - if coAuthorValidPattern.MatchString(author) { - translated = "" - toTranslate = append(toTranslate, author) - } - - translatedCoAuthors[author] = translated - } - } - - mailmapAuthors, err := checkMailmap(rootDir, toTranslate) - if err != nil { - return err - } - - for i, author := range toTranslate { - translatedCoAuthors[author] = mailmapAuthors[i] - } - - for i := range commitList { - commitInfo := &commitList[i] - - for j := range commitInfo.CoAuthors { - commitInfo.CoAuthors[j] = translatedCoAuthors[commitInfo.CoAuthors[j]] - } - } - - return nil -} - -func checkMailmap(rootDir string, authors []string) ([]string, error) { - cmd := logAndBuildCommand("check-mailmap", "--stdin") - cmd.Dir = rootDir - - stdin, err := cmd.StdinPipe() - if err != nil { - killProcess(cmd) - return nil, err - } - - go func() { - for _, author := range authors { - fmt.Fprintln(stdin, author) - } - - stdin.Close() - }() - - logWriter := &debugLogWriter{} - cmd.Stderr = logWriter - - stdout, err := cmd.StdoutPipe() - if err != nil { - killProcess(cmd) - return nil, err - } - - if err := cmd.Start(); err != nil { - killProcess(cmd) - return nil, err - } - - result := []string{} - scanner := bufio.NewScanner(stdout) - - for scanner.Scan() { - result = append(result, scanner.Text()) - } - - if err := scanner.Err(); err != nil { - killProcess(cmd) - return nil, err - } - - stdout.Close() - - if err := cmd.Wait(); err != nil { - killProcess(cmd) - return nil, newError(err, logWriter.AllOutput()) - } - - return result, nil -} diff --git a/internal/git/commits.go b/internal/git/commits.go new file mode 100644 index 000000000..ef72eac0e --- /dev/null +++ b/internal/git/commits.go @@ -0,0 +1,53 @@ +package git + +import ( + "context" + "strings" +) + +// GetCurrentCommit gets a current commit from a HEAD for a local directory +func GetCurrentCommit(rootDir string) (string, error) { + output, err := captureCommandBasic( + context.TODO(), + rootDir, + "rev-parse", + "HEAD", + ) + + if err != nil && strings.Contains(err.Error(), "unknown revision") { + return "", nil + } + + return strings.TrimSpace(output), err +} + +func GetMergeBase(rootDir string, ref1, ref2 string) (string, error) { + output, err := captureCommandBasic( + context.TODO(), + rootDir, + "merge-base", + ref1, + ref2, + ) + + return strings.TrimSpace(output), err +} + +func CommitPresent(rootDir, hash string) (bool, error) { + output, err := captureCommandBasic( + context.TODO(), + rootDir, + "cat-file", + "-t", + hash, + ) + if err != nil { + if strings.Contains(err.Error(), "could not get object info") { + return false, nil + } + + return false, err + } + + return output == "commit\n", nil +} diff --git a/internal/git/current_commit.go b/internal/git/current_commit.go deleted file mode 100644 index 726465083..000000000 --- a/internal/git/current_commit.go +++ /dev/null @@ -1,36 +0,0 @@ -package git - -import "bufio" - -// GetCurrentCommit gets a current commit from a HEAD for a local directory -func GetCurrentCommit(dir string) (hash string, err error) { - cmd := logAndBuildCommand( - "rev-parse", - "HEAD", - ) - cmd.Dir = dir - - logWriter := &debugLogWriter{} - cmd.Stderr = logWriter - - stdout, err := cmd.StdoutPipe() - if err != nil { - killProcess(cmd) - return "", err - } - - if err := cmd.Start(); err != nil { - killProcess(cmd) - return "", err - } - - if err := cmd.Wait(); err != nil { - killProcess(cmd) - return "", newError(err, logWriter.AllOutput()) - } - - scanner := bufio.NewScanner(stdout) - hashB := scanner.Bytes() - - return string(hashB), nil -} diff --git a/internal/git/defunct_cleanup.go b/internal/git/defunct_cleanup.go deleted file mode 100644 index 1d443a2cc..000000000 --- a/internal/git/defunct_cleanup.go +++ /dev/null @@ -1,59 +0,0 @@ -package git - -import ( - "context" - "os/exec" - "strings" - "time" - - "github.com/rs/zerolog/log" -) - -// MonitorDefunct periodically cleans up defunct zombie git proccess that happen on git error and pile up over time -func MonitorDefunct(ctx context.Context) { - timer := time.NewTicker(5 * time.Second) - for { - select { - case <-timer.C: - cleanupDefunct() - case <-ctx.Done(): - return - } - } -} - -func cleanupDefunct() { - log.Debug().Msgf("[git] defunct checking for abandoned process") - - cmdProcess := exec.Command("ps", "-A", "-H") - stdout, err := cmdProcess.CombinedOutput() - if err != nil { - log.Debug().Msgf("failed to get output of command %s", err) - } - - defunctPIDs := make([]string, 0) - - lines := strings.Split(string(stdout), "\n") - - for _, line := range lines { - if !regexpDefunctProcess.MatchString(line) { - continue - } - - pid := regexpPID.FindString(line) - if pid == "" { - continue - } - - defunctPIDs = append(defunctPIDs, strings.Trim(pid, " ")) - } - - for _, pid := range defunctPIDs { - cmdKill := exec.Command("kill", pid) - if err := cmdKill.Run(); err != nil { - log.Debug().Msgf("failed to kill git process %s", err) - } - } - - log.Debug().Msgf("[git] defunct process cleanup found %d processes", len(defunctPIDs)) -} diff --git a/internal/git/diff.go b/internal/git/diff.go new file mode 100644 index 000000000..a26652815 --- /dev/null +++ b/internal/git/diff.go @@ -0,0 +1,226 @@ +package git + +import ( + "bufio" + "context" + "fmt" + "io" + "strconv" + "strings" +) + +type ChunkRange struct { + LineNumber, + LineCount int +} + +type Chunks []Chunk + +type Chunk struct { + From ChunkRange + To ChunkRange +} + +type FilePatch struct { + FromPath, + ToPath string + Chunks Chunks +} + +func Diff(rootDir, baseRef string) ([]FilePatch, error) { + var result []FilePatch + + err := captureCommand( + context.TODO(), + rootDir, + []string{ + "diff", + "--unified=0", + "--first-parent", + "--find-renames", + "--break-rewrites", + "--no-prefix", + "--no-color", + baseRef, + "--", + }, + func(stdout io.Reader) error { + var err error + result, err = parseDiff(bufio.NewScanner(stdout)) + if err != nil { + return err + } + + return nil + }, + ) + + return result, err +} + +func parseDiff(scanner *bufio.Scanner) ([]FilePatch, error) { + var result []FilePatch + var fromPath, toPath string + var chunks []Chunk + + flush := func() { + if fromPath == "" && toPath == "" { + return + } + + result = append(result, FilePatch{ + FromPath: fromPath, + ToPath: toPath, + Chunks: chunks, + }) + + fromPath = "" + toPath = "" + chunks = nil + } + + for scanner.Scan() { + line := scanner.Text() + + var err error + switch { + case strings.HasPrefix(line, "diff --git"): + flush() + + fromPath, toPath, err = parseDiffHeader(line) + if err != nil { + return nil, err + } + case strings.HasPrefix(line, "new file"): + fromPath = "" + case strings.HasPrefix(line, "deleted file"): + toPath = "" + case strings.HasPrefix(line, "@@"): + chunk, err := parseChunkHeader(line) + if err != nil { + return nil, err + } + + chunks = append(chunks, chunk) + } + + } + + flush() + + return result, nil +} + +func parseDiffHeader(value string) (string, string, error) { + parts := strings.Split(value, " ") + fromPath, err := unquoteFilename(parts[2]) + if err != nil { + return "", "", fmt.Errorf("error parsing header 'from' path: %w", err) + } + + toPath, err := unquoteFilename(parts[3]) + if err != nil { + return "", "", fmt.Errorf("error parsing header 'to' path: %w", err) + } + + return fromPath, toPath, nil +} + +func parseChunkHeader(value string) (Chunk, error) { + parts := strings.Split(value, " ") + + fromRange, err := parseRange(parts[1]) + if err != nil { + return Chunk{}, fmt.Errorf("failed to parse chunk 'from' range: %w", err) + } + + toRange, err := parseRange(parts[2]) + if err != nil { + return Chunk{}, fmt.Errorf("failed to parse chunk 'to' range: %w", err) + } + + return Chunk{From: fromRange, To: toRange}, nil +} + +func parseRange(value string) (ChunkRange, error) { + parts := strings.Split(value[1:], ",") + + lineNumber, err := strconv.Atoi(parts[0]) + if err != nil { + return ChunkRange{}, fmt.Errorf("error decoding line number: %w", err) + } + + count := 1 + if len(parts) > 1 { + var err error + count, err = strconv.Atoi(parts[1]) + if err != nil { + return ChunkRange{}, fmt.Errorf("error decoding line count: %w", err) + } + } + + return ChunkRange{LineNumber: lineNumber, LineCount: count}, nil +} + +func (chunks Chunks) TranslateRange(baseRange ChunkRange) ChunkRange { + baseStartLine := baseRange.LineNumber + startLine := baseStartLine + if startChunk := chunks.getClosestChunk(baseStartLine); startChunk != nil { + if baseStartLine > startChunk.From.EndLineNumber() { + startLine = baseStartLine + startChunk.EndDelta() + } else { + startLine = startChunk.To.LineNumber + } + } + + baseEndLine := baseRange.EndLineNumber() + endLine := baseEndLine + if endChunk := chunks.getClosestChunk(baseEndLine); endChunk != nil { + if baseEndLine > endChunk.From.EndLineNumber() { + endLine = baseEndLine + endChunk.EndDelta() + } else { + endLine = endChunk.To.EndLineNumber() + } + } + + lineCount := endLine - startLine + 1 + if endLine == 0 { + lineCount = 0 + } + + return ChunkRange{LineNumber: startLine, LineCount: lineCount} +} + +func (chunks Chunks) getClosestChunk(baseLineNumber int) *Chunk { + var result *Chunk + + for i, chunk := range chunks { + if chunk.From.StartLineNumber() > baseLineNumber { + break + } + + result = &chunks[i] + } + + return result +} + +func (chunk Chunk) EndDelta() int { + return chunk.To.EndLineNumber() - chunk.From.EndLineNumber() +} + +func (chunkRange ChunkRange) StartLineNumber() int { + if chunkRange.LineCount == 0 { + return chunkRange.LineNumber + 1 + } + + return chunkRange.LineNumber +} + +func (chunkRange ChunkRange) EndLineNumber() int { + return chunkRange.StartLineNumber() + chunkRange.LineCount - 1 +} + +func (chunkRange ChunkRange) Overlap(other ChunkRange) bool { + return chunkRange.LineNumber <= other.EndLineNumber() && chunkRange.EndLineNumber() >= other.LineNumber +} diff --git a/internal/git/diff_test.go b/internal/git/diff_test.go new file mode 100644 index 000000000..877ba8ef3 --- /dev/null +++ b/internal/git/diff_test.go @@ -0,0 +1,321 @@ +package git_test + +import ( + "os" + "path" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/bearer/bearer/internal/git" +) + +var _ = Describe("Diff", func() { + var tempDir, baseSHA string + filename := "foo.txt" + + BeforeEach(func() { + var err error + + tempDir, err = os.MkdirTemp("", "diff-test") + Expect(err).To(BeNil()) + + runGit(tempDir, "init", ".") + writeFile(tempDir, filename, "1\n2\n3") + addAndCommit(tempDir) + + baseSHA, err = git.GetCurrentCommit(tempDir) + Expect(err).To(BeNil()) + Expect(baseSHA).NotTo(BeEmpty()) + }) + + AfterEach(func() { + if tempDir != "" { + Expect(os.RemoveAll(tempDir)).To(Succeed()) + } + }) + + When("a file was added", func() { + BeforeEach(func() { + writeFile(tempDir, "new.txt", "abc") + addAndCommit(tempDir) + }) + + It("returns the expected result", func() { + Expect(git.Diff(tempDir, baseSHA)).To(ConsistOf([]git.FilePatch{{ + ToPath: "new.txt", + Chunks: []git.Chunk{{ + From: git.ChunkRange{LineNumber: 0, LineCount: 0}, + To: git.ChunkRange{LineNumber: 1, LineCount: 1}, + }}, + }})) + }) + }) + + When("a file was removed", func() { + BeforeEach(func() { + Expect(os.Remove(path.Join(tempDir, filename))).To(Succeed()) + addAndCommit(tempDir) + }) + + It("returns the expected result", func() { + Expect(git.Diff(tempDir, baseSHA)).To(ConsistOf([]git.FilePatch{{ + FromPath: filename, + Chunks: []git.Chunk{{ + From: git.ChunkRange{LineNumber: 1, LineCount: 3}, + To: git.ChunkRange{LineNumber: 0, LineCount: 0}, + }}, + }})) + }) + }) + + When("a file was renamed", func() { + toPath := "to.txt" + + BeforeEach(func() { + Expect(os.Rename(path.Join(tempDir, filename), path.Join(tempDir, toPath))).To(Succeed()) + addAndCommit(tempDir) + }) + + It("returns the expected result", func() { + Expect(git.Diff(tempDir, baseSHA)).To(ConsistOf([]git.FilePatch{ + {FromPath: filename, ToPath: toPath}, + })) + }) + }) + + When("paths contain characters requiring quoting", func() { + fromPath := "from\t.txt" + toPath := "to\t.txt" + + BeforeEach(func() { + writeFile(tempDir, fromPath, "1\n2") + addAndCommit(tempDir) + + var err error + baseSHA, err = git.GetCurrentCommit(tempDir) + Expect(err).To(BeNil()) + + Expect(os.Rename(path.Join(tempDir, fromPath), path.Join(tempDir, toPath))).To(Succeed()) + addAndCommit(tempDir) + }) + + It("decodes the paths correctly", func() { + Expect(git.Diff(tempDir, baseSHA)).To(ConsistOf([]git.FilePatch{ + {FromPath: fromPath, ToPath: toPath}, + })) + }) + }) + + When("a file contains changes", func() { + BeforeEach(func() { + writeFile(tempDir, filename, "x\ny\n2\nd") + addAndCommit(tempDir) + }) + + It("returns the expected result", func() { + Expect(git.Diff(tempDir, baseSHA)).To(ConsistOf([]git.FilePatch{{ + FromPath: filename, + ToPath: filename, + Chunks: []git.Chunk{ + { + From: git.ChunkRange{LineNumber: 1, LineCount: 1}, + To: git.ChunkRange{LineNumber: 1, LineCount: 2}, + }, + { + From: git.ChunkRange{LineNumber: 3, LineCount: 1}, + To: git.ChunkRange{LineNumber: 4, LineCount: 1}, + }, + }, + }})) + }) + }) + + When("a file contains a single line change (line count omitted from diff output)", func() { + BeforeEach(func() { + writeFile(tempDir, filename, "x\n2\n3") + addAndCommit(tempDir) + runGit(tempDir, "diff", "--unified=0", baseSHA) + }) + + It("returns the correct line counts", func() { + Expect(git.Diff(tempDir, baseSHA)).To(ConsistOf([]git.FilePatch{{ + FromPath: filename, + ToPath: filename, + Chunks: []git.Chunk{{ + From: git.ChunkRange{LineNumber: 1, LineCount: 1}, + To: git.ChunkRange{LineNumber: 1, LineCount: 1}, + }}, + }})) + }) + }) +}) + +var _ = Describe("ChunkRange", func() { + Describe("StartLineNumber", func() { + It("returns the line number", func() { + Expect(git.ChunkRange{LineNumber: 2, LineCount: 1}.StartLineNumber()).To(Equal(2)) + }) + + When("there are no lines in the range", func() { + It("returns the next line after the line number", func() { + Expect(git.ChunkRange{LineNumber: 2, LineCount: 0}.StartLineNumber()).To(Equal(3)) + }) + }) + }) + + Describe("EndLineNumber", func() { + It("returns the (inclusive) end line number", func() { + Expect(git.ChunkRange{LineNumber: 2, LineCount: 1}.EndLineNumber()).To(Equal(2)) + Expect(git.ChunkRange{LineNumber: 3, LineCount: 2}.EndLineNumber()).To(Equal(4)) + }) + + When("there are no lines in the range", func() { + It("returns the start line number", func() { + Expect(git.ChunkRange{LineNumber: 2, LineCount: 0}.EndLineNumber()).To(Equal(2)) + }) + }) + }) + + Describe("Overlap", func() { + When("the ranges are equal", func() { + a := git.ChunkRange{LineNumber: 1, LineCount: 2} + b := git.ChunkRange{LineNumber: 1, LineCount: 2} + + It("is true", func() { + Expect(a.Overlap(b)).To(BeTrue()) + }) + }) + + When("B overlaps A's start", func() { + a := git.ChunkRange{LineNumber: 2, LineCount: 2} + b := git.ChunkRange{LineNumber: 1, LineCount: 2} + + It("is true", func() { + Expect(a.Overlap(b)).To(BeTrue()) + }) + }) + + When("B overlaps A's end", func() { + a := git.ChunkRange{LineNumber: 1, LineCount: 2} + b := git.ChunkRange{LineNumber: 2, LineCount: 2} + + It("is true", func() { + Expect(a.Overlap(b)).To(BeTrue()) + }) + }) + + When("B is before A", func() { + a := git.ChunkRange{LineNumber: 2, LineCount: 2} + b := git.ChunkRange{LineNumber: 1, LineCount: 1} + + It("is false", func() { + Expect(a.Overlap(b)).To(BeFalse()) + }) + }) + + When("B is after A", func() { + a := git.ChunkRange{LineNumber: 1, LineCount: 2} + b := git.ChunkRange{LineNumber: 3, LineCount: 2} + + It("is false", func() { + Expect(a.Overlap(b)).To(BeFalse()) + }) + }) + }) +}) + +var _ = Describe("Chunks", func() { + Describe("TranslateRange", func() { + When("the base range is preceded by an add chunk", func() { + chunks := git.Chunks{{ + From: git.ChunkRange{LineNumber: 0, LineCount: 0}, + To: git.ChunkRange{LineNumber: 1, LineCount: 1}, + }} + + It("returns a range shifted by the add", func() { + Expect(chunks.TranslateRange(git.ChunkRange{LineNumber: 1, LineCount: 2})).To( + Equal(git.ChunkRange{LineNumber: 2, LineCount: 2}), + ) + }) + }) + + When("the base range is at an add chunk", func() { + chunks := git.Chunks{{ + From: git.ChunkRange{LineNumber: 1, LineCount: 0}, + To: git.ChunkRange{LineNumber: 2, LineCount: 1}, + }} + + It("returns a range shifted by the add", func() { + Expect(chunks.TranslateRange(git.ChunkRange{LineNumber: 2, LineCount: 2})).To( + Equal(git.ChunkRange{LineNumber: 3, LineCount: 2}), + ) + }) + }) + + When("the base range surrounds a remove chunk", func() { + chunks := git.Chunks{{ + From: git.ChunkRange{LineNumber: 3, LineCount: 1}, + To: git.ChunkRange{LineNumber: 2, LineCount: 0}, + }} + + It("returns a range that still overlaps the unchanged portion by the same amount", func() { + Expect(chunks.TranslateRange(git.ChunkRange{LineNumber: 2, LineCount: 3})).To( + Equal(git.ChunkRange{LineNumber: 2, LineCount: 2}), + ) + }) + }) + + When("the base range overlaps the start of a remove chunk", func() { + chunks := git.Chunks{{ + From: git.ChunkRange{LineNumber: 3, LineCount: 2}, + To: git.ChunkRange{LineNumber: 2, LineCount: 0}, + }} + + It("returns a range that ends at the removed chunk", func() { + Expect(chunks.TranslateRange(git.ChunkRange{LineNumber: 2, LineCount: 2})).To( + Equal(git.ChunkRange{LineNumber: 2, LineCount: 1}), + ) + }) + }) + + When("the base range is inside a remove chunk", func() { + chunks := git.Chunks{{ + From: git.ChunkRange{LineNumber: 1, LineCount: 2}, + To: git.ChunkRange{LineNumber: 0, LineCount: 0}, + }} + + It("returns an invalid range (will be ignored)", func() { + Expect(chunks.TranslateRange(git.ChunkRange{LineNumber: 1, LineCount: 1})).To( + Equal(git.ChunkRange{LineNumber: 0, LineCount: 0}), + ) + }) + }) + + When("the base range overlaps the start of an edit chunk", func() { + chunks := git.Chunks{{ + From: git.ChunkRange{LineNumber: 2, LineCount: 1}, + To: git.ChunkRange{LineNumber: 2, LineCount: 2}, + }} + + It("expands the range to the end of the chunk", func() { + Expect(chunks.TranslateRange(git.ChunkRange{LineNumber: 1, LineCount: 2})).To( + Equal(git.ChunkRange{LineNumber: 1, LineCount: 3}), + ) + }) + }) + + When("the base range overlaps the end of an edit chunk", func() { + chunks := git.Chunks{{ + From: git.ChunkRange{LineNumber: 1, LineCount: 2}, + To: git.ChunkRange{LineNumber: 1, LineCount: 3}, + }} + + It("expands the range to the start of the chunk", func() { + Expect(chunks.TranslateRange(git.ChunkRange{LineNumber: 2, LineCount: 2})).To( + Equal(git.ChunkRange{LineNumber: 1, LineCount: 4}), + ) + }) + }) + }) +}) diff --git a/internal/git/fetch.go b/internal/git/fetch.go index 989d4bdda..6ecd12e7a 100644 --- a/internal/git/fetch.go +++ b/internal/git/fetch.go @@ -1,14 +1,16 @@ package git -func FetchBranchLatest(rootDir string, branchName string) error { +import "context" + +func FetchRef(ctx context.Context, rootDir string, ref string) error { return basicCommand( + ctx, rootDir, - "git", "fetch", "--no-tags", "--no-recurse-submodules", "--depth=1", "origin", - branchName, + ref, ) } diff --git a/internal/git/git.go b/internal/git/git.go index ca447325b..6cd424cb3 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -2,20 +2,19 @@ package git import ( "bytes" + "context" "fmt" - "net/url" + "io" "os" "os/exec" + "path" "regexp" "strconv" "strings" - "time" "github.com/rs/zerolog/log" -) -const ( - mailmapFilename = ".mailmap" + "github.com/bearer/bearer/internal/util/file" ) var ( @@ -25,37 +24,42 @@ var ( "GCM_INTERACTIVE=never", "GIT_LFS_SKIP_SMUDGE=1", ) +) - specialFiles []string = []string{ - mailmapFilename, - ".gitignore", - ".gitattributes", - ".gitmodules", +func GetRoot(targetPath string) (string, error) { + dir := targetPath + if !file.IsDir(dir) { + dir = path.Dir(dir) } -) -type CommitIdentifier struct { - SHA string `json:"sha" yaml:"sha"` - Timestamp time.Time `json:"timestamp" yaml:"timestamp"` -} + output, err := captureCommandBasic(context.TODO(), dir, "rev-parse", "--show-toplevel") + if err != nil { + if strings.Contains(err.Error(), "not a git repository") { + return "", nil + } + + return "", err + } -func urlWithCredentials(originalURL *url.URL, token string) *url.URL { - result := *originalURL - result.User = url.UserPassword("x", token) - return &result + path := strings.TrimSpace(output) + if path == "" { + return "", nil + } + + return file.CanonicalPath(path) } -func logAndBuildCommand(args ...string) *exec.Cmd { +func logAndBuildCommand(ctx context.Context, args ...string) *exec.Cmd { log.Debug().Msgf("running command: git %s", strings.Join(args, " ")) - cmd := exec.Command("git", args...) + cmd := exec.CommandContext(ctx, "git", args...) cmd.Env = environment return cmd } -func basicCommand(workingDir string, args ...string) error { - cmd := logAndBuildCommand(args...) +func basicCommand(ctx context.Context, workingDir string, args ...string) error { + cmd := logAndBuildCommand(ctx, args...) if workingDir != "" { cmd.Dir = workingDir } @@ -65,20 +69,55 @@ func basicCommand(workingDir string, args ...string) error { cmd.Stderr = logWriter if err := cmd.Run(); err != nil { - killProcess(cmd) + cmd.Cancel() //nolint:errcheck return newError(err, logWriter.AllOutput()) } return nil } -var regexpDefunctProcess = regexp.MustCompile(" git ") -var regexpPID = regexp.MustCompile("[0-9]+ ") +func captureCommand(ctx context.Context, workingDir string, args []string, capture func(io.Reader) error) error { + command := logAndBuildCommand(ctx, args...) + if workingDir != "" { + command.Dir = workingDir + } + + logWriter := &debugLogWriter{} + command.Stderr = logWriter + + stdout, err := command.StdoutPipe() + if err != nil { + return err + } + + if err := command.Start(); err != nil { + command.Cancel() //nolint:errcheck + return err + } -func killProcess(cmd *exec.Cmd) { - if cmd != nil && cmd.Process != nil { - cmd.Process.Kill() //nolint:all,errcheck + if err := capture(stdout); err != nil { + command.Cancel() //nolint:errcheck + return err } + + stdout.Close() + + if err := command.Wait(); err != nil { + command.Cancel() //nolint:errcheck + return newError(err, logWriter.AllOutput()) + } + + return nil +} + +func captureCommandBasic(ctx context.Context, workingDir string, args ...string) (output string, err error) { + err = captureCommand(ctx, workingDir, args, func(r io.Reader) error { + outputBytes, readErr := io.ReadAll(r) + output = string(outputBytes) + return readErr + }) + + return } func unquoteFilename(quoted string) (string, error) { diff --git a/internal/git/git_suite_test.go b/internal/git/git_suite_test.go new file mode 100644 index 000000000..d92d0ca73 --- /dev/null +++ b/internal/git/git_suite_test.go @@ -0,0 +1,44 @@ +package git_test + +import ( + "fmt" + "os" + "os/exec" + "path" + "strings" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestGit(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Git Suite") +} + +func runGit(dir string, args ...string) { + command := exec.Command("git", args...) + command.Dir = dir + + output, err := command.CombinedOutput() + if err != nil { + Fail(fmt.Sprintf("failed to run git command [%s]: %s\n%s", strings.Join(args, " "), err, output)) + } +} + +func addAndCommit(dir string) { + runGit(dir, "add", ".") + runGit( + dir, + "-c", "user.name=Bearer CI", + "-c", "user.email=ci@bearer.com", + "commit", + "--allow-empty-message", + "--message=", + ) +} + +func writeFile(tempDir, filename, content string) { + Expect(os.WriteFile(path.Join(tempDir, filename), []byte(content), 0600)).To(Succeed()) +} diff --git a/internal/git/git_test.go b/internal/git/git_test.go new file mode 100644 index 000000000..4241deeda --- /dev/null +++ b/internal/git/git_test.go @@ -0,0 +1,65 @@ +package git_test + +import ( + "os" + "path" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/bearer/bearer/internal/git" + "github.com/bearer/bearer/internal/util/file" +) + +var _ = Describe("GetRoot", func() { + var tempDir string + filename := "foo.txt" + dirname := "stuff" + + BeforeEach(func() { + var err error + tempDir, err = os.MkdirTemp("", "diff-test") + Expect(err).To(BeNil()) + tempDir, err = file.CanonicalPath(tempDir) + Expect(err).To(BeNil()) + + writeFile(tempDir, filename, "42") + Expect(os.Mkdir(path.Join(tempDir, dirname), 0700)).To(Succeed()) + }) + + AfterEach(func() { + if tempDir != "" { + Expect(os.RemoveAll(tempDir)).To(Succeed()) + } + }) + + When("the target path is in a git repository", func() { + BeforeEach(func() { + runGit(tempDir, "init", ".") + }) + + When("the target path is the repository root", func() { + It("returns the root", func() { + Expect(git.GetRoot(tempDir)).To(Equal(tempDir)) + }) + }) + + When("the target path is a file", func() { + It("returns the root", func() { + Expect(git.GetRoot(path.Join(tempDir, filename))).To(Equal(tempDir)) + }) + }) + + When("the target path is in a subfolder", func() { + It("returns the root", func() { + Expect(git.GetRoot(path.Join(tempDir, dirname))).To(Equal(tempDir)) + }) + }) + }) + + When("the target path is NOT in a git repository", func() { + It("returns an empty string", func() { + Expect(git.GetRoot(tempDir)).To(BeEmpty()) + }) + }) +}) diff --git a/internal/git/remote.go b/internal/git/remote.go new file mode 100644 index 000000000..f88f5f3b7 --- /dev/null +++ b/internal/git/remote.go @@ -0,0 +1,22 @@ +package git + +import ( + "context" + "strings" +) + +func GetOriginURL(dir string) (string, error) { + output, err := captureCommandBasic( + context.TODO(), + dir, + "remote", + "get-url", + "origin", + ) + + if err != nil && strings.Contains(err.Error(), "No such remote 'origin'") { + return "", nil + } + + return strings.TrimSpace(output), err +} diff --git a/internal/git/renames.go b/internal/git/renames.go deleted file mode 100644 index 73c1955d6..000000000 --- a/internal/git/renames.go +++ /dev/null @@ -1,87 +0,0 @@ -package git - -import ( - "bufio" - "fmt" - "strings" -) - -type RenamedFile struct { - PreviousFilename string `json:"previous_filename" yaml:"previous_filename"` - NewFilename string `json:"new_filename" yaml:"new_filename"` -} - -func GetRenames(rootDir, firstCommitSHA, lastCommitSHA string) ([]RenamedFile, error) { - cmd := logAndBuildCommand( - "log", - "--first-parent", - "--find-renames", - "--break-rewrites", - "--name-status", - "--diff-filter=R", - "--pretty=tformat:", - firstCommitSHA+".."+lastCommitSHA, - "--", - ) - cmd.Dir = rootDir - - logWriter := &debugLogWriter{} - cmd.Stderr = logWriter - - stdout, err := cmd.StdoutPipe() - if err != nil { - killProcess(cmd) - return nil, err - } - - if err := cmd.Start(); err != nil { - killProcess(cmd) - return nil, err - } - - scanner := bufio.NewScanner(stdout) - - renameMap := make(map[string]string) - - for scanner.Scan() { - line := scanner.Text() - splitLine := strings.Split(line, "\t") - - prevFilename, err := unquoteFilename(splitLine[1]) - if err != nil { - killProcess(cmd) - return nil, fmt.Errorf("failed to unquote previous filename: %s", err) - } - newFilename, err := unquoteFilename(splitLine[2]) - if err != nil { - killProcess(cmd) - return nil, fmt.Errorf("failed to unquote new filename: %s", err) - } - - if latestFilename, alreadyRenamed := renameMap[newFilename]; alreadyRenamed { - delete(renameMap, newFilename) - newFilename = latestFilename - } - - renameMap[prevFilename] = newFilename - } - - if err := scanner.Err(); err != nil { - killProcess(cmd) - return nil, err - } - - stdout.Close() - - if err := cmd.Wait(); err != nil { - killProcess(cmd) - return nil, newError(err, logWriter.AllOutput()) - } - - result := []RenamedFile{} - for prevFilename, newFilename := range renameMap { - result = append(result, RenamedFile{PreviousFilename: prevFilename, NewFilename: newFilename}) - } - - return result, nil -} diff --git a/internal/git/tree.go b/internal/git/tree.go index e5c2971b3..4badf91e6 100644 --- a/internal/git/tree.go +++ b/internal/git/tree.go @@ -2,232 +2,65 @@ package git import ( "bufio" - "fmt" + "context" "io" - "path" - "slices" "strings" - "time" ) -const blankID = "0000000000000000000000000000000000000000" - -type Tree struct { - Commit CommitIdentifier `json:"commit" yaml:"commit"` - Files []TreeFile `json:"files" yaml:"files"` -} - type TreeFile struct { Filename string `json:"filename" yaml:"filename"` SHA string `json:"sha" yaml:"sha"` } -func GetTree(rootDir string) (*Tree, error) { - commit, err := getHeadCommitIdentifier(rootDir) - if err != nil { - return nil, fmt.Errorf("failed to get commit identifier: %s", err) - } - - files, err := listTree(rootDir, commit.SHA) - if err != nil { - return nil, fmt.Errorf("failed to list tree: %s", err) - } - - return &Tree{Commit: *commit, Files: files}, nil -} - -func listTree(rootDir, commitSHA string) ([]TreeFile, error) { - result := []TreeFile{} - - cmd := logAndBuildCommand("ls-tree", "-r", "-z", commitSHA) - cmd.Dir = rootDir - - logWriter := &debugLogWriter{} - cmd.Stderr = logWriter - - stdout, err := cmd.StdoutPipe() - if err != nil { - killProcess(cmd) - return nil, err - } - - if err := cmd.Start(); err != nil { - killProcess(cmd) - return nil, err - } - - stdoutBuf := bufio.NewReader(stdout) - for { - metadata, err := stdoutBuf.ReadString('\t') - if err == io.EOF { - break - } - if err != nil { - killProcess(cmd) - return nil, err - } - - splitMeta := strings.Split(metadata[:len(metadata)-1], " ") - if len(splitMeta) != 3 { - continue - } - sha := splitMeta[2] - - filename, err := stdoutBuf.ReadString(0) - if err != nil && err != io.EOF { - killProcess(cmd) - return nil, err - } - - if len(filename) > 1 { - result = append(result, TreeFile{Filename: filename[:len(filename)-1], SHA: sha}) - } - } - - stdout.Close() - - if err := cmd.Wait(); err != nil { - killProcess(cmd) - return nil, newError(err, logWriter.AllOutput()) - } - - return result, nil -} - -func getObjectIDsForRangeFiles(rootDir, firstCommitSHA, lastCommitSHA string, filenames []string) ([]string, error) { - firstCommitFileObjectIDs, err := getObjectIDsForFiles(rootDir, firstCommitSHA, filenames) - if err != nil { - return nil, err - } - - rangeUsedObjectIDs, err := getObjectIDsUsedByRange(rootDir, firstCommitSHA, lastCommitSHA) - if err != nil { - return nil, err - } - - ids := append(firstCommitFileObjectIDs, rangeUsedObjectIDs...) - slices.Sort(ids) - return slices.Compact(ids), nil -} - -// Returns all the object ids of files touched by the given range of commits. -func getObjectIDsUsedByRange(rootDir, firstCommitSHA, lastCommitSHA string) ([]string, error) { - result := []string{} - - cmd := logAndBuildCommand( - "log", +func HasUncommittedChanges(rootDir string) (bool, error) { + output, err := captureCommandBasic( + context.TODO(), + rootDir, + "status", + "--porcelain=v1", "--no-renames", - "--first-parent", - "--raw", - "--no-abbrev", - "--format=%H", - firstCommitSHA+".."+lastCommitSHA, ) - cmd.Dir = rootDir - - logWriter := &debugLogWriter{} - cmd.Stderr = logWriter - - stdout, err := cmd.StdoutPipe() - if err != nil { - killProcess(cmd) - return nil, err - } - - if err := cmd.Start(); err != nil { - killProcess(cmd) - return nil, err - } - - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - line := scanner.Text() - if line == "" || line[0] != ':' { - continue - } - - splitLine := strings.Split(line, " ") - if splitLine[2] != blankID { - result = append(result, splitLine[2]) - } - if splitLine[3] != blankID { - result = append(result, splitLine[3]) - } - } - - if err := scanner.Err(); err != nil { - killProcess(cmd) - return nil, err - } - - stdout.Close() - - if err := cmd.Wait(); err != nil { - killProcess(cmd) - return nil, newError(err, logWriter.AllOutput()) - } - - return result, nil + return strings.TrimSpace(output) != "", err } -// Get's the object ids of the given filenames in the specified commit. -// Also includes special git metadata files -func getObjectIDsForFiles(rootDir, commitSHA string, filenames []string) ([]string, error) { - wantedFilenames := make(map[string]struct{}) - for _, filename := range filenames { - wantedFilenames[filename] = struct{}{} - } - - treeFiles, err := listTree(rootDir, commitSHA) - if err != nil { - return nil, err - } - - objectIDs := []string{} - - for _, treeFile := range treeFiles { - if _, ok := wantedFilenames[treeFile.Filename]; ok { - objectIDs = append(objectIDs, treeFile.SHA) - continue - } - - if slices.Contains(specialFiles, path.Base(treeFile.Filename)) { - objectIDs = append(objectIDs, treeFile.SHA) - } - } - - return objectIDs, nil -} - -func getHeadCommitIdentifier(rootDir string) (*CommitIdentifier, error) { - cmd := logAndBuildCommand("log", "-1", "--format=%H %cI") - cmd.Dir = rootDir - - logWriter := &debugLogWriter{} - cmd.Stderr = logWriter - - output, err := cmd.Output() - if err != nil { - killProcess(cmd) - return nil, newError(err, logWriter.AllOutput()) - } - - splitOutput := strings.SplitN(strings.TrimSpace(string(output)), " ", 2) - - parsedTimestamp, err := time.Parse(time.RFC3339, splitOutput[1]) - if err != nil { - killProcess(cmd) - return nil, err - } - - return &CommitIdentifier{SHA: splitOutput[0], Timestamp: parsedTimestamp.UTC()}, nil -} +func ListTree(rootDir, commitSHA string) ([]TreeFile, error) { + result := []TreeFile{} -func commitPresent(rootDir, sha string) bool { - cmd := logAndBuildCommand("cat-file", "-t", sha) - cmd.Dir = rootDir + err := captureCommand( + context.TODO(), + rootDir, + []string{"ls-tree", "-r", "-z", commitSHA}, + func(stdout io.Reader) error { + stdoutBuf := bufio.NewReader(stdout) + for { + metadata, err := stdoutBuf.ReadString('\t') + if err == io.EOF { + break + } + if err != nil { + return err + } + + splitMeta := strings.Split(metadata[:len(metadata)-1], " ") + if len(splitMeta) != 3 { + continue + } + sha := splitMeta[2] + + filename, err := stdoutBuf.ReadString(0) + if err != nil && err != io.EOF { + return err + } + + if len(filename) > 1 { + result = append(result, TreeFile{Filename: filename[:len(filename)-1], SHA: sha}) + } + } + + return nil + }, + ) - output, _ := cmd.Output() - return string(output) == "commit\n" + return result, err } diff --git a/internal/languages/testhelper/testhelper.go b/internal/languages/testhelper/testhelper.go index 8c3d9043c..1a201b79f 100644 --- a/internal/languages/testhelper/testhelper.go +++ b/internal/languages/testhelper/testhelper.go @@ -160,6 +160,7 @@ func (runner *Runner) scanSingleFile(t *testing.T, testDataPath string, fileRela }, runner.config, nil, + nil, ) if err != nil { t.Fatalf("failed to get output: %s", err) diff --git a/internal/report/basebranchfindings/basebranchfindings.go b/internal/report/basebranchfindings/basebranchfindings.go index 635a4a04c..56fa7ed9a 100644 --- a/internal/report/basebranchfindings/basebranchfindings.go +++ b/internal/report/basebranchfindings/basebranchfindings.go @@ -4,7 +4,7 @@ import ( "slices" "github.com/bearer/bearer/internal/commands/process/filelist/files" - "github.com/bearer/bearer/internal/report/basebranchfindings/types" + "github.com/bearer/bearer/internal/git" ) type key struct { @@ -14,15 +14,15 @@ type key struct { type Findings struct { fileList *files.List - chunks map[string][]chunk - items map[key][]types.LineRange + chunks map[string]git.Chunks + items map[key][]git.ChunkRange } func New(fileList *files.List) *Findings { return &Findings{ fileList: fileList, - chunks: make(map[string][]chunk), - items: make(map[key][]types.LineRange), + chunks: make(map[string]git.Chunks), + items: make(map[key][]git.ChunkRange), } } @@ -38,7 +38,10 @@ func (findings Findings) Add(ruleID string, baseFilename string, baseStartLine, Filename: filename, } - findings.items[key] = append(findings.items[key], fileChunks.TranslateRange(baseStartLine, baseEndLine)) + findings.items[key] = append( + findings.items[key], + fileChunks.TranslateRange(newRange(baseStartLine, baseEndLine)), + ) } func (findings Findings) Consume(ruleID string, filename string, startLine, endLine int) bool { @@ -47,7 +50,7 @@ func (findings Findings) Consume(ruleID string, filename string, startLine, endL Filename: filename, } - lineRange := types.LineRange{Start: startLine, End: endLine} + lineRange := newRange(startLine, endLine) for i, findingLineRange := range findings.items[key] { if findingLineRange.Overlap(lineRange) { @@ -58,3 +61,7 @@ func (findings Findings) Consume(ruleID string, filename string, startLine, endL return false } + +func newRange(startLine, endLine int) git.ChunkRange { + return git.ChunkRange{LineNumber: startLine, LineCount: endLine - startLine + 1} +} diff --git a/internal/report/basebranchfindings/basebranchfindings_test.go b/internal/report/basebranchfindings/basebranchfindings_test.go deleted file mode 100644 index 06cba38d8..000000000 --- a/internal/report/basebranchfindings/basebranchfindings_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package basebranchfindings_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/bearer/bearer/internal/report/basebranchfindings" - "github.com/bearer/bearer/internal/report/basebranchfindings/types" -) - -func TestLineRangeOverlap(t *testing.T) { - tests := []struct { - Name string - A, - B types.LineRange - Expected bool - }{ - { - "equal", - types.LineRange{Start: 1, End: 2}, - types.LineRange{Start: 1, End: 2}, - true, - }, - { - "B overlap A start", - types.LineRange{Start: 2, End: 3}, - types.LineRange{Start: 1, End: 2}, - true, - }, - { - "B overlap A end", - types.LineRange{Start: 1, End: 2}, - types.LineRange{Start: 2, End: 3}, - true, - }, - { - "B before A", - types.LineRange{Start: 2, End: 3}, - types.LineRange{Start: 1, End: 1}, - false, - }, - { - "B after A", - types.LineRange{Start: 1, End: 2}, - types.LineRange{Start: 3, End: 4}, - false, - }, - } - - for _, testCase := range tests { - t.Run(testCase.Name, func(t *testing.T) { - assert.Equal(t, testCase.Expected, testCase.A.Overlap(testCase.B)) - }) - } -} - -func TestChunksTranslateRange(t *testing.T) { - chunks := basebranchfindings.NewChunks() - chunks.Add(types.ChunkAdd, 1) - chunks.Add(types.ChunkEqual, 2) - chunks.Add(types.ChunkRemove, 2) - chunks.Add(types.ChunkEqual, 2) - chunks.Add(types.ChunkRemove, 1) - chunks.Add(types.ChunkAdd, 2) - chunks.Add(types.ChunkRemove, 1) - - tests := []struct { - Name string - BaseLineRange, - Expected types.LineRange - }{ - { - "equal after add", - types.LineRange{Start: 1, End: 2}, - types.LineRange{Start: 2, End: 3}, - }, - { - "remove inbetween equals", - types.LineRange{Start: 2, End: 5}, - types.LineRange{Start: 3, End: 4}, - }, - { - "equal overlapping remove", - types.LineRange{Start: 2, End: 3}, - types.LineRange{Start: 3, End: 3}, - }, - { - "in remove", - types.LineRange{Start: 3, End: 3}, - types.LineRange{Start: 4, End: 3}, - }, - { - "in remove followed by add", - types.LineRange{Start: 7, End: 7}, - types.LineRange{Start: 6, End: 7}, - }, - { - "add between removes", - types.LineRange{Start: 7, End: 8}, - types.LineRange{Start: 6, End: 7}, - }, - } - - for _, testCase := range tests { - t.Run(testCase.Name, func(t *testing.T) { - actual := chunks.TranslateRange(testCase.BaseLineRange.Start, testCase.BaseLineRange.End) - assert.Equal(t, testCase.Expected, actual) - }) - } -} diff --git a/internal/report/basebranchfindings/chunks.go b/internal/report/basebranchfindings/chunks.go deleted file mode 100644 index 4514624d5..000000000 --- a/internal/report/basebranchfindings/chunks.go +++ /dev/null @@ -1,98 +0,0 @@ -package basebranchfindings - -import "github.com/bearer/bearer/internal/report/basebranchfindings/types" - -type chunk struct { - ChangeType types.ChangeType - LineCount int - HeadLine, - BaseLine int -} - -type Chunks struct { - items []chunk -} - -func NewChunks() types.Chunks { - return &Chunks{} -} - -func (chunks *Chunks) Add(changeType types.ChangeType, lineCount int) { - baseLine := 1 - headLine := 1 - - if len(chunks.items) != 0 { - previousChunk := chunks.items[len(chunks.items)-1] - baseLine, headLine = previousChunk.nextLine() - } - - chunks.items = append(chunks.items, chunk{ - ChangeType: changeType, - LineCount: lineCount, - BaseLine: baseLine, - HeadLine: headLine, - }) -} - -func (chunks *Chunks) TranslateRange(baseStartLine, baseEndLine int) types.LineRange { - startLine := 0 - endLine := 0 - - for i, chunk := range chunks.items { - if chunk.ChangeType == types.ChunkAdd { - continue - } - - chunkBaseEndLine, chunkHeadEndLine := chunk.nextLine() - - if baseStartLine >= chunk.BaseLine && baseStartLine < chunkBaseEndLine { - if chunk.ChangeType == types.ChunkEqual { - startLine = baseStartLine - chunk.BaseLine + chunk.HeadLine - } else { - // for a removal, use the start of the next chunk - startLine = chunkHeadEndLine - } - } - - if baseEndLine >= chunk.BaseLine && baseEndLine < chunkBaseEndLine { - if chunk.ChangeType == types.ChunkEqual { - endLine = baseEndLine - chunk.BaseLine + chunk.HeadLine - } else { - // for a removal, use the end of the previous chunk - endLine = chunk.HeadLine - 1 - - // but if there's an addition next then incorporate that, as it could - // be an edit - if i != len(chunks.items)-1 { - nextChunk := chunks.items[i+1] - if nextChunk.ChangeType == types.ChunkAdd { - endLine = nextChunk.HeadLine + nextChunk.LineCount - 1 - } - } - } - } - - if startLine != 0 && endLine != 0 { - break - } - } - - return types.LineRange{ - Start: startLine, - End: endLine, - } -} - -func (chunk *chunk) nextLine() (int, int) { - baseLine := chunk.BaseLine - if chunk.ChangeType != types.ChunkAdd { - baseLine += chunk.LineCount - } - - headLine := chunk.HeadLine - if chunk.ChangeType != types.ChunkRemove { - headLine += chunk.LineCount - } - - return baseLine, headLine -} diff --git a/internal/report/basebranchfindings/types/types.go b/internal/report/basebranchfindings/types/types.go deleted file mode 100644 index 84b997003..000000000 --- a/internal/report/basebranchfindings/types/types.go +++ /dev/null @@ -1,23 +0,0 @@ -package types - -type ChangeType int - -const ( - ChunkAdd ChangeType = iota - ChunkRemove - ChunkEqual -) - -type LineRange struct { - Start, - End int -} - -type Chunks interface { - Add(changeType ChangeType, lineCount int) - TranslateRange(baseStartLine, baseEndLine int) LineRange -} - -func (lineRange *LineRange) Overlap(other LineRange) bool { - return lineRange.Start <= other.End && lineRange.End >= other.Start -} diff --git a/internal/report/output/output.go b/internal/report/output/output.go index 8728da1e9..bf9da13ca 100644 --- a/internal/report/output/output.go +++ b/internal/report/output/output.go @@ -9,6 +9,7 @@ import ( "github.com/hhatto/gocloc" "golang.org/x/exp/slices" + "github.com/bearer/bearer/internal/commands/process/gitrepository" "github.com/bearer/bearer/internal/commands/process/settings" "github.com/bearer/bearer/internal/flag" "github.com/bearer/bearer/internal/report/basebranchfindings" @@ -27,6 +28,7 @@ var ErrUndefinedFormat = errors.New("undefined output format") func GetData( report globaltypes.Report, config settings.Config, + gitContext *gitrepository.Context, baseBranchFindings *basebranchfindings.Findings, ) (*types.ReportData, error) { data := &types.ReportData{} @@ -61,7 +63,7 @@ func GetData( if err = security.AddReportData(data, config, baseBranchFindings, report.HasFiles); err != nil { return nil, err } - err = saas.GetReport(data, config, false) + err = saas.GetReport(data, config, gitContext, false) case flag.ReportPrivacy: err = privacy.AddReportData(data, config) case flag.ReportStats: @@ -73,10 +75,10 @@ func GetData( return data, err } -func UploadReportToCloud(report *types.ReportData, config settings.Config) { +func UploadReportToCloud(report *types.ReportData, config settings.Config, gitContext *gitrepository.Context) { if slices.Contains([]string{flag.ReportSecurity, flag.ReportSaaS}, config.Report.Report) { if config.Client != nil && config.Client.Error == nil { - saas.SendReport(config, report) + saas.SendReport(config, report, gitContext) } } } diff --git a/internal/report/output/saas/saas.go b/internal/report/output/saas/saas.go index 29490a166..1f94777db 100644 --- a/internal/report/output/saas/saas.go +++ b/internal/report/output/saas/saas.go @@ -2,18 +2,18 @@ package saas import ( "compress/gzip" + "errors" "fmt" "os" - "os/exec" "strings" - "github.com/gitsight/go-vcsurl" "github.com/rs/zerolog/log" "golang.org/x/exp/maps" "github.com/bearer/bearer/api" "github.com/bearer/bearer/api/s3" "github.com/bearer/bearer/cmd/bearer/build" + "github.com/bearer/bearer/internal/commands/process/gitrepository" "github.com/bearer/bearer/internal/commands/process/settings" saas "github.com/bearer/bearer/internal/report/output/saas/types" securitytypes "github.com/bearer/bearer/internal/report/output/security/types" @@ -23,14 +23,14 @@ import ( pointer "github.com/bearer/bearer/internal/util/pointers" ) -var ShaEnvVarNames = [2]string{"SHA", "CI_COMMIT_SHA"} -var CurrentBranchEnvVarNames = [2]string{"CURRENT_BRANCH", "CI_COMMIT_REF_NAME"} -var DefaultBranchEnvVarNames = [2]string{"DEFAULT_BRANCH", "CI_DEFAULT_BRANCH"} -var OriginUrlEnvVarNames = [2]string{"ORIGIN_URL", "CI_REPOSITORY_URL"} - -func GetReport(reportData *types.ReportData, config settings.Config, ensureMeta bool) error { +func GetReport( + reportData *types.ReportData, + config settings.Config, + gitContext *gitrepository.Context, + ensureMeta bool, +) error { var meta *saas.Meta - meta, err := getMeta(reportData, config) + meta, err := getMeta(reportData, config, gitContext) if err != nil { if ensureMeta { return err @@ -58,24 +58,9 @@ func GetReport(reportData *types.ReportData, config settings.Config, ensureMeta return nil } -func GetVCSInfo(target string) (*vcsurl.VCS, error) { - gitRemote, err := getRemote(target) - if err != nil { - return nil, err - } - - info, err := vcsurl.Parse(*gitRemote) - if err != nil { - log.Debug().Msgf("couldn't parse origin url %s", err) - return nil, err - } - - return info, nil -} - -func SendReport(config settings.Config, reportData *types.ReportData) { +func SendReport(config settings.Config, reportData *types.ReportData, gitContext *gitrepository.Context) { if reportData.SaasReport == nil { - err := GetReport(reportData, config, true) + err := GetReport(reportData, config, gitContext, true) if err != nil { errorMessage := fmt.Sprintf("Unable to calculate Metadata. %s", err) log.Debug().Msgf(errorMessage) @@ -173,105 +158,59 @@ func createBearerGzipFileReport( return &tempDir, &filename, nil } -func getMeta(reportData *types.ReportData, config settings.Config) (*saas.Meta, error) { - sha, err := getSha(config.Scan.Target) - if err != nil { - return nil, err +func getMeta( + reportData *types.ReportData, + config settings.Config, + gitContext *gitrepository.Context, +) (*saas.Meta, error) { + if gitContext == nil { + return nil, errors.New("not a git repository") } - currentBranch, err := getCurrentBranch(config.Scan.Target) - if err != nil { - return nil, err + var messages []string + if gitContext.Branch == "" { + messages = append(messages, + "Couldn't determine the name of the branch being scanned. "+ + "Please set the 'BEARER_BRANCH' environment variable.", + ) } - - defaultBranch, err := getDefaultBranch(config.Scan.Target) - if err != nil { - return nil, err + if gitContext.DefaultBranch == "" { + messages = append(messages, + "Couldn't determine the default branch of the repository. "+ + "Please set the 'BEARER_DEFAULT_BRANCH' environment variable.", + ) + } + if gitContext.CommitHash == "" { + messages = append(messages, + "Couldn't determine the hash of the current commit of the repository. "+ + "Please set the 'BEARER_COMMIT' environment variable.", + ) + } + if gitContext.OriginURL == "" { + messages = append(messages, + "Couldn't determine the origin URL of the repository. "+ + "Please set the 'BEARER_REPOSITORY_URL' environment variable.", + ) } - info, err := GetVCSInfo(config.Scan.Target) - if err != nil { - return nil, err + if len(messages) != 0 { + return nil, errors.New(strings.Join(messages, "\n")) } return &saas.Meta{ - ID: info.ID, - Host: string(info.Host), - Username: info.Username, - Name: info.Name, - FullName: info.FullName, - URL: info.Raw, + ID: gitContext.ID, + Host: gitContext.Host, + Username: gitContext.Owner, + Name: gitContext.Name, + FullName: gitContext.FullName, + URL: gitContext.OriginURL, Target: config.Scan.Target, - SHA: *sha, - CurrentBranch: *currentBranch, - DefaultBranch: *defaultBranch, - DiffBaseBranch: config.Scan.DiffBaseBranch, + SHA: gitContext.CommitHash, + CurrentBranch: gitContext.Branch, + DefaultBranch: gitContext.DefaultBranch, + DiffBaseBranch: gitContext.BaseBranch, BearerRulesVersion: config.BearerRulesVersion, BearerVersion: build.Version, FoundLanguages: reportData.FoundLanguages, }, nil } - -func getSha(target string) (*string, error) { - for _, key := range ShaEnvVarNames { - env := os.Getenv(key) - if env != "" { - return pointer.String(env), nil - } - } - - bytes, err := exec.Command("git", "-C", target, "rev-parse", "HEAD").Output() - if err != nil { - log.Error().Msg("Couldn't extract git info for commit sha please set 'SHA' environment variable.") - return nil, err - } - return pointer.String(strings.TrimSuffix(string(bytes), "\n")), nil -} - -func getCurrentBranch(target string) (*string, error) { - for _, key := range CurrentBranchEnvVarNames { - env := os.Getenv(key) - if env != "" { - return pointer.String(env), nil - } - } - - bytes, err := exec.Command("git", "-C", target, "rev-parse", "--abbrev-ref", "HEAD").Output() - if err != nil { - log.Error().Msg("Couldn't extract git info for current branch please set 'CURRENT_BRANCH' environment variable.") - return nil, err - } - return pointer.String(strings.TrimSuffix(string(bytes), "\n")), nil -} - -func getDefaultBranch(target string) (*string, error) { - for _, key := range DefaultBranchEnvVarNames { - env := os.Getenv(key) - if env != "" { - return pointer.String(env), nil - } - } - - bytes, err := exec.Command("git", "-C", target, "rev-parse", "--abbrev-ref", "origin/HEAD").Output() - if err != nil { - log.Error().Msg("Couldn't extract the default branch of this repository. Please set 'DEFAULT_BRANCH' environment variable.") - return nil, err - } - return pointer.String(strings.TrimPrefix(strings.TrimSuffix(string(bytes), "\n"), "origin/")), nil -} - -func getRemote(target string) (*string, error) { - for _, key := range OriginUrlEnvVarNames { - env := os.Getenv(key) - if env != "" { - return pointer.String(env), nil - } - } - - bytes, err := exec.Command("git", "-C", target, "remote", "get-url", "origin").Output() - if err != nil { - log.Error().Msg("Couldn't extract git info for origin url please set 'ORIGIN_URL' environment variable.") - return nil, err - } - return pointer.String(strings.TrimSuffix(string(bytes), "\n")), nil -} diff --git a/internal/report/output/security/security.go b/internal/report/output/security/security.go index 3fb109968..587a10b51 100644 --- a/internal/report/output/security/security.go +++ b/internal/report/output/security/security.go @@ -130,7 +130,7 @@ func AddReportData( config.Report.ExcludeFingerprint, config.IgnoredFingerprints, config.StaleIgnoredFingerprintIds, - config.Scan.DiffBaseBranch != "", + config.Scan.Diff, ) } diff --git a/internal/report/output/security/security_test.go b/internal/report/output/security/security_test.go index 567ad20b5..0b52ba64f 100644 --- a/internal/report/output/security/security_test.go +++ b/internal/report/output/security/security_test.go @@ -10,8 +10,8 @@ import ( "github.com/bearer/bearer/internal/commands/process/filelist/files" "github.com/bearer/bearer/internal/commands/process/settings" "github.com/bearer/bearer/internal/flag" + "github.com/bearer/bearer/internal/git" "github.com/bearer/bearer/internal/report/basebranchfindings" - basebranchfindingstypes "github.com/bearer/bearer/internal/report/basebranchfindings/types" "github.com/bearer/bearer/internal/report/schema" globaltypes "github.com/bearer/bearer/internal/types" "github.com/bearer/bearer/internal/util/set" @@ -262,16 +262,15 @@ func TestFingerprintIsStableWithBaseBranchFindings(t *testing.T) { fullScanFinding := data.FindingsBySeverity[globaltypes.LevelMedium][1] - chunks := basebranchfindings.NewChunks() - chunks.Add(basebranchfindingstypes.ChunkEqual, 1) - chunks.Add(basebranchfindingstypes.ChunkAdd, 1) - file := files.File{FilePath: filename} fileList := &files.List{ Files: []files.File{file}, BaseFiles: []files.File{file}, - Chunks: map[string]basebranchfindingstypes.Chunks{ - filename: chunks, + Chunks: map[string]git.Chunks{ + filename: {{ + From: git.ChunkRange{LineNumber: 1, LineCount: 0}, + To: git.ChunkRange{LineNumber: 2, LineCount: 1}, + }}, }, } diff --git a/internal/util/file/file.go b/internal/util/file/file.go index 168a8da35..c559276d8 100644 --- a/internal/util/file/file.go +++ b/internal/util/file/file.go @@ -400,3 +400,21 @@ func GetFullFilename(path string, filename string) string { return path + "/" + filename } + +func IsDir(path string) bool { + fileInfo, err := os.Stat(path) + if err != nil { + return false + } + + return fileInfo.IsDir() +} + +func CanonicalPath(path string) (string, error) { + resolvedPath, err := filepath.EvalSymlinks(path) + if err != nil { + return "", err + } + + return filepath.Abs(resolvedPath) +}