├── cosign.pub ├── .gitignore ├── pkg ├── common │ ├── constants.go │ ├── variables.go │ ├── commands.go │ └── version.go ├── utils │ ├── utils.go │ └── hash.go ├── commands │ ├── version.go │ ├── global.go │ ├── create.go │ └── validate.go └── integrity │ ├── integrity_test.go │ └── integrity.go ├── .releaserc.yml ├── .github ├── workflows │ ├── golangci-lint.yml │ ├── semantic.yml │ ├── tests.yml │ └── goreleaser.yml └── renovate.json ├── go.mod ├── LICENSE ├── main.go ├── Makefile ├── .golangci.yml ├── .goreleaser.yml ├── go.sum └── README.md /cosign.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+XlR2vLB9Jmg2kyE7Fhb0+9KN51I 3 | fkHQTilzveJrutfMfdcTsVfOxCz3VbkEV1MfYPifYI1g9lteAqR2YVGpyg== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .DS_Store? 3 | ._* 4 | .Spotlight-V100 5 | .Trashes 6 | ehthumbs.db 7 | Thumbs.db 8 | vendor 9 | release 10 | VERSION-* 11 | ?sans-integrity* 12 | sans-integrity* 13 | .envrc 14 | *.key 15 | .idea 16 | dist -------------------------------------------------------------------------------- /pkg/common/constants.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | const NameFormat = `\d{3}[A-Z]?.\d{2}.\d[A-Z]?` 4 | 5 | const Filename = "sans-integrity.yml" 6 | const FilenameSigned = "sans-integrity.yml.gpg" 7 | const GetFirstDirectory = "get_first" 8 | -------------------------------------------------------------------------------- /pkg/common/variables.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | var IgnoreAlways = []string{ 4 | Filename, 5 | FilenameSigned, 6 | } 7 | 8 | var IgnoreOnCreate = []string{ 9 | ".DS_Store", 10 | "desktop.ini", 11 | ".System Volume Information", 12 | "Icon?", 13 | } 14 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | func IsDirectoryEmpty(name string) (bool, error) { 9 | f, err := os.Open(name) 10 | if err != nil { 11 | return false, err 12 | } 13 | defer f.Close() 14 | 15 | _, err = f.Readdirnames(1) 16 | if err == io.EOF { 17 | return true, nil 18 | } 19 | return false, err 20 | } 21 | -------------------------------------------------------------------------------- /.releaserc.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - "@semantic-release/commit-analyzer" 3 | - "@semantic-release/release-notes-generator" 4 | - "@semantic-release/github" 5 | tagFormat: "${version}" 6 | branches: 7 | - name: +([0-9])?(.{+([0-9]),x}).x 8 | - name: main 9 | - name: next 10 | prerelease: true 11 | - name: pre/rc 12 | prerelease: '${name.replace(/^pre\\//g, "")}' 13 | -------------------------------------------------------------------------------- /pkg/common/commands.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | var commands []*cli.Command 9 | 10 | // Commander -- 11 | type Commander interface { 12 | Execute(c *cli.Context) 13 | } 14 | 15 | // RegisterCommand -- 16 | func RegisterCommand(command *cli.Command) { 17 | logrus.Debugln("Registering", command.Name, "command...") 18 | commands = append(commands, command) 19 | } 20 | 21 | // GetCommands -- 22 | func GetCommands() []*cli.Command { 23 | return commands 24 | } 25 | -------------------------------------------------------------------------------- /pkg/commands/version.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sans-sroc/integrity/pkg/common" 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | type versionCommand struct { 11 | } 12 | 13 | func (w *versionCommand) Execute(c *cli.Context) error { 14 | fmt.Printf("%s\n", common.AppVersion.Summary) 15 | 16 | return nil 17 | } 18 | 19 | func init() { 20 | cmd := versionCommand{} 21 | 22 | cliCmd := &cli.Command{ 23 | Name: "version", 24 | Usage: "print version", 25 | Action: cmd.Execute, 26 | } 27 | 28 | common.RegisterCommand(cliCmd) 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | golangci-lint: 12 | name: golangci-lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 16 | - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5 17 | with: 18 | go-version: '1.22.x' 19 | cache: false 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v6 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sans-sroc/integrity 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 7 | github.com/sirupsen/logrus v1.9.3 8 | github.com/urfave/cli/v2 v2.27.5 9 | gopkg.in/yaml.v2 v2.4.0 10 | ) 11 | 12 | require ( 13 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 14 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 15 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 16 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 17 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /pkg/utils/hash.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "io" 7 | "os" 8 | ) 9 | 10 | // HashFileSha256 hashes a file using sha256 11 | func HashFileSha256(filePath string) (string, error) { 12 | var sha256String string 13 | file, err := os.Open(filePath) 14 | if err != nil { 15 | return "", err 16 | } 17 | defer file.Close() 18 | 19 | hash := sha256.New() 20 | if _, err := io.Copy(hash, file); err != nil { 21 | return sha256String, err 22 | } 23 | 24 | hashInBytes := hash.Sum(nil)[:32] 25 | sha256String = hex.EncodeToString(hashInBytes) 26 | 27 | return sha256String, nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/integrity/integrity_test.go: -------------------------------------------------------------------------------- 1 | package integrity 2 | 3 | import "testing" 4 | 5 | var valid = []string{ 6 | "123.21.1", 7 | "123A.21.1", 8 | "123.21.1A", 9 | "123A.21.1A", 10 | } 11 | 12 | var invalid = []string{ 13 | "SEC123.21.1", 14 | "123.21.10", 15 | "123AA.21.1", 16 | "123.21.1AA", 17 | "12.12.1", 18 | "123.2.2", 19 | } 20 | 21 | func TestValidNames(t *testing.T) { 22 | for _, v := range valid { 23 | integrity, err := New("/tmp", false) 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | 28 | if err := integrity.SetName(v); err != nil { 29 | t.Error(err) 30 | } 31 | } 32 | } 33 | 34 | func TestInvalidNames(t *testing.T) { 35 | for _, v := range invalid { 36 | integrity, err := New("/tmp", false) 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | 41 | if err := integrity.SetName(v); err == nil { 42 | t.Errorf("Name matched and it should not have: %s", v) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/common/version.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // NAME of the App 4 | var NAME = "integrity" 5 | 6 | // SUMMARY of the Version, this is using git describe 7 | // Note: This generally gets set to the major version of the app, 8 | // it gets set to the real version during the build process. 9 | var SUMMARY = "3.0.0" 10 | 11 | // BRANCH of the Version 12 | var BRANCH = "main" 13 | 14 | // VERSION of Release 15 | // Note: This generally gets set to the major version of the app, 16 | // it gets set to the real version during the build process. 17 | var VERSION = "3.0.0" 18 | 19 | // AppVersion -- 20 | var AppVersion AppVersionInfo 21 | 22 | // AppVersionInfo -- 23 | type AppVersionInfo struct { 24 | Name string 25 | Version string 26 | Branch string 27 | Summary string 28 | } 29 | 30 | func init() { 31 | AppVersion = AppVersionInfo{ 32 | Name: NAME, 33 | Version: VERSION, 34 | Branch: BRANCH, 35 | Summary: SUMMARY, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 SANS Institute 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 6 | persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 9 | Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 12 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 14 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path" 6 | 7 | "github.com/sirupsen/logrus" 8 | "github.com/urfave/cli/v2" 9 | 10 | _ "github.com/sans-sroc/integrity/pkg/commands" 11 | "github.com/sans-sroc/integrity/pkg/common" 12 | ) 13 | 14 | func main() { 15 | defer func() { 16 | if r := recover(); r != nil { 17 | // log panics forces exit 18 | if _, ok := r.(*logrus.Entry); ok { 19 | os.Exit(1) 20 | } 21 | panic(r) 22 | } 23 | }() 24 | 25 | app := cli.NewApp() 26 | app.Name = path.Base(os.Args[0]) 27 | app.Usage = common.AppVersion.Name 28 | app.Version = common.AppVersion.Summary 29 | app.HideHelpCommand = true 30 | app.HideVersion = true 31 | app.Authors = []*cli.Author{ 32 | { 33 | Name: "Ryan Nicholson", 34 | Email: "rnicholson@sans.org", 35 | }, 36 | { 37 | Name: "Don Williams", 38 | Email: "dwilliams@sans.org", 39 | }, 40 | { 41 | Name: "Erik Kristensen", 42 | Email: "ekristensen@sans.org", 43 | }, 44 | } 45 | 46 | app.Commands = common.GetCommands() 47 | app.CommandNotFound = func(context *cli.Context, command string) { 48 | logrus.Fatalf("Command %s not found.", command) 49 | } 50 | 51 | if err := app.Run(os.Args); err != nil { 52 | logrus.Fatal(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/semantic.yml: -------------------------------------------------------------------------------- 1 | name: semantic 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - next 7 | 8 | permissions: 9 | contents: read # for checkout 10 | 11 | jobs: 12 | release: 13 | name: release 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write # to be able to publish a GitHub release 17 | issues: write # to be able to comment on released issues 18 | pull-requests: write # to be able to comment on released pull requests 19 | id-token: write # to enable use of OIDC for npm provenance 20 | steps: 21 | - name: checkout 22 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 23 | with: 24 | fetch-depth: 0 25 | - name: setup node.js 26 | uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4 27 | with: 28 | node-version: "lts/*" 29 | - name: generate-token 30 | id: generate_token 31 | uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 32 | with: 33 | app_id: ${{ secrets.SROC_BOT_APP_ID }} 34 | private_key: ${{ secrets.SROC_BOT_APP_PEM }} 35 | revoke: true 36 | - name: release 37 | uses: cycjimmy/semantic-release-action@v4 38 | env: 39 | GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 40 | 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BRANCH := $(shell git rev-parse --symbolic-full-name --abbrev-ref HEAD) 2 | SUMMARY := $(shell bash .ci/version) 3 | VERSION := $(shell cat VERSION) 4 | NAME := $(shell basename `pwd`) 5 | MODULE := $(shell cat go.mod | head -n1 | cut -f2 -d' ') 6 | 7 | .PHONY: build release vendor release-all 8 | 9 | vendor: 10 | go mod vendor 11 | 12 | build: vendor 13 | go build -ldflags "-X $(MODULE)/pkg/common.SUMMARY=$(SUMMARY) -X $(MODULE)/pkg/common.BRANCH=$(BRANCH) -X $(MODULE)/pkg/common.VERSION=$(VERSION)" -o $(NAME) 14 | 15 | release: vendor 16 | mkdir -p release 17 | go build -mod=vendor -ldflags "-X $(MODULE)/pkg/common.SUMMARY=$(SUMMARY) -X $(MODULE)/pkg/common.BRANCH=$(BRANCH) -X $(MODULE)/pkg/common.VERSION=$(VERSION)" -o release/$(NAME) . 18 | 19 | release-all: vendor 20 | mkdir -p release 21 | env GOOS=windows GOARCH=amd64 go build -mod=vendor -ldflags "-X $(MODULE)/pkg/common.SUMMARY=$(SUMMARY) -X $(MODULE)/pkg/common.BRANCH=$(BRANCH) -X $(MODULE)/pkg/common.VERSION=$(VERSION)" -o release/$(NAME).exe . 22 | env GOOS=linux GOARCH=amd64 go build -mod=vendor -ldflags "-X $(MODULE)/pkg/common.SUMMARY=$(SUMMARY) -X $(MODULE)/pkg/common.BRANCH=$(BRANCH) -X $(MODULE)/pkg/common.VERSION=$(VERSION)" -o release/$(NAME)-linux . 23 | env GOOS=darwin GOARCH=amd64 go build -mod=vendor -ldflags "-X $(MODULE)/pkg/common.SUMMARY=$(SUMMARY) -X $(MODULE)/pkg/common.BRANCH=$(BRANCH) -X $(MODULE)/pkg/common.VERSION=$(VERSION)" -o release/$(NAME) . 24 | 25 | run-%: vendor 26 | go run -mod=vendor -ldflags "-X $(MODULE)/pkg/common.SUMMARY=$(SUMMARY) -X $(MODULE)/pkg/common.BRANCH=$(BRANCH) -X $(MODULE)/pkg/common.VERSION=$(VERSION)" main.go $* 27 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | dupl: 3 | threshold: 100 4 | funlen: 5 | lines: 100 6 | statements: 50 7 | goconst: 8 | min-len: 2 9 | min-occurrences: 3 10 | gocritic: 11 | enabled-tags: 12 | - diagnostic 13 | - experimental 14 | - opinionated 15 | - performance 16 | - style 17 | disabled-checks: 18 | - dupImport # https://github.com/go-critic/go-critic/issues/845 19 | - ifElseChain 20 | - octalLiteral 21 | - whyNoLint 22 | gocyclo: 23 | min-complexity: 15 24 | golint: 25 | min-confidence: 0 26 | lll: 27 | line-length: 140 28 | maligned: 29 | suggest-new: true 30 | misspell: 31 | locale: US 32 | 33 | linters: 34 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 35 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 36 | disable-all: true 37 | enable: 38 | - bodyclose 39 | #- depguard 40 | - dogsled 41 | - dupl 42 | - errcheck 43 | - copyloopvar 44 | - funlen 45 | - goconst 46 | - gocritic 47 | - gocyclo 48 | - gofmt 49 | - goimports 50 | - goprintffuncname 51 | - gosec 52 | - gosimple 53 | - govet 54 | - ineffassign 55 | - lll 56 | - misspell 57 | - nakedret 58 | - noctx 59 | - nolintlint 60 | - staticcheck 61 | - stylecheck 62 | - typecheck 63 | - unconvert 64 | - unparam 65 | - unused 66 | - whitespace 67 | 68 | issues: 69 | exclude-rules: 70 | - path: _test\.go 71 | linters: 72 | - funlen 73 | 74 | run: 75 | timeout: 2m -------------------------------------------------------------------------------- /pkg/commands/global.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "runtime" 7 | 8 | "github.com/shiena/ansicolor" 9 | "github.com/sirupsen/logrus" 10 | "github.com/urfave/cli/v2" 11 | 12 | "github.com/sans-sroc/integrity/pkg/common" 13 | ) 14 | 15 | func globalFlags() []cli.Flag { 16 | globalFlags := []cli.Flag{ 17 | &cli.StringFlag{ 18 | Name: "log-level", 19 | Usage: "Log Level", 20 | Aliases: []string{"l"}, 21 | EnvVars: []string{"LOG_LEVEL"}, 22 | Value: "info", 23 | }, 24 | &cli.StringFlag{ 25 | Name: "directory", 26 | Usage: "The directory that will be the current working directory for the tool when it runs", 27 | Aliases: []string{"d"}, 28 | EnvVars: []string{"DIRECTORY"}, 29 | Value: ".", 30 | }, 31 | &cli.StringFlag{ 32 | Name: "filename", 33 | Usage: "The integrity file", 34 | Hidden: true, 35 | Value: common.Filename, 36 | }, 37 | } 38 | 39 | return globalFlags 40 | } 41 | 42 | func globalBefore(c *cli.Context) error { 43 | switch c.String("log-level") { 44 | case "debug": 45 | logrus.SetLevel(logrus.DebugLevel) 46 | case "info": 47 | logrus.SetLevel(logrus.InfoLevel) 48 | case "warn": 49 | logrus.SetLevel(logrus.WarnLevel) 50 | case "error": 51 | logrus.SetLevel(logrus.ErrorLevel) 52 | case "none": 53 | logrus.SetOutput(io.Discard) 54 | } 55 | 56 | if runtime.GOOS == "windows" { 57 | logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true}) 58 | logrus.SetOutput(ansicolor.NewAnsiColorWriter(os.Stdout)) 59 | } 60 | 61 | if c.Bool("json") { 62 | logrus.SetOutput(os.Stderr) 63 | 64 | if runtime.GOOS == "windows" { 65 | logrus.SetOutput(ansicolor.NewAnsiColorWriter(os.Stderr)) 66 | } 67 | } 68 | 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:best-practices" 4 | ], 5 | "vulnerabilityAlerts": { 6 | "labels": [ 7 | "security" 8 | ], 9 | "automerge": true, 10 | "assignees": [ 11 | "@ekristen" 12 | ] 13 | }, 14 | "packageRules": [ 15 | { 16 | "matchUpdateTypes": [ 17 | "minor", 18 | "patch" 19 | ], 20 | "matchCurrentVersion": "!/^0/", 21 | "automerge": false 22 | }, 23 | { 24 | "matchManagers": [ 25 | "dockerfile" 26 | ], 27 | "matchUpdateTypes": [ 28 | "pin", 29 | "digest" 30 | ], 31 | "automerge": true, 32 | "labels": [ 33 | "patch" 34 | ] 35 | }, 36 | { 37 | "groupName": "golang", 38 | "groupSlug": "golang", 39 | "matchPackageNames": [ 40 | "/^golang.*/" 41 | ] 42 | }, 43 | { 44 | "matchFileNames": [ 45 | ".github/workflows/*.yml" 46 | ], 47 | "matchDepTypes": [ 48 | "action" 49 | ], 50 | "matchCurrentVersion": "!/^0/", 51 | "automerge": true 52 | } 53 | ], 54 | "customManagers": [ 55 | { 56 | "customType": "regex", 57 | "fileMatch": [ 58 | ".*.go$" 59 | ], 60 | "matchStrings": [ 61 | "\"(?.*)\" // renovate: datasource=(?.*?) depName=(?.*?)( versioning=(?.*?))?\\s" 62 | ], 63 | "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}" 64 | }, 65 | { 66 | "customType": "regex", 67 | "fileMatch": [ 68 | "^.github/workflows/.*" 69 | ], 70 | "matchStrings": [ 71 | "go-version: (?.*?).x\n" 72 | ], 73 | "depNameTemplate": "golang", 74 | "datasourceTemplate": "docker" 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | release: 3 | github: 4 | owner: sans-sroc 5 | name: integrity 6 | make_latest: false 7 | env: 8 | - REGISTRY=ghcr.io 9 | - IMAGE=sans-sroc/integrity 10 | builds: 11 | - id: integrity 12 | goos: 13 | - linux 14 | - windows 15 | - darwin 16 | goarch: 17 | - amd64 18 | - arm64 19 | flags: 20 | - -trimpath 21 | ldflags: 22 | - -s 23 | - -w 24 | - -extldflags="-static" 25 | - -X '{{ .ModulePath }}/pkg/common.SUMMARY=v{{ .Version }}' 26 | - -X '{{ .ModulePath }}/pkg/common.BRANCH={{ .Branch }}' 27 | - -X '{{ .ModulePath }}/pkg/common.VERSION={{ .Tag }}' 28 | - -X '{{ .ModulePath }}/pkg/common.COMMIT={{ .Commit }}' 29 | mod_timestamp: '{{ .CommitTimestamp }}' 30 | hooks: 31 | post: 32 | - cmd: | 33 | {{- if eq .Os "darwin" -}} 34 | quill sign-and-notarize "{{ .Path }}" --dry-run={{ .IsSnapshot }} --ad-hoc={{ .IsSnapshot }} -vv 35 | {{- else -}} 36 | true 37 | {{- end -}} 38 | env: 39 | - QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log 40 | sboms: 41 | - artifacts: archive 42 | archives: 43 | - id: integrity 44 | builds: 45 | - integrity 46 | name_template: "{{ .ProjectName }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}" 47 | format_overrides: 48 | - goos: windows 49 | format: zip 50 | signs: 51 | - ids: 52 | - default 53 | - darwin 54 | cmd: cosign 55 | signature: "${artifact}.sig" 56 | certificate: "${artifact}.pem" 57 | args: ["sign-blob", "--yes", "--oidc-provider=github", "--oidc-issuer=https://token.actions.githubusercontent.com", "--output-certificate=${certificate}", "--output-signature=${signature}", "${artifact}"] 58 | artifacts: all 59 | checksum: 60 | name_template: "checksums.txt" 61 | snapshot: 62 | version_template: '{{ trimprefix .Summary "v" }}' 63 | # We are skipping changelog because we are using semantic release 64 | changelog: 65 | disable: true 66 | -------------------------------------------------------------------------------- /pkg/commands/create.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/urfave/cli/v2" 8 | 9 | "github.com/sans-sroc/integrity/pkg/common" 10 | "github.com/sans-sroc/integrity/pkg/integrity" 11 | ) 12 | 13 | type createCommand struct { 14 | } 15 | 16 | func (w *createCommand) Execute(c *cli.Context) error { 17 | if c.Args().Len() > 0 { 18 | return fmt.Errorf("positional arguments are not supported with this command.\n\n" + //nolint:stylecheck 19 | "Did you mean to use `-d` to change the directory that the command runs against?\n\n") 20 | } 21 | 22 | run, err := integrity.New(c.String("directory"), false) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | if err := run.SetName(c.String("name")); err != nil { 28 | return err 29 | } 30 | 31 | run.SetFilename(c.String("filename")) 32 | run.SetIgnore(c.StringSlice("ignore")) 33 | 34 | if err := run.SetAlgorithm(c.String("algorithm")); err != nil { 35 | return err 36 | } 37 | 38 | if err := run.Checks(); err != nil { 39 | return err 40 | } 41 | 42 | if err := run.DiscoverFiles(); err != nil { 43 | return err 44 | } 45 | 46 | if err := run.HashFiles(); err != nil { 47 | return err 48 | } 49 | 50 | if err := run.WriteFile(); err != nil { 51 | return err 52 | } 53 | 54 | logrus.Info("Integrity file created successfully!") 55 | 56 | return nil 57 | } 58 | 59 | func init() { 60 | cmd := createCommand{} 61 | 62 | flags := []cli.Flag{ 63 | &cli.StringFlag{ 64 | Name: "name", 65 | Usage: fmt.Sprintf("The name that will be given to the ISO volume during USB creation. Format: %s", common.NameFormat), 66 | Aliases: []string{"n"}, 67 | EnvVars: []string{"NAME"}, 68 | Required: true, 69 | }, 70 | &cli.StringFlag{ 71 | Name: "algorithm", 72 | Usage: "Algorithm to use for hashing the files", 73 | Value: "sha256", 74 | Aliases: []string{"a"}, 75 | Hidden: true, 76 | }, 77 | &cli.StringSliceFlag{ 78 | Name: "ignore", 79 | Usage: "Ignore files or directories as a direct match, prefix, or as a regular expressions", 80 | Aliases: []string{"i"}, 81 | Hidden: true, 82 | Value: cli.NewStringSlice(common.IgnoreOnCreate...), 83 | }, 84 | &cli.StringFlag{ 85 | Name: "user", 86 | Usage: "DEPRECATED: no longer used -- allow setting what user created the file", 87 | Aliases: []string{"u"}, 88 | EnvVars: []string{"USER"}, 89 | }, 90 | } 91 | 92 | cliCmd := &cli.Command{ 93 | Name: "create", 94 | Usage: "create integrity files", 95 | Action: cmd.Execute, 96 | Flags: append(flags, globalFlags()...), 97 | Before: globalBefore, 98 | } 99 | 100 | common.RegisterCommand(cliCmd) 101 | } 102 | -------------------------------------------------------------------------------- /pkg/commands/validate.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/sans-sroc/integrity/pkg/common" 10 | "github.com/sans-sroc/integrity/pkg/integrity" 11 | "github.com/sirupsen/logrus" 12 | "github.com/urfave/cli/v2" 13 | ) 14 | 15 | type validateCommand struct { 16 | } 17 | 18 | func (w *validateCommand) Execute(c *cli.Context) error { 19 | if c.Args().Len() > 0 { 20 | return fmt.Errorf("positional arguments are not supported with this command.\n\n" + //nolint:stylecheck 21 | "did you mean to use `-d` to change the directory that the command runs against?\n\n") 22 | } 23 | 24 | run, err := integrity.New(c.String("directory"), true) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | if _, err := os.Stat(c.String("filename")); err != nil && strings.Contains(err.Error(), "no such file") { 30 | return errors.New("the sans-integrity.yml checksum file does not exist") 31 | } 32 | 33 | run.SetFilename(c.String("filename")) 34 | run.SetIgnore(common.IgnoreAlways) 35 | 36 | if err := run.Checks(); err != nil { 37 | return err 38 | } 39 | 40 | if err := run.DiscoverFiles(); err != nil { 41 | return err 42 | } 43 | 44 | if err := run.HashFiles(); err != nil { 45 | return err 46 | } 47 | 48 | identical, err := run.CompareFiles() 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if identical { 54 | logrus.Info("Success! All files successfully validated") 55 | } 56 | 57 | if c.String("output-format") == "json" { 58 | b, err := run.GetValidationOutput("json") 59 | if err != nil { 60 | return err 61 | } 62 | 63 | if c.String("output") == "-" { 64 | _, _ = os.Stdout.Write(b) 65 | _, _ = os.Stdout.WriteString("\n") 66 | } else { 67 | if err := os.WriteFile(c.String("output"), b, 0600); err != nil { 68 | return err 69 | } 70 | } 71 | } 72 | 73 | if !identical { 74 | return fmt.Errorf("validation Failed") 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func init() { 81 | cmd := validateCommand{} 82 | 83 | flags := []cli.Flag{ 84 | &cli.StringFlag{ 85 | Name: "output-format", 86 | Usage: "Chose which format to output the validation results (default is none) (valid options: none, json)", 87 | Aliases: []string{"format"}, 88 | EnvVars: []string{"OUTPUT_FORMAT"}, 89 | Value: "none", 90 | }, 91 | &cli.StringFlag{ 92 | Name: "output", 93 | Usage: "When output-format is specified, this controls where it goes, (defaults to stdout)", 94 | Aliases: []string{"o"}, 95 | EnvVars: []string{"OUTPUT"}, 96 | Value: "-", 97 | }, 98 | } 99 | 100 | cliCmd := &cli.Command{ 101 | Name: "validate", 102 | Usage: "validate integrity files", 103 | Action: cmd.Execute, 104 | Flags: append(flags, globalFlags()...), 105 | Before: globalBefore, 106 | } 107 | 108 | common.RegisterCommand(cliCmd) 109 | } 110 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | paths: 11 | - '**/*.go' 12 | - main.go 13 | 14 | jobs: 15 | test: 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | go-version: [1.22.x] 20 | os: [ubuntu-latest, macos-latest, windows-latest] 21 | steps: 22 | - name: setup-go 23 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5 24 | with: 25 | go-version: ${{ matrix.go-version }} 26 | - name: checkout 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 28 | - name: Test 29 | run: go test ./... 30 | 31 | build: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: setup-go 35 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5 36 | with: 37 | go-version: ${{ matrix.go-version }} 38 | - name: checkout 39 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 40 | - name: "build" 41 | run: | 42 | make release-all 43 | - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 44 | with: 45 | name: release 46 | path: release/* 47 | 48 | create: 49 | strategy: 50 | matrix: 51 | os: [ubuntu-latest, macos-latest, windows-latest] 52 | runs-on: ${{ matrix.os }} 53 | needs: build 54 | steps: 55 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 56 | with: 57 | name: release 58 | - run: chmod +x integrity* 59 | if: ${{ matrix.os != 'windows-latest' }} 60 | - run: ./integrity.exe create -n "100.00.0" --filename sans-integrity-${{ matrix.os }}.yml 61 | if: ${{ matrix.os == 'windows-latest' }} 62 | - run: ./integrity-linux create -n "200.00.0" --filename sans-integrity-${{ matrix.os }}.yml 63 | if: ${{ matrix.os == 'ubuntu-latest' }} 64 | - run: ./integrity create -n "300.00.0" --filename sans-integrity-${{ matrix.os }}.yml 65 | if: ${{ matrix.os == 'macos-latest' }} 66 | - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 67 | with: 68 | name: integrity-${{ matrix.os }} 69 | path: sans-integrity* 70 | 71 | validate: 72 | strategy: 73 | matrix: 74 | os: [ubuntu-latest, macos-latest, windows-latest] 75 | dst_os: [ubuntu-latest, macos-latest, windows-latest] 76 | runs-on: ${{ matrix.os }} 77 | needs: 78 | - create 79 | steps: 80 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 81 | with: 82 | name: release 83 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 84 | with: 85 | name: integrity-${{ matrix.dst_os }} 86 | - run: chmod +x integrity* 87 | if: ${{ matrix.os != 'windows-latest' }} 88 | - run: ./integrity.exe validate --filename sans-integrity-${{ matrix.dst_os }}.yml 89 | if: ${{ matrix.os == 'windows-latest' }} 90 | - run: ./integrity-linux validate --filename sans-integrity-${{ matrix.dst_os }}.yml 91 | if: ${{ matrix.os == 'ubuntu-latest' }} 92 | - run: ./integrity validate --filename sans-integrity-${{ matrix.dst_os }}.yml 93 | if: ${{ matrix.os == 'macos-latest' }} 94 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 12 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 13 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 14 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 15 | github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 h1:DAYUYH5869yV94zvCES9F51oYtN5oGlwjxJJz7ZCnik= 16 | github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= 17 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 18 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 19 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 20 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 22 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 23 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 24 | github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= 25 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 26 | github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= 27 | github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 28 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 29 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 30 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 31 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 33 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 35 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 36 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 37 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 38 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 39 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | - next 9 | tags: 10 | - "*" 11 | pull_request: 12 | branches: 13 | - main 14 | - next 15 | paths: 16 | - '**/*.go' 17 | - 'main.go' 18 | release: 19 | types: 20 | - published 21 | 22 | permissions: 23 | contents: write 24 | packages: write 25 | id-token: write 26 | 27 | jobs: 28 | release: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 32 | if: github.event_name == 'pull_request' 33 | with: 34 | fetch-depth: 0 35 | ref: ${{ github.event.pull_request.head.ref }} 36 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 37 | if: github.event_name != 'pull_request' 38 | with: 39 | fetch-depth: 0 40 | - name: setup-go 41 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5 42 | with: 43 | go-version: 1.22.x 44 | - uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0 45 | - name: install cosign 46 | uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3 47 | - name: install quill 48 | env: 49 | QUILL_VERSION: 0.4.2 50 | run: | 51 | curl -Lo /tmp/quill_${QUILL_VERSION}_linux_amd64.tar.gz https://github.com/anchore/quill/releases/download/v${QUILL_VERSION}/quill_${QUILL_VERSION}_linux_amd64.tar.gz 52 | tar -xvf /tmp/quill_${QUILL_VERSION}_linux_amd64.tar.gz -C /tmp 53 | mv /tmp/quill /usr/local/bin/quill 54 | chmod +x /usr/local/bin/quill 55 | - name: set goreleaser default args 56 | if: startsWith(github.ref, 'refs/tags/') == true 57 | run: | 58 | echo "GORELEASER_ARGS=" >> $GITHUB_ENV 59 | - name: set goreleaser args for branch 60 | if: startsWith(github.ref, 'refs/tags/') == false 61 | run: | 62 | echo "GORELEASER_ARGS=--snapshot" >> $GITHUB_ENV 63 | - name: set goreleaser args renovate 64 | if: startsWith(github.ref, 'refs/heads/renovate') == true 65 | run: | 66 | echo "GORELEASER_ARGS=--snapshot --skip publish --skip sign" >> $GITHUB_ENV 67 | - name: setup-quill 68 | uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0 # v2 69 | if: startsWith(github.ref, 'refs/tags/') == true && (github.actor == github.repository_owner || github.actor == 'sans-sroc[bot]') 70 | with: 71 | export-env: true 72 | env: 73 | OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} 74 | QUILL_NOTARY_KEY: ${{ secrets.OP_QUILL_NOTARY_KEY }} 75 | QUILL_NOTARY_KEY_ID: ${{ secrets.OP_QUILL_NOTARY_KEY_ID }} 76 | QUILL_NOTARY_ISSUER: ${{ secrets.OP_QUILL_NOTARY_ISSUER }} 77 | QUILL_SIGN_PASSWORD: ${{ secrets.OP_QUILL_SIGN_PASSWORD }} 78 | QUILL_SIGN_P12: ${{ secrets.OP_QUILL_SIGN_P12 }} 79 | - name: run goreleaser 80 | uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 # v6 81 | with: 82 | distribution: goreleaser 83 | version: latest 84 | args: release --clean ${{ env.GORELEASER_ARGS }} 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | - name: upload artifacts 88 | if: github.event.pull_request.base.ref == 'main' || github.event_name == 'workflow_dispatch' 89 | uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 90 | with: 91 | name: binaries 92 | path: dist/*.tar.gz 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Integrity 2 | 3 | ## Overview 4 | 5 | File validation at it's finest. 6 | 7 | ## Help 8 | 9 | ```help 10 | NAME: 11 | integrity - integrity 12 | 13 | USAGE: 14 | integrity [global options] command [command options] [arguments...] 15 | 16 | AUTHORS: 17 | Ryan Nicholson 18 | Don Williams 19 | Erik Kristensen 20 | 21 | COMMANDS: 22 | create create integrity files 23 | validate validate integrity files 24 | version print version 25 | 26 | GLOBAL OPTIONS: 27 | --help, -h show help (default: false) 28 | ``` 29 | 30 | ### Create 31 | 32 | ```help 33 | NAME: 34 | integrity create - create integrity files 35 | 36 | USAGE: 37 | integrity create [command options] [arguments...] 38 | 39 | OPTIONS: 40 | --name value, -n value The name that will be given to the ISO volume during USB creation. [$NAME] 41 | --user value, -u value allow setting what user created the file (default: "ekristen") [$USER] 42 | --log-level value, -l value Log Level (default: "info") [$LOG_LEVEL] 43 | --directory value, -d value The directory that will be the current working directory for the tool when it runs (default: ".") [$DIRECTORY] 44 | --help, -h show help (default: false) 45 | 46 | ``` 47 | 48 | ### Validate 49 | 50 | ```help 51 | NAME: 52 | integrity validate - validate integrity files 53 | 54 | USAGE: 55 | integrity validate [command options] [arguments...] 56 | 57 | OPTIONS: 58 | --output-format value, --format value Chose which format to output the validation results (default is none) (valid options: none, json) (default: "none") [$OUTPUT_FORMAT] 59 | --output value, -o value When output-format is specified, this controls where it goes, (defaults to stdout) (default: "-") [$OUTPUT] 60 | --log-level value, -l value Log Level (default: "info") [$LOG_LEVEL] 61 | --directory value, -d value The directory that will be the current working directory for the tool when it runs (default: ".") [$DIRECTORY] 62 | --help, -h show help (default: false) 63 | ``` 64 | 65 | #### Validate Output 66 | 67 | The validate output options change the behavior of the too slightly. 68 | 69 | If the `--output-format` is set to `json` and the `--log-level` has not been set to `none` it will write all logs to `STDERR` while the JSON format is written to `STDOUT`, this is to allow the capture of the `json` separately from the log output. 70 | 71 | ## Examples 72 | 73 | ### Simple Create 74 | 75 | ```bash 76 | integrity create -n 572.00.0 77 | ``` 78 | 79 | ### Create w/ Specified Directory 80 | 81 | ```bash 82 | integrity create -n 572.00.0 -d /tmp 83 | ``` 84 | 85 | ### Simple Validate 86 | 87 | **Note:** this assumes the create was run in the current working directory and `sans-integrity.yml` already exists. 88 | 89 | ```bash 90 | integrity validate 91 | ``` 92 | 93 | ### Validate w/ Specified Directory 94 | 95 | ```bash 96 | integrity validate -d /tmp 97 | ``` 98 | 99 | ### Validate w/ JSON Output 100 | 101 | **Note:** this is really only useful for programmatic validation purposes. 102 | 103 | ```bash 104 | integrity validate --output-format json 105 | ``` 106 | 107 | ### Validate w/ JSON Output to File 108 | 109 | **Note:** this is really only useful for programmatic validation purposes. 110 | 111 | ```bash 112 | integrity validate --output-format json --output results.json 113 | ``` 114 | 115 | ## Building 116 | 117 | Requires [goreleaser](https://goreleaser.com/) to build the binaries. 118 | 119 | 120 | To simply build for development purposes: 121 | 122 | ```bash 123 | goreleaser build --clean --snapshot 124 | ``` 125 | 126 | ## Development 127 | 128 | There are go modules included on this project, so you will need to make sure you run `go mod vendor` to bring them to 129 | your local directory if you are using the Makefile as the makefile prefers the use of the vendor directory. 130 | 131 | If you are simply running `go run main.go` the modules will be pulled from vendor or your go root depending on where it 132 | finds it. Golang will also automatically pull the mods down when you run if there are changes. 133 | 134 | During iterative updates if the modules change you will find yourself needing to run `go mod vendor` or at least 135 | `go mod download` to ensure you have the updated modules locally. 136 | 137 | ### Ignore Files 138 | 139 | There are two types of ignore files. `IgnoreOnCreate` and `IgnoreAlways`, both are defined in [pkg/common/constants.go](pkg/common/constants.go). 140 | Files that should go in the `IgnoreOnCreate` are things like `.DS_Store`, whereas files that should go into 141 | `IgnoreAlways` is the `sans-integrity.yml` and `sans-integrity.yml.gpg` 142 | 143 | Changing the ignore files will require a new release of the tool. 144 | 145 | **Note:** to aid developers, the option `-i` is present that allows you to pass a custom ignore strictly for development 146 | purposes while testing and developing on the tool. This is useful when needing to ensure the validation tool is properly 147 | picking up files that are not in the file. 148 | -------------------------------------------------------------------------------- /pkg/integrity/integrity.go: -------------------------------------------------------------------------------- 1 | package integrity 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "strings" 10 | "time" 11 | 12 | "gopkg.in/yaml.v2" 13 | 14 | "github.com/sans-sroc/integrity/pkg/common" 15 | "github.com/sans-sroc/integrity/pkg/utils" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | type Metadata struct { 20 | Name string `json:"name" yaml:"name"` 21 | CreatedAt time.Time `json:"created_at" yaml:"created_at"` 22 | Version string `json:"version" yaml:"version"` 23 | Algorithm string `json:"algorithm" yaml:"algorithm"` 24 | } 25 | 26 | type File struct { 27 | Name string `json:"file" yaml:"file"` 28 | Path string `json:"-" yaml:"-"` 29 | Hash string `json:"hash" yaml:"hash"` 30 | Status string `json:"status,omitempty" yaml:"status,omitempty"` 31 | } 32 | 33 | type Files struct { 34 | Split []*File `json:"split,omitempty" yaml:"split,omitempty"` 35 | Core []*File `json:"core,omitempty" yaml:"core,omitempty"` 36 | } 37 | 38 | type Output struct { 39 | Files []*File `json:"files" yaml:"files"` 40 | } 41 | 42 | type Integrity struct { 43 | Version int `json:"version" yaml:"version"` 44 | Metadata Metadata `json:"metadata" yaml:"metadata"` 45 | Files Files `json:"files" yaml:"files"` 46 | 47 | expectedFiles []*File 48 | validateFiles []*File 49 | combinedFiles []*File 50 | 51 | ignore []string 52 | validate bool 53 | directory string 54 | filename string 55 | baseFilename string 56 | getFirstExists bool 57 | getFirstEmpty bool 58 | getFirstValidate bool 59 | } 60 | 61 | func New(directory string, validate bool) (*Integrity, error) { 62 | i := &Integrity{ 63 | Version: 1, 64 | Metadata: Metadata{ 65 | CreatedAt: time.Now().UTC(), 66 | Version: common.AppVersion.Summary, 67 | }, 68 | 69 | ignore: []string{}, 70 | validate: validate, 71 | 72 | getFirstExists: false, 73 | getFirstEmpty: false, 74 | getFirstValidate: false, 75 | } 76 | 77 | i.directory = filepath.ToSlash(directory) 78 | i.filename = filepath.Join(directory, common.Filename) 79 | 80 | return i, nil 81 | } 82 | 83 | func (i *Integrity) SetFilename(name string) { 84 | i.filename = filepath.Join(i.directory, name) 85 | i.baseFilename = name 86 | } 87 | 88 | var nameRe = regexp.MustCompile(common.NameFormat) 89 | 90 | func (i *Integrity) SetName(name string) error { 91 | matches := nameRe.FindAllString(name, 4) 92 | if len(matches) == 0 { 93 | return fmt.Errorf("%s does not match the required format. Format: %s", name, common.NameFormat) 94 | } 95 | 96 | exactMatch := false 97 | for _, m := range matches { 98 | if m == name { 99 | exactMatch = true 100 | } 101 | } 102 | 103 | if !exactMatch { 104 | return fmt.Errorf("%s does not match the required format. Format: %s", name, common.NameFormat) 105 | } 106 | 107 | i.Metadata.Name = name 108 | 109 | return nil 110 | } 111 | 112 | func (i *Integrity) SetIgnore(ignore []string) { 113 | i.ignore = ignore 114 | 115 | i.ignore = append(i.ignore, common.IgnoreAlways...) 116 | i.ignore = append(i.ignore, i.baseFilename) 117 | } 118 | 119 | func (i *Integrity) SetAlgorithm(algorithm string) error { 120 | algorithm = strings.ToLower(algorithm) 121 | 122 | if algorithm != "sha256" { 123 | return fmt.Errorf("algorithm %s is not supported", algorithm) 124 | } 125 | 126 | i.Metadata.Algorithm = algorithm 127 | 128 | return nil 129 | } 130 | 131 | func (i *Integrity) Checks() error { 132 | if i.validate { 133 | if _, err := os.Stat(i.filename); err == nil { 134 | if err := i.LoadFile(); err != nil { 135 | return err 136 | } 137 | } 138 | } 139 | 140 | getFirstPath := filepath.Join(i.directory, common.GetFirstDirectory) 141 | 142 | if _, err := os.Stat(getFirstPath); err == nil { 143 | isEmpty, err := utils.IsDirectoryEmpty(getFirstPath) 144 | if err != nil { 145 | return err 146 | } 147 | 148 | i.getFirstExists = true 149 | i.getFirstEmpty = isEmpty 150 | } 151 | 152 | if i.validate && i.getFirstExists && !i.getFirstEmpty { 153 | i.getFirstValidate = true 154 | } 155 | 156 | i.expectedFiles = i.Files.Split 157 | i.expectedFiles = append(i.expectedFiles, i.Files.Core...) 158 | 159 | if i.getFirstExists && i.getFirstEmpty { 160 | return fmt.Errorf("%s exists and is empty, this is not allowed, please delete or populate files", common.GetFirstDirectory) 161 | } 162 | 163 | return nil 164 | } 165 | 166 | func (i *Integrity) LoadFile() error { 167 | yamlFile, err := os.ReadFile(i.filename) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | if err := yaml.Unmarshal(yamlFile, i); err != nil { 173 | return err 174 | } 175 | 176 | return nil 177 | } 178 | 179 | func (i *Integrity) SortFiles() error { 180 | for _, f := range i.expectedFiles { 181 | if strings.HasPrefix(f.Name, common.GetFirstDirectory) { 182 | i.Files.Split = append(i.Files.Split, f) 183 | } else { 184 | i.Files.Core = append(i.Files.Core, f) 185 | } 186 | } 187 | 188 | return nil 189 | } 190 | 191 | func (i *Integrity) WriteFile() error { 192 | if err := i.SortFiles(); err != nil { 193 | return err 194 | } 195 | 196 | data, err := yaml.Marshal(i) 197 | if err != nil { 198 | return err 199 | } 200 | 201 | f, err := os.OpenFile(i.filename, os.O_RDWR|os.O_CREATE, 0644) 202 | if err != nil { 203 | return err 204 | } 205 | defer f.Close() 206 | 207 | if err := f.Truncate(0); err != nil { 208 | return err 209 | } 210 | 211 | if _, err := f.Write(data); err != nil { 212 | return err 213 | } 214 | 215 | return nil 216 | } 217 | 218 | func (i *Integrity) CompareFiles() (identical bool, err error) { 219 | identical = true 220 | skippedSplit := false 221 | 222 | expected := map[string]*File{} 223 | actual := map[string]*File{} 224 | combined := map[string]*File{} 225 | 226 | for _, file := range i.validateFiles { 227 | actual[file.Name] = file 228 | } 229 | 230 | for _, file := range i.expectedFiles { 231 | expected[file.Name] = file 232 | } 233 | 234 | for _, file := range i.validateFiles { 235 | if _, ok := expected[file.Name]; !ok { 236 | logrus.WithField("file", file.Name).WithField("status", "added").Error("Added File") 237 | 238 | file.Status = "added" 239 | combined[file.Name] = file 240 | identical = false 241 | } 242 | } 243 | 244 | for _, file := range i.expectedFiles { 245 | if _, ok := actual[file.Name]; ok { 246 | continue 247 | } 248 | 249 | if strings.HasPrefix(file.Name, common.GetFirstDirectory) && !i.getFirstValidate { 250 | skippedSplit = true 251 | logrus.WithField("file", file.Name).Debugf("skipping split file because directory %s does not exist", common.GetFirstDirectory) 252 | continue 253 | } 254 | 255 | logrus.WithField("status", "missing").WithField("file", file.Name).Error("Missing File") 256 | file.Status = "missing" 257 | combined[file.Name] = file 258 | identical = false 259 | } 260 | 261 | for _, file := range i.validateFiles { 262 | if ef, ok := expected[file.Name]; ok { 263 | if ef.Hash != file.Hash { 264 | logrus. 265 | WithField("file", file.Name). 266 | WithField("status", "mismatch"). 267 | WithField("expected_hash", ef.Hash). 268 | WithField("actual_hash", file.Hash). 269 | Error("Checksum Failure") 270 | 271 | file.Status = "failed" 272 | combined[file.Name] = file 273 | identical = false 274 | } else { 275 | logrus. 276 | WithField("file", file.Name). 277 | WithField("status", "ok"). 278 | WithField("hash", ef.Hash). 279 | Debug("Checksum Validated") 280 | 281 | file.Status = "ok" 282 | combined[file.Name] = file 283 | } 284 | } 285 | } 286 | 287 | for _, f := range combined { 288 | i.combinedFiles = append(i.combinedFiles, f) 289 | } 290 | 291 | if skippedSplit { 292 | logrus.Warnf("Split files skipped as %s was missing or empty", common.GetFirstDirectory) 293 | } 294 | 295 | return identical, nil 296 | } 297 | 298 | func (i *Integrity) HashFiles() error { 299 | var files []*File 300 | 301 | if i.validate { 302 | files = i.validateFiles 303 | } else { 304 | files = i.expectedFiles 305 | } 306 | 307 | for _, file := range files { 308 | log := logrus.WithField("file", file.Name) 309 | 310 | log.Info("Processing File") 311 | 312 | hash, err := utils.HashFileSha256(file.Path) 313 | if err != nil { 314 | log.WithError(err).Error("unable to hash file") 315 | return err 316 | } 317 | 318 | log.WithField("hash", hash).Debug("File Processed Successfully") 319 | 320 | file.Hash = hash 321 | } 322 | 323 | return nil 324 | } 325 | 326 | func (i *Integrity) DiscoverFiles() error { 327 | var err error 328 | 329 | if i.validate { 330 | i.validateFiles, err = i.GetFiles() 331 | if err != nil { 332 | return err 333 | } 334 | } else { 335 | i.expectedFiles, err = i.GetFiles() 336 | if err != nil { 337 | return err 338 | } 339 | } 340 | 341 | return nil 342 | } 343 | 344 | func (i *Integrity) GetFiles() (files []*File, err error) { 345 | if err := filepath.Walk(i.directory, 346 | func(path string, info os.FileInfo, err error) error { 347 | pathCheck, pathErr := os.Stat(path) 348 | if pathErr != nil { 349 | return pathErr 350 | } 351 | 352 | if !pathCheck.IsDir() { 353 | fileName, err := filepath.Rel(i.directory, path) 354 | if err != nil { 355 | return err 356 | } 357 | 358 | for _, ignore := range i.ignore { 359 | baseFileName := filepath.Base(fileName) 360 | log := logrus.WithField("ignore", ignore).WithField("file", fileName).WithField("base", baseFileName) 361 | 362 | if fileName == ignore { 363 | log.WithField("reason", "full-filename-match").Debug("ignored file") 364 | return nil 365 | } 366 | 367 | if baseFileName == ignore { 368 | log.WithField("reason", "base-filename-match").Debug("ignored file") 369 | return nil 370 | } 371 | 372 | if strings.HasPrefix(fileName, ignore) { 373 | log.WithField("reason", "prefix-full-filename").Debug("ignored file") 374 | return nil 375 | } 376 | 377 | if strings.HasPrefix(baseFileName, ignore) { 378 | log.WithField("reason", "prefix-base-filename").Debug("ignored file") 379 | return nil 380 | } 381 | 382 | if matched, _ := regexp.MatchString(ignore, fileName); matched { 383 | log.WithField("reason", "regex-full-filename").Debug("ignored file") 384 | return nil 385 | } 386 | 387 | if matched, _ := regexp.MatchString(ignore, baseFileName); matched { 388 | log.WithField("reason", "regex-base-filename").Debug("ignored file") 389 | return nil 390 | } 391 | } 392 | 393 | // Both name and path must be ToSlash because the Name is what 394 | // is ultimately written to the versioning file 395 | files = append(files, &File{ 396 | Name: filepath.ToSlash(fileName), 397 | Path: filepath.ToSlash(path), 398 | }) 399 | } 400 | 401 | return nil 402 | }, 403 | ); err != nil { 404 | return nil, err 405 | } 406 | 407 | return files, nil 408 | } 409 | 410 | func (i *Integrity) GetValidationOutput(format string) ([]byte, error) { 411 | if format != "json" { 412 | return nil, fmt.Errorf("unsupported format: %s", format) 413 | } 414 | 415 | if format == "json" { 416 | b, err := json.Marshal(Output{Files: i.combinedFiles}) 417 | if err != nil { 418 | return nil, err 419 | } 420 | 421 | return b, nil 422 | } 423 | 424 | return nil, nil 425 | } 426 | --------------------------------------------------------------------------------