diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ecd30e7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +name: release + +permissions: + contents: write + +on: + push: + tags: + - "*" + +jobs: + goreleaser: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Fetch git tags + run: git fetch --force --tags + + - name: Setup go + uses: actions/setup-go@v3 + with: + go-version: ">=1.19.2" + cache: true + + - uses: goreleaser/goreleaser-action@v2 + with: + distribution: goreleaser + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0e32b60 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,59 @@ +name: test + +on: + push: + branches: + - main + pull_request: + +jobs: + analyzer: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Generate org-level access token for test-org + id: org-token + uses: getsentry/action-github-app-token@v1 + with: + app_id: ${{ secrets.TEST_GITHUB_APP_ID }} + private_key: ${{ secrets.TEST_GITHUB_APP_PRIVATE_KEY }} + + - name: Scan test-org + env: + GH_SECURITY_AUDITOR_TOKEN: ${{ steps.org-token.outputs.token }} + run: | + docker-compose run --rm github-analyzer \ + --organization ${{ secrets.TEST_GITHUB_ORG }} \ + --userPermissionStats \ + --disableServer + + - name: "Upload Artifact" + uses: actions/upload-artifact@v3 + with: + name: output + path: output + retention-days: 7 + + asserts: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Generate org-level access token for test-org + id: org-token + uses: getsentry/action-github-app-token@v1 + with: + app_id: ${{ secrets.TEST_GITHUB_APP_ID }} + private_key: ${{ secrets.TEST_GITHUB_APP_PRIVATE_KEY }} + + - name: Run tests on output data + env: + GH_SECURITY_AUDITOR_TOKEN: ${{ steps.org-token.outputs.token }} + GH_SECURITY_AUDITOR_ORGANIZATION: ${{ secrets.TEST_GITHUB_ORG }} + run: | + docker-compose run --rm tests diff --git a/.gitignore b/.gitignore index e4ad979..1bce003 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ _testmain.go tags wiki *.envrc* +version.txt /VERSION.cache bin/ @@ -50,3 +51,5 @@ bin/ # For example, set up .git/info/exclude or use a global .gitignore. githubsecurity.json + +dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..827a30c --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,41 @@ +# https://goreleaser.com/customization/build/ + +before: + hooks: + - go mod tidy + - go generate ./... + +builds: + - main: cmd/github-analyzer/main.go + binary: github-analyzer + env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm + - arm64 + +archives: + - replacements: + darwin: Darwin + linux: Linux + windows: Windows + 386: i386 + amd64: x86_64 + +checksum: + name_template: "checksums.txt" + +snapshot: + name_template: "{{ incpatch .Version }}-next" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" diff --git a/Dockerfile b/Dockerfile index c2d150f..cc98d36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,22 @@ -# syntax=docker/dockerfile:1 +FROM golang:1.19-alpine as build -FROM golang:1.19-alpine +RUN apk add --no-cache git make WORKDIR /ghanalyzer -ADD . /ghanalyzer +ADD go.* /ghanalyzer/ RUN go mod download RUN go env -w GO111MODULE=on -RUN mkdir -p bin && go generate && go build -v -o bin/github-analyzer cmd/github-analyzer/main.go +ADD . /ghanalyzer/ -ENTRYPOINT [ "/ghanalyzer/bin/github-analyzer" ] +RUN make all + +# ---------------------------------------------------------------------------- + +FROM alpine + +COPY --from=build /ghanalyzer/bin/github-analyzer /bin/github-analyzer + +ENTRYPOINT [ "/bin/github-analyzer" ] diff --git a/Makefile b/Makefile index b4a76ad..43c0185 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,22 @@ +BIN=$(notdir $(wildcard cmd/*)) +VERSION=$(shell git describe --tags --long) + .PHONY: all -all: ## compile auditor +all: $(addprefix bin/,$(BIN)) ## compile auditor + +bin/%: bin generate + go build \ + -v \ + -ldflags "-X main.version=$(VERSION)" \ + -o $@ \ + cmd/$*/main.go + +bin: mkdir -p bin - go generate - go build -v -o bin/github-analyzer cmd/github-analyzer/main.go + +.PHONY: generate +generate: ## process go:generate files + go generate ./... .PHONY: lint lint: ## lint everything with pre-commit @@ -22,11 +36,11 @@ fmt: ## go format gofmt -w ./$* .PHONY: vet -vet: ## go vet +vet: generate ## go vet go vet ./... .PHONY: test -test: ## run go tests (requires GitHub to be reachable via the network) +test: generate ## run go tests (requires GitHub to be reachable via the network) go test -v -race -coverprofile coverage.txt ./... .PHONY: help diff --git a/README.md b/README.md index 1868efb..207026f 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ You can see available options via the `--help` flag. ```sh docker compose run \ --rm --service-ports \ - co-github-analyzer \ + github-analyzer \ --organization \ --output output \ --token "$GH_SECURITY_AUDITOR_TOKEN" diff --git a/cmd/github-analyzer/main.go b/cmd/github-analyzer/main.go index fd9556b..c91304b 100644 --- a/cmd/github-analyzer/main.go +++ b/cmd/github-analyzer/main.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "runtime/debug" "strings" _ "embed" @@ -21,9 +22,22 @@ import ( "github.com/spf13/viper" ) -//go:generate sh version.sh -//go:embed version.txt -var version string +var version = "(devel)" + +func getVersion() (response string) { + // inspired from + // https://github.com/mvdan/sh/blob/6ba49e2c622e3f56330f4de6238a390f395db2d8/cmd/shfmt/main.go#L181-L192 + if info, ok := debug.ReadBuildInfo(); ok && version == "(devel)" { + mod := &info.Main + if mod.Replace != nil { + mod = mod.Replace + } + if mod.Version != "" { + version = mod.Version + } + } + return version +} func main() { if err := NewRootCommand().Execute(); err != nil { @@ -137,7 +151,7 @@ func NewRootCommand() *cobra.Command { rootCmd := &cobra.Command{ Use: fmt.Sprintf( "github-analyzer (%s)", - strings.TrimSuffix(version, "\n"), + strings.TrimSuffix(getVersion(), "\n"), ), Short: "A tool to collect statistics and highlight potential security issues within a GitHub org", Long: "A tool to collect statistics and highlight potential security issues within a GitHub org", @@ -148,7 +162,7 @@ func NewRootCommand() *cobra.Command { PreRun: func(cmd *cobra.Command, args []string) { onlyPrintVersion, _ := cmd.Flags().GetBool("version") if onlyPrintVersion { - fmt.Println(version) + fmt.Println(getVersion()) os.Exit(0) } cmd.MarkFlagRequired("organization") @@ -175,9 +189,6 @@ func NewRootCommand() *cobra.Command { rootCmd.Flags(). BoolVarP(&config.ViperEnv.UserPermissionStats, "userPermissionStats", "", false, "enable user permission statistics (might be slow in large orgs due to throttling limits)") - rootCmd.Flags(). - BoolVarP(&config.ViperEnv.DisableServer, "disableServer", "", false, "do not spin up an HTTP server, and only emit data in the designated output folder") - rootCmd.Flags(). BoolVarP(&config.ViperEnv.EnableScraping, "enableScraping", "", false, "enable experimental checks that rely on screen scraping") rootCmd.Flags(). @@ -189,6 +200,8 @@ func NewRootCommand() *cobra.Command { rootCmd.Flags(). IntVarP(&config.ViperEnv.Port, "port", "", 3000, "port for local http server used to display HTML with summary of findings (if you are using docker you will need to override the default port appropriately)") + rootCmd.Flags(). + BoolVarP(&config.ViperEnv.DisableServer, "disableServer", "", false, "do not spin up an HTTP server, and only emit data in the designated output folder") return rootCmd } diff --git a/cmd/github-analyzer/version.sh b/cmd/github-analyzer/version.sh deleted file mode 100644 index 319982b..0000000 --- a/cmd/github-analyzer/version.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -git describe --tags --long > version.txt diff --git a/cmd/github-analyzer/version.txt b/cmd/github-analyzer/version.txt deleted file mode 100644 index 45b3c63..0000000 --- a/cmd/github-analyzer/version.txt +++ /dev/null @@ -1 +0,0 @@ -v0.1.4-pre-alpha-5-g1722c63 diff --git a/docker-compose.yml b/docker-compose.yml index 45b9208..9065875 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,26 @@ version: "3.8" services: - co-github-analyzer: - # image allows to cache all deps hence to speed up CI - # CI image: ghcr.io/crashappsec/github-analyzer:latest - build: . # CI - container_name: github-analyzer - working_dir: $PWD + github-analyzer: + build: . ports: - 3000:3000 + working_dir: $PWD + volumes: + - $PWD:$PWD # this allows to share ./output/ + environment: + GH_SECURITY_AUDITOR_TOKEN: ${GH_SECURITY_AUDITOR_TOKEN:-} + GH_SECURITY_AUDITOR_USERNAME: ${GH_SECURITY_AUDITOR_USERNAME:-} + GH_SECURITY_AUDITOR_PASSWORD: ${GH_SECURITY_AUDITOR_PASSWORD:-} + GH_SECURITY_AUDITOR_OTP_SEED: ${GH_SECURITY_AUDITOR_OTP_SEED:-} + + tests: + image: golang:1.19 + command: make test + init: true + working_dir: $PWD volumes: - - $PWD:$PWD + - $PWD:$PWD # this allows to share ./output/ environment: GH_SECURITY_AUDITOR_TOKEN: ${GH_SECURITY_AUDITOR_TOKEN:-} GH_SECURITY_AUDITOR_USERNAME: ${GH_SECURITY_AUDITOR_USERNAME:-} diff --git a/pkg/config/config.go b/pkg/config/config.go index d85a74c..7e914c3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -9,7 +9,6 @@ const ( type ViperEnvVars struct { CfgFile string `mapstructure:"CFG_FILE"` EnableScraping bool `mapstructure:"ENABLE_SCRAPING"` - DisableServer bool `mapstructure:"DISABLE_SERVER"` UserPermissionStats bool `mapstructure:"USER_PERMISSION_STATS"` Version bool `mapstructure:"VERSION"` Organization string `mapstructure:"ORGANIZATION"` @@ -17,6 +16,7 @@ type ViperEnvVars struct { OutputDir string `mapstructure:"OUTPUT_DIR"` Password string `mapstructure:"PASSWORD"` Port int `mapstructure:"PORT"` + DisableServer bool `mapstructure:"DISABLE_SERVER"` ScmURL string `mapstructure:"SCM_URL"` Token string `mapstructure:"TOKEN"` Username string `mapstructure:"USERNAME"` diff --git a/pkg/github/auditor/auditor_test.go b/pkg/github/auditor/auditor_test.go index d70aee5..7e54a91 100644 --- a/pkg/github/auditor/auditor_test.go +++ b/pkg/github/auditor/auditor_test.go @@ -42,12 +42,18 @@ func TestSampleOrg(t *testing.T) { Max: 3 * time.Minute, Jitter: true, } - name := "github-security-auditor-test-org" + + name := os.Getenv("GH_SECURITY_AUDITOR_ORGANIZATION") + if name == "" { + name = "github-security-auditor-test-org" + } + org, err := org.NewOrganization(ctx, auditor.client, back, name) + assert.Nil(t, err, "Could not create organization") assert.NotNil(t, org.CoreStats, "Could not fetch core stats") assert.Equal(t, name, *org.CoreStats.Login) - assert.GreaterOrEqual(t, 1, org.CoreStats.TotalPrivateRepos) + assert.GreaterOrEqual(t, 1, *org.CoreStats.TotalPrivateRepos) assert.NotNil( t, org.CoreStats.TwoFactorRequirementEnabled,