├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── go.yml │ ├── golangci-lint.yml │ ├── goreportcard.yaml │ ├── mega-linter.yml │ ├── release.yml │ └── scorecard.yml ├── .gitignore ├── .golangci.yaml ├── .mega-linter.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── PKGBUILD ├── README.md ├── SECURITY.md ├── cmd └── validator │ ├── validator.go │ └── validator_test.go ├── functional_test.md ├── go.mod ├── go.sum ├── img ├── custom_recursion.gif ├── custom_reporter.gif ├── demo.gif ├── docker_run.png ├── exclude_dirs.gif ├── exclude_file_types.gif ├── fork-me-on-github.png ├── gb-filetype-and-pass-fail.gif ├── gb-filetype.gif ├── logo.png ├── multiple_paths.gif └── standard_run.gif ├── index.md ├── pkg ├── cli │ ├── cli.go │ ├── cli_test.go │ ├── group_output.go │ └── group_output_test.go ├── filetype │ └── file_type.go ├── finder │ ├── finder.go │ ├── finder_test.go │ └── fsfinder.go ├── misc │ └── arrToMap.go ├── reporter │ ├── json_reporter.go │ ├── junit_reporter.go │ ├── reporter.go │ ├── reporter_test.go │ ├── sarif_reporter.go │ ├── stdout_reporter.go │ ├── writer.go │ └── writer_test.go └── validator │ ├── csv.go │ ├── editorconfig.go │ ├── env.go │ ├── hcl.go │ ├── hocon.go │ ├── ini.go │ ├── json.go │ ├── plist.go │ ├── properties.go │ ├── toml.go │ ├── validator.go │ ├── validator_test.go │ ├── xml.go │ └── yaml.go ├── test ├── fixtures │ ├── exclude-file-types │ │ ├── excluded.json │ │ └── not-excluded.toml │ ├── good.csv │ ├── good.editorconfig │ ├── good.env │ ├── good.hcl │ ├── good.hocon │ ├── good.ini │ ├── good.json │ ├── good.plist │ ├── good.properties │ ├── good.toml │ ├── good.yaml │ ├── mixedcase-extension │ │ ├── good.CSv │ │ ├── good.HCl │ │ ├── good.INi │ │ ├── good.JSon │ │ ├── good.PList │ │ ├── good.PRoperties │ │ ├── good.TOml │ │ └── good.YAml │ ├── subdir │ │ ├── bad.json │ │ ├── bad.toml │ │ ├── bad.yml │ │ └── good.json │ ├── subdir2 │ │ ├── bad.csv │ │ ├── bad.editorconfig │ │ ├── bad.env │ │ ├── bad.hcl │ │ ├── bad.hocon │ │ ├── bad.ini │ │ ├── bad.json │ │ ├── bad.plist │ │ ├── bad.properties │ │ ├── good.ini │ │ └── good.json │ ├── uppercase-extension │ │ ├── good.CSV │ │ ├── good.HCL │ │ ├── good.INI │ │ ├── good.JSON │ │ ├── good.PLIST │ │ ├── good.PROPERTIES │ │ ├── good.TOML │ │ └── good.YAML │ ├── with-depth │ │ ├── additional-depth │ │ │ └── sample.json │ │ └── sample.json │ └── wrong_ext.jason └── output │ └── example │ ├── good.json │ ├── result.json │ ├── result.sarif │ ├── result.xml │ └── writer_example.txt └── version.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # recommended by https://github.com/golangci/golangci-lint-action 2 | # this will force line ending to be lf on windows 3 | *.go text eol=lf -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | 9 | - package-ecosystem: "docker" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | --- 4 | name: Go Pipeline 5 | 6 | # Enable this workflow to run for pull requests and 7 | # pushes to the main branch 8 | on: 9 | push: 10 | branches: 11 | - main 12 | pull_request: 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | download: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Harden Runner 22 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 23 | with: 24 | egress-policy: audit 25 | 26 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 27 | 28 | - name: Set up Go 29 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 30 | with: 31 | go-version: "1.22" 32 | 33 | - name: Download dependencies 34 | run: go mod download 35 | 36 | lint: 37 | needs: download 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Harden Runner 41 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 42 | with: 43 | egress-policy: audit 44 | 45 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 46 | 47 | - name: Set up Go 48 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 49 | with: 50 | go-version: "1.22" 51 | 52 | - name: Static Analysis 53 | run: go vet ./... 54 | 55 | - name: Check Formatting 56 | run: test -z "$(gofmt -s -l -e .)" 57 | 58 | build: 59 | needs: download 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Harden Runner 63 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 64 | with: 65 | egress-policy: audit 66 | 67 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 68 | 69 | - name: Set up Go 70 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 71 | with: 72 | go-version: "1.22" 73 | 74 | - name: Build 75 | run: | 76 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ 77 | go build -ldflags='-w -s -extldflags "-static"' -tags netgo -o validator cmd/validator/validator.go 78 | 79 | test: 80 | needs: download 81 | runs-on: ubuntu-latest 82 | name: Update coverage badge 83 | permissions: 84 | contents: write 85 | steps: 86 | - name: Harden Runner 87 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 88 | with: 89 | egress-policy: audit 90 | 91 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 92 | with: 93 | fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. 94 | 95 | - name: Set up Go 96 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 97 | with: 98 | go-version: "1.22" 99 | 100 | - name: Unit test 101 | run: go test -v -cover -coverprofile coverage.out ./... 102 | 103 | - name: Check coverage 104 | id: check-coverage 105 | env: 106 | COVERAGE_THRESHOLD: 94 107 | run: | 108 | # Validate that the coverage is above or at the required threshold 109 | echo "Checking if test coverage is above threshold ..." 110 | echo "Coverage threshold: ${COVERAGE_THRESHOLD} %" 111 | totalCoverage=$(go tool cover -func coverage.out | grep 'total' | grep -Eo '[0-9]+\.[0-9]+') 112 | echo "Current test coverage : ${totalCoverage} %" 113 | if (( $(echo "${COVERAGE_THRESHOLD} <= ${totalCoverage}" | bc -l) )); then 114 | echo "Coverage OK" 115 | else 116 | echo "Current test coverage is below threshold" 117 | exit 1 118 | fi 119 | echo "total_coverage=${totalCoverage}" >> "${GITHUB_OUTPUT}" 120 | 121 | - name: Create badge img tag and apply to README files 122 | id: generate-badge 123 | run: | 124 | # Create Badge URL 125 | # Badge will always be green because of coverage threshold check 126 | # so we just have to populate the total coverage 127 | totalCoverage=${{ steps.check-coverage.outputs.total_coverage }} 128 | BADGE_URL="https://img.shields.io/badge/Coverage-${totalCoverage}%25-brightgreen" 129 | BADGE_IMG_TAG="\"Code" 130 | 131 | # Update README.md and index.md 132 | for markdown_file in README.md index.md; do 133 | sed -i "/id=\"cov\"/c\\${BADGE_IMG_TAG}" "${markdown_file}" 134 | done 135 | 136 | # Check to see if files were updated 137 | if git diff --quiet; then 138 | echo "badge_updates=false" >> "${GITHUB_OUTPUT}" 139 | else 140 | echo "badge_updates=true" >> "${GITHUB_OUTPUT}" 141 | fi 142 | 143 | - name: Commit changes 144 | if: steps.generate-badge.outputs.badge_updates == 'true' && github.event_name == 'push' 145 | run: | 146 | git config --local user.email "action@github.com" 147 | git config --local user.name "GitHub Action" 148 | git add -- README.md index.md 149 | git commit -m "chore: Updated coverage badge." 150 | git push 151 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: golangci-lint 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | pull_request: 9 | 10 | permissions: 11 | contents: read 12 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 13 | pull-requests: read 14 | 15 | jobs: 16 | golangci: 17 | strategy: 18 | matrix: 19 | go: ["1.21"] 20 | os: [ubuntu-latest, macos-latest, windows-latest] 21 | permissions: 22 | # Optional: Allow write access to checks to allow the action to annotate code in the PR. 23 | checks: write 24 | 25 | name: lint 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - name: Harden Runner 29 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 30 | with: 31 | egress-policy: audit 32 | 33 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 34 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 35 | with: 36 | go-version: ${{ matrix.go }} 37 | cache: false 38 | - name: golangci-lint 39 | uses: golangci/golangci-lint-action@4696ba8babb6127d732c3c6dde519db15edab9ea # v6.5.1 40 | with: 41 | # Require: The version of golangci-lint to use. 42 | # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. 43 | # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. 44 | version: v1.57.2 45 | 46 | # Optional: working directory, useful for monorepos 47 | # working-directory: somedir 48 | 49 | # Optional: golangci-lint command line arguments. 50 | # 51 | # Note: by default the `.golangci.yml` file should be at the root of the repository. 52 | # The location of the configuration file can be changed by using `--config=` 53 | args: --timeout=10m 54 | 55 | # Optional: show only new issues if it's a pull request. The default value is `false`. 56 | # only-new-issues: true 57 | 58 | # Optional:The mode to install golangci-lint. It can be 'binary' or 'goinstall'. 59 | # install-mode: "goinstall" 60 | -------------------------------------------------------------------------------- /.github/workflows/goreportcard.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Go Report Card 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | 10 | permissions: # added using https://github.com/step-security/secure-repo 11 | contents: read 12 | 13 | jobs: 14 | goreportcard: 15 | strategy: 16 | matrix: 17 | go: ["stable"] 18 | os: [ubuntu-latest] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - name: Harden Runner 22 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 23 | with: 24 | egress-policy: audit 25 | 26 | - name: Setup Go ${{ matrix.go }} 27 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 28 | with: 29 | go-version: ${{ matrix.go }} 30 | cache: false 31 | - name: Checkout gojp/goreportcard repo 32 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | with: 34 | repository: gojp/goreportcard 35 | path: goreportcard 36 | - name: Install goreportcard-cli 37 | # goreportcard-cli requires the following linters: 38 | # 1. gometalinter 39 | # 2. golint 40 | # 3. gocyclo 41 | # 4. ineffassign 42 | # 5. misspell 43 | # among which, the linter gometalinter is deprecated. However, goreportcard repo has a vendor version of it. 44 | # Hence installing from the repo instead of `go install`. Refer https://github.com/gojp/goreportcard/issues/301 45 | run: | 46 | cd goreportcard 47 | 48 | # Install prerequisite linter binaries: gometalinter, golint, gocyclo, ineffassign & misspell 49 | # Refer: https://github.com/gojp/goreportcard?tab=readme-ov-file#command-line-interface 50 | make install 51 | 52 | # Install goreportcard-cli binary 53 | go install ./cmd/goreportcard-cli 54 | - name: Checkout Boeing/config-file-validator repo 55 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 56 | - name: Run goreportcard 57 | run: | 58 | # Failure threshold is set to 95% to fail at any errors. Default is 75%. 59 | goreportcard-cli -t 95 60 | -------------------------------------------------------------------------------- /.github/workflows/mega-linter.yml: -------------------------------------------------------------------------------- 1 | # MegaLinter GitHub Action configuration file 2 | # More info at https://megalinter.io 3 | --- 4 | name: MegaLinter 5 | 6 | on: # yamllint disable-line rule:truthy - false positive 7 | push: 8 | 9 | pull_request: 10 | branches: 11 | - main 12 | - master 13 | 14 | concurrency: 15 | group: ${{ github.ref }}-${{ github.workflow }} 16 | cancel-in-progress: true 17 | 18 | permissions: # added using https://github.com/step-security/secure-repo 19 | contents: read 20 | 21 | jobs: 22 | megalinter: 23 | name: MegaLinter 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - name: Harden Runner 28 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 29 | with: 30 | egress-policy: audit 31 | 32 | # Git Checkout 33 | - name: Checkout Code 34 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 35 | with: 36 | token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} 37 | 38 | # MegaLinter 39 | - name: MegaLinter 40 | uses: oxsecurity/megalinter@5a91fb06c83d0e69fbd23756d47438aa723b4a5a # v8.7.0 41 | id: megalinter 42 | env: 43 | VALIDATE_ALL_CODEBASE: true 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | # Upload MegaLinter artifacts 47 | - name: Archive production artifacts 48 | if: success() || failure() 49 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 50 | with: 51 | name: MegaLinter reports 52 | path: | 53 | megalinter-reports 54 | mega-linter.log 55 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release Pipeline 3 | 4 | on: 5 | release: 6 | types: [created] 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | releases-matrix: 17 | name: Release Go Binary 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | goos: [linux, darwin, windows] 22 | goarch: ["386", amd64, arm64] 23 | exclude: 24 | - goarch: "386" 25 | goos: darwin 26 | - goarch: arm64 27 | goos: windows 28 | 29 | permissions: 30 | packages: write 31 | contents: write 32 | 33 | steps: 34 | - name: Harden Runner 35 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 36 | with: 37 | egress-policy: audit 38 | 39 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 40 | - uses: wangyoucao577/go-release-action@481a2c1a0f1be199722e3e9b74d7199acafc30a8 # v1.53 41 | with: 42 | github_token: ${{ secrets.GITHUB_TOKEN }} 43 | goos: ${{ matrix.goos }} 44 | goarch: ${{ matrix.goarch }} 45 | go_version: 1.22 46 | binary_name: "validator" 47 | ldflags: -w -s -extldflags "-static" -X github.com/Boeing/config-file-validator.version=${{ github.event.release.tag_name }} 48 | build_tags: -tags netgo 49 | project_path: cmd/validator 50 | extra_files: LICENSE README.md 51 | 52 | aur-publish: 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Harden Runner 56 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 57 | with: 58 | egress-policy: audit 59 | 60 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 61 | 62 | - name: Publish AUR package 63 | uses: KSXGitHub/github-actions-deploy-aur@2ac5a4c1d7035885d46b10e3193393be8460b6f1 # v4.1.1 64 | with: 65 | pkgname: config-file-validator 66 | pkgbuild: ./PKGBUILD 67 | commit_username: ${{ secrets.AUR_USERNAME }} 68 | commit_email: ${{ secrets.AUR_EMAIL }} 69 | ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} 70 | commit_message: Update AUR package 71 | ssh_keyscan_types: rsa,ecdsa,ed25519 72 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | --- 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: "43 11 * * 5" 14 | push: 15 | branches: ["main"] 16 | pull_request: 17 | workflow_dispatch: 18 | 19 | # Declare default permissions as read only. 20 | permissions: read-all 21 | 22 | jobs: 23 | analysis: 24 | name: Scorecard analysis 25 | runs-on: ubuntu-latest 26 | permissions: 27 | # Needed to upload the results to code-scanning dashboard. 28 | security-events: write 29 | # Needed to publish results and get a badge (see publish_results below). 30 | id-token: write 31 | 32 | steps: 33 | - name: Harden Runner 34 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 35 | with: 36 | egress-policy: audit 37 | 38 | - name: "Checkout code" 39 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 40 | with: 41 | persist-credentials: false 42 | 43 | - name: "Run analysis" 44 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 45 | with: 46 | results_file: results.sarif 47 | results_format: sarif 48 | 49 | # Public repositories: 50 | # - Publish results to OpenSSF REST API for easy access by consumers 51 | # - Allows the repository to include the Scorecard badge. 52 | # - See https://github.com/ossf/scorecard-action#publishing-results. 53 | publish_results: true 54 | 55 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 56 | # format to the repository Actions tab. 57 | - name: "Upload artifact" 58 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v3.pre.node20 59 | with: 60 | name: SARIF file 61 | path: results.sarif 62 | retention-days: 5 63 | 64 | # Upload the results to GitHub's code scanning dashboard (optional). 65 | # Commenting out will disable upload of results to your repo's Code Scanning dashboard 66 | - name: "Upload to code-scanning" 67 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 68 | with: 69 | sarif_file: results.sarif 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | cmd/validator/validator 8 | bin/ 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | vendor/ 18 | .vscode 19 | .idea 20 | megalinter-reports/ 21 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Options for analysis running. 3 | issues: 4 | # Maximum issues count per one linter. 5 | # Set to 0 to disable. 6 | # Default: 50 7 | max-issues-per-linter: 0 8 | # Maximum count of issues with the same text. 9 | # Set to 0 to disable. 10 | # Default: 3 11 | max-same-issues: 0 12 | 13 | linters: 14 | enable: 15 | # check when errors are compared without errors.Is 16 | - errorlint 17 | 18 | # check imports order and makes it always deterministic. 19 | - gci 20 | 21 | # linter to detect errors invalid key values count 22 | - loggercheck 23 | 24 | # report functions we don't want (println for example) 25 | # see linter-settings.forbidigo for more explanations 26 | # - forbidigo 27 | 28 | # Very Basic spell error checker 29 | - misspell 30 | 31 | # simple security check 32 | - gosec 33 | 34 | # Fast, configurable, extensible, flexible, and beautiful linter for Go. 35 | # Drop-in replacement of golint. 36 | - revive 37 | 38 | # Finds sending http request without context.Context 39 | - noctx 40 | 41 | # make sure to use t.Helper() when needed 42 | - thelper 43 | 44 | # make sure that error are checked after a rows.Next() 45 | - rowserrcheck 46 | 47 | # ensure that lint exceptions have explanations. Consider the case below: 48 | - nolintlint 49 | 50 | # detect duplicated words in code 51 | - dupword 52 | 53 | # detect the possibility to use variables/constants from the Go standard library. 54 | - usestdlibvars 55 | 56 | # mirror suggests rewrites to avoid unnecessary []byte/string conversion 57 | - mirror 58 | 59 | # testify checks good usage of github.com/stretchr/testify. 60 | - testifylint 61 | 62 | linters-settings: 63 | usestdlibvars: 64 | # Suggest the use of http.MethodXX. 65 | # Default: true 66 | http-method: true 67 | # Suggest the use of http.StatusXX. 68 | # Default: true 69 | http-status-code: true 70 | # Suggest the use of time.Weekday.String(). 71 | # Default: true 72 | # We don't want this 73 | time-weekday: false 74 | # Suggest the use of constants available in time package 75 | # Default: false 76 | time-layout: true 77 | 78 | nolintlint: 79 | # Disable to ensure that all nolint directives actually have an effect. 80 | # Default: false 81 | allow-unused: true # too many false positive reported 82 | # Exclude following linters from requiring an explanation. 83 | # Default: [] 84 | allow-no-explanation: [] 85 | # Enable to require an explanation of nonzero length 86 | # after each nolint directive. 87 | # Default: false 88 | require-explanation: true 89 | # Enable to require nolint directives to mention the specific 90 | # linter being suppressed. 91 | # Default: false 92 | require-specific: true 93 | 94 | # define the import orders 95 | gci: 96 | sections: 97 | # Standard section: captures all standard packages. 98 | - standard 99 | # Default section: catchall that is not standard or custom 100 | - default 101 | # Custom section: groups all imports with the specified Prefix. 102 | - prefix(github.com/Boeing/config-file-validator) 103 | 104 | staticcheck: 105 | # SAxxxx checks in https://staticcheck.io/docs/configuration/options/#checks 106 | checks: ["all"] 107 | 108 | revive: 109 | enable-all-rules: true 110 | rules: 111 | # we must provide configuration for linter that requires them 112 | # enable-all-rules is OK, but many revive linters expect configuration 113 | # and cannot work without them 114 | 115 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#context-as-argument 116 | - name: context-as-argument 117 | arguments: 118 | - allowTypesBefore: "*testing.T" 119 | 120 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#exported 121 | - name: exported 122 | arguments: 123 | # enables checking public methods of private types 124 | - "checkPrivateReceivers" 125 | # make error messages clearer 126 | - "sayRepetitiveInsteadOfStutters" 127 | 128 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unhandled-error 129 | - name: unhandled-error 130 | arguments: # here are the exceptions we don't want to be reported 131 | - "fmt.Print.*" 132 | - "fmt.Fprint.*" 133 | - "bytes.Buffer.Write" 134 | - "bytes.Buffer.WriteByte" 135 | - "bytes.Buffer.WriteString" 136 | - "strings.Builder.WriteString" 137 | - "strings.Builder.WriteRune" 138 | 139 | # disable everything we don't want 140 | - name: add-constant 141 | disabled: true # too noisy 142 | - name: line-length-limit 143 | disabled: true 144 | - name: argument-limit 145 | disabled: true 146 | - name: cognitive-complexity 147 | disabled: true 148 | - name: banned-characters 149 | disabled: true 150 | - name: cyclomatic 151 | disabled: true 152 | - name: max-public-structs 153 | disabled: true 154 | - name: function-result-limit 155 | disabled: true 156 | - name: function-length 157 | disabled: true 158 | - name: file-header 159 | disabled: true 160 | - name: empty-lines 161 | disabled: true 162 | -------------------------------------------------------------------------------- /.mega-linter.yml: -------------------------------------------------------------------------------- 1 | # Configuration file for MegaLinter 2 | # See all available variables at https://megalinter.io/configuration/ and in linters documentation 3 | --- 4 | EXCLUDED_DIRECTORIES: 5 | - test/fixtures/ 6 | 7 | # Disabled certain linters due to duplication and or redundancy. 8 | DISABLE_LINTERS: 9 | # https://megalinter.io/latest/descriptors/repository_kics/ 10 | - REPOSITORY_KICS 11 | # already enabled as a dedicated linter for this repo 12 | - GO_GOLANGCI_LINT 13 | # another linter, https://megalinter.io/latest/descriptors/go_revive/ 14 | - GO_REVIVE 15 | # Another vulnerability scanner, https://megalinter.io/latest/descriptors/repository_grype/ 16 | - REPOSITORY_GRYPE 17 | # Spell checker, https://megalinter.io/latest/descriptors/spell_lychee/ 18 | - SPELL_LYCHEE 19 | 20 | DISABLE_ERRORS_LINTERS: 21 | # To prevent unnecessary spelling errors (will spit out warnings) 22 | - SPELL_CSPELL 23 | # copypaste checker (JSCPD), can be added in a separate PR as this will need test refactor 24 | - COPYPASTE_JSCPD 25 | 26 | FILTER_REGEX_EXCLUDE: "(test/)" 27 | JSON_JSONLINT_FILTER_REGEX_EXCLUDE: "(test/)" 28 | YAML_YAMLLINT_FILTER_REGEX_EXCLUDE: "(test/)" 29 | YAML_PRETTIER_FILTER_REGEX_EXCLUDE: "(test/)" 30 | SHOW_ELAPSED_TIME: true 31 | REPORT_OUTPUT_FOLDER: megalinter-reports 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | We welcome contributions! We want to make contributing to this project as easy and transparent as possible, whether it's: 3 | 4 | - Reporting a bug 5 | - Discussing the current state of the code 6 | - Submitting a fix 7 | - Proposing new features 8 | 9 | ## We Develop with GitHub 10 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 11 | 12 | ## We Use [GitHub Flow](https://docs.github.com/en/get-started/quickstart/github-flow), So All Code Changes Happen Through Pull Requests 13 | Pull requests are the best way to propose changes to the codebase (we use [GitHub Flow](https://docs.github.com/en/get-started/quickstart/github-flow)). We actively welcome your pull requests: 14 | 15 | 1. Fork the repository and create your branch from `main`. 16 | 2. If you've added code that should be tested, add tests. 17 | 3. Ensure the test suite passes. 18 | 4. Ensure linters are happy, a CI pipeline will be run for your changes. 19 | 5. Submit that pull request! 20 | 21 | ## Report bugs using GitHub's [issues](https://github.com/boeing/config-file-validator/issues) 22 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/Boeing/config-file-validator/issues/new); 23 | 24 | ## Functional Testing 25 | Until we can automate the functional testing, you must run through the [functional testing](./functional_test.md) procedures before submitting your PR for review. 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24@sha256:d9db32125db0c3a680cfb7a1afcaefb89c898a075ec148fdc2f0f646cc2ed509 AS go-builder 2 | ARG VALIDATOR_VERSION=unknown 3 | COPY . /build/ 4 | WORKDIR /build 5 | RUN CGO_ENABLED=0 \ 6 | GOOS=linux \ 7 | GOARCH=amd64 \ 8 | go build \ 9 | -ldflags="-w -s -extldflags '-static' -X github.com/Boeing/config-file-validator.version=$VALIDATOR_VERSION" \ 10 | -tags netgo \ 11 | -o validator \ 12 | cmd/validator/validator.go 13 | 14 | FROM alpine:3.22@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715 15 | USER user 16 | COPY --from=go-builder /build/validator / 17 | HEALTHCHECK NONE 18 | ENTRYPOINT [ "/validator" ] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Clayton Kehoe 2 | # Contributor : wiz64 3 | pkgname=config-file-validator 4 | pkgver=1.8.0 5 | pkgrel=1 6 | pkgdesc="A tool to validate the syntax of configuration files" 7 | arch=('x86_64') 8 | url="https://github.com/Boeing/config-file-validator" 9 | license=('Apache 2.0') 10 | depends=('glibc') 11 | makedepends=('go>=1.21' 'git' 'sed') 12 | source=("git+https://github.com/Boeing/config-file-validator.git") 13 | sha256sums=('SKIP') 14 | md5sums=('SKIP') 15 | 16 | pkgver() { 17 | cd "$srcdir/$pkgname" 18 | git describe --tags --abbrev=0 | sed 's/^v//' 19 | } 20 | 21 | build() { 22 | cd "$srcdir/$pkgname" 23 | CGO_ENABLED=0 \ 24 | GOOS=linux \ 25 | GOARCH=amd64 \ 26 | go build \ 27 | -ldflags="-w -s -extldflags '-static' \ 28 | -X github.com/Boeing/config-file-validator.version=$pkgver" \ 29 | -tags netgo \ 30 | -o validator \ 31 | cmd/validator/validator.go 32 | } 33 | 34 | package() { 35 | cd "$srcdir/$pkgname" 36 | install -Dm755 validator "$pkgdir/usr/bin/validator" 37 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Config File Validator

4 |

Single cross-platform CLI tool to validate different configuration file types

5 |
6 | 7 |

8 | Code Coverage 9 | 10 | 11 | OpenSSF Scorecard 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Apache 2 License 20 | 21 | 22 | 23 | Awesome Go 24 | 25 | 26 | 27 | Go Reference 28 | 29 | 30 | 31 | Go Report Card 32 | 33 | 34 | 35 | Pipeline Status 36 | 37 |

38 | 39 | ## Supported config files formats: 40 | * Apple PList XML 41 | * CSV 42 | * EDITORCONFIG 43 | * ENV 44 | * HCL 45 | * HOCON 46 | * INI 47 | * JSON 48 | * Properties 49 | * TOML 50 | * XML 51 | * YAML 52 | 53 | ## Demo 54 | 55 | demo 56 | 57 | ## Installation 58 | There are several ways to install the config file validator tool 59 | 60 | ### Binary Releases 61 | Download and unpack from https://github.com/Boeing/config-file-validator/releases 62 | 63 | ### Aqua 64 | You can install the validator using [aqua](https://aquaproj.github.io/). 65 | 66 | ``` 67 | aqua g -i Boeing/config-file-validator 68 | ``` 69 | 70 | ### Scoop 71 | You can install the validator using [Scoop](https://scoop.sh/). 72 | 73 | ``` 74 | scoop install config-file-validator 75 | ``` 76 | 77 | ### Arch Linux 78 | We release an [AUR package](https://aur.archlinux.org/packages/config-file-validator) for the config-file-validator 79 | 80 | ``` 81 | git clone https://aur.archlinux.org/config-file-validator.git 82 | cd config-file-validator 83 | makepkg -si 84 | ``` 85 | 86 | ### `go install` 87 | If you have a go environment on your desktop you can use [go install](https://go.dev/doc/go-get-install-deprecation) to install the validator executable. The validator executable will be installed to the directory named by the GOBIN environment variable, which defaults to $GOPATH/bin or $HOME/go/bin if the GOPATH environment variable is not set. 88 | 89 | ``` 90 | go install github.com/Boeing/config-file-validator/cmd/validator@v1.8.0 91 | ``` 92 | 93 | ## Usage 94 | ``` 95 | Usage: validator [OPTIONS] [...] 96 | 97 | positional arguments: 98 | search_path: The search path on the filesystem for configuration files. Defaults to the current working directory if no search_path provided 99 | 100 | optional flags: 101 | -depth int 102 | Depth of recursion for the provided search paths. Set depth to 0 to disable recursive path traversal 103 | -exclude-dirs string 104 | Subdirectories to exclude when searching for configuration files 105 | -exclude-file-types string 106 | A comma separated list of file types to ignore 107 | -globbing 108 | If globbing flag is set, check for glob patterns in the arguments. 109 | -groupby string 110 | Group output by filetype, directory, pass-fail. Supported for Standard and JSON reports 111 | -quiet 112 | If quiet flag is set. It doesn't print any output to stdout. 113 | -reporter value 114 | A string representing report format and optional output file path separated by colon if present. 115 | Usage: --reporter : 116 | Multiple reporters can be specified: --reporter json:file_path.json --reporter junit:another_file_path.xml 117 | Omit the file path to output to stdout: --reporter json or explicitly specify stdout using "-": --reporter json:- 118 | Supported formats: standard, json, junit, and sarif (default: "standard") 119 | -version 120 | Version prints the release version of validator 121 | ``` 122 | 123 | ### Environment Variables 124 | 125 | The config-file-validator supports setting options via environment variables. If both command-line flags and environment variables are set, the command-line flags will take precedence. The supported environment variables are as follows: 126 | 127 | | Environment Variable | Equivalent Flag | 128 | |----------------------|-----------------| 129 | | `CFV_DEPTH` | `-depth` | 130 | | `CFV_EXCLUDE_DIRS` | `-exclude-dirs` | 131 | | `CFV_EXCLUDE_FILE_TYPES` | `-exclude-file-types` | 132 | | `CFV_REPORTER` | `-reporter` | 133 | | `CFV_GROUPBY` | `-groupby` | 134 | | `CFV_QUIET` | `-quiet` | 135 | | `CFV_GLOBBING` | `-globbing` | 136 | 137 | ### Examples 138 | #### Standard Run 139 | If the search path is omitted it will search the current directory 140 | ``` 141 | validator /path/to/search 142 | ``` 143 | 144 | ![Standard Run](./img/standard_run.gif) 145 | 146 | #### Multiple search paths 147 | Multiple search paths are supported, and the results will be merged into a single report 148 | ``` 149 | validator /path/to/search /another/path/to/search 150 | ``` 151 | 152 | ![Multiple Search Paths Run](./img/multiple_paths.gif) 153 | 154 | #### Exclude directories 155 | Exclude subdirectories in the search path 156 | 157 | ``` 158 | validator --exclude-dirs=/path/to/search/tests /path/to/search 159 | ``` 160 | 161 | ![Exclude Dirs Run](./img/exclude_dirs.gif) 162 | 163 | #### Exclude file types 164 | Exclude file types in the search path. Available file types are `csv`, `env`, `hcl`, `hocon`, `ini`, `json`, `plist`, `properties`, `toml`, `xml`, `yaml`, and `yml` 165 | 166 | ``` 167 | validator --exclude-file-types=json /path/to/search 168 | ``` 169 | 170 | ![Exclude File Types Run](./img/exclude_file_types.gif) 171 | 172 | #### Customize recursion depth 173 | By default there is no recursion limit. If desired, the recursion depth can be set to an integer value. If depth is set to `0` recursion will be disabled and only the files in the search path will be validated. 174 | 175 | ``` 176 | validator --depth=0 /path/to/search 177 | ``` 178 | 179 | ![Custom Recursion Run](./img/custom_recursion.gif) 180 | 181 | #### Customize report output 182 | You can customize the report output and save the results to a file (default name is result.{extension}). The available report types are `standard`, `junit`, `json`, and `sarif`. You can specify multiple report types by chaining the `--reporter` flags. 183 | 184 | You can specify a path to an output file for any reporter by appending `:` the the name of the reporter. Providing an output file is optional and the results will be printed to stdout by default. To explicitly direct the output to stdout, use `:-` as the file path. 185 | 186 | ``` 187 | validator --reporter=json:- /path/to/search 188 | validator --reporter=json:output.json --reporter=standard /path/to/search 189 | ``` 190 | 191 | ![Exclude File Types Run](./img/custom_reporter.gif) 192 | 193 | ### Group report output 194 | Group the report output by file type, directory, or pass-fail. Supports one or more groupings. 195 | 196 | ``` 197 | validator -groupby filetype 198 | ``` 199 | 200 | ![Groupby File Type](./img/gb-filetype.gif) 201 | 202 | #### Multiple groups 203 | ``` 204 | validator -groupby directory,pass-fail 205 | ``` 206 | 207 | ![Groupby File Type and Pass/Fail](./img/gb-filetype-and-pass-fail.gif) 208 | 209 | ### Suppress output 210 | Passing the `--quiet` flag suppresses all output to stdout. If there are invalid config files the validator tool will exit with 1. Any errors in execution such as an invalid path will still be displayed. 211 | 212 | ``` 213 | validator --quiet /path/to/search 214 | ``` 215 | 216 | ### Search files using a glob pattern 217 | 218 | Use the `-globbing` flag to validate files matching a specified pattern. Include the pattern as a positional argument in double quotes. Multiple glob patterns and direct file paths are supported. If invalid config files are detected, the validator tool exits with code 1, and errors (e.g., invalid patterns) are displayed. 219 | 220 | [Learn more about glob patterns](https://www.digitalocean.com/community/tools/glob) 221 | 222 | ``` 223 | # Validate all `.json` files in a directory 224 | validator -globbing "/path/to/files/*.json" 225 | 226 | # Recursively validate all `.json` files in subdirectories 227 | validator -globbing "/path/to/files/**/*.json" 228 | 229 | # Mix glob patterns and paths 230 | validator -globbing "/path/*.json" /path/to/search 231 | ``` 232 | 233 | ## Build 234 | The project can be downloaded and built from source using an environment with Go 1.21+ installed. After a successful build, the binary can be moved to a location on your operating system PATH. 235 | 236 | ### macOS 237 | #### Build 238 | ``` 239 | CGO_ENABLED=0 \ 240 | GOOS=darwin \ 241 | GOARCH=amd64 \ # for Apple Silicon use arm64 242 | go build \ 243 | -ldflags='-w -s -extldflags "-static"' \ 244 | -tags netgo \ 245 | -o validator \ 246 | cmd/validator/validator.go 247 | ``` 248 | 249 | #### Install 250 | ``` 251 | cp ./validator /usr/local/bin/ 252 | chmod +x /usr/local/bin/validator 253 | ``` 254 | 255 | ### Linux 256 | #### Build 257 | ``` 258 | CGO_ENABLED=0 \ 259 | GOOS=linux \ 260 | GOARCH=amd64 \ 261 | go build \ 262 | -ldflags='-w -s -extldflags "-static"' \ 263 | -tags netgo \ 264 | -o validator \ 265 | cmd/validator/validator.go 266 | ``` 267 | 268 | #### Install 269 | ``` 270 | cp ./validator /usr/local/bin/ 271 | chmod +x /usr/local/bin/validator 272 | ``` 273 | 274 | ### Windows 275 | #### Build 276 | ``` 277 | CGO_ENABLED=0 \ 278 | GOOS=windows \ 279 | GOARCH=amd64 \ 280 | go build \ 281 | -ldflags='-w -s -extldflags "-static"' \ 282 | -tags netgo \ 283 | -o validator.exe \ 284 | cmd/validator/validator.go 285 | ``` 286 | 287 | #### Install 288 | ```powershell 289 | mkdir -p 'C:\Program Files\validator' 290 | cp .\validator.exe 'C:\Program Files\validator' 291 | [Environment]::SetEnvironmentVariable("C:\Program Files\validator", $env:Path, [System.EnvironmentVariableTarget]::Machine) 292 | ``` 293 | 294 | ### Docker 295 | You can also use the provided Dockerfile to build the config file validator tool as a container 296 | 297 | ``` 298 | docker build . -t config-file-validator:v1.8.0 299 | ``` 300 | 301 | ## Contributors 302 | 303 | 304 | 305 | 306 | ## Contributing 307 | We welcome contributions! Please refer to our [contributing guide](./CONTRIBUTING.md) 308 | 309 | ## License 310 | The Config File Validator is released under the [Apache 2.0](./LICENSE) License 311 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | The config-file-validator admins and community take security bugs in the config-file-validator project seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. 4 | 5 | To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/boeing/config-file-validator/security/advisories/new) tab. 6 | 7 | The config-file-validator admins will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. 8 | 9 | Report security bugs in third-party modules to the person or team maintaining the module. -------------------------------------------------------------------------------- /cmd/validator/validator.go: -------------------------------------------------------------------------------- 1 | /* 2 | Validator recursively scans a directory to search for configuration files and 3 | validates them using the go package for each configuration type. 4 | 5 | Currently Apple PList XML, CSV, HCL, HOCON, INI, JSON, Properties, TOML, XML, and YAML. 6 | configuration file types are supported. 7 | 8 | Usage: validator [OPTIONS] [...] 9 | 10 | positional arguments: 11 | search_path: The search path on the filesystem for configuration files. Defaults to the current working directory if no search_path provided. Multiple search paths can be declared separated by a space. 12 | 13 | optional flags: 14 | -depth int 15 | Depth of recursion for the provided search paths. Set depth to 0 to disable recursive path traversal 16 | -exclude-dirs string 17 | Subdirectories to exclude when searching for configuration files 18 | -exclude-file-types string 19 | A comma separated list of file types to ignore 20 | -globbing bool 21 | Set globbing to true to enable pattern matching for search paths 22 | -reporter string 23 | A string representing report format and optional output file path separated by colon if present. 24 | Usage: --reporter : 25 | Multiple reporters can be specified: --reporter json:file_path.json --reporter junit:another_file_path.xml 26 | Omit the file path to output to stdout: --reporter json or explicitly specify stdout using "-": --reporter json:- 27 | Supported formats: standard, json, junit, and sarif (default: "standard") 28 | -version 29 | Version prints the release version of validator 30 | */ 31 | 32 | package main 33 | 34 | import ( 35 | "errors" 36 | "flag" 37 | "fmt" 38 | "log" 39 | "os" 40 | "slices" 41 | "sort" 42 | "strings" 43 | 44 | "github.com/bmatcuk/doublestar/v4" 45 | 46 | configfilevalidator "github.com/Boeing/config-file-validator" 47 | "github.com/Boeing/config-file-validator/pkg/cli" 48 | "github.com/Boeing/config-file-validator/pkg/filetype" 49 | "github.com/Boeing/config-file-validator/pkg/finder" 50 | "github.com/Boeing/config-file-validator/pkg/misc" 51 | "github.com/Boeing/config-file-validator/pkg/reporter" 52 | ) 53 | 54 | type validatorConfig struct { 55 | searchPaths []string 56 | excludeDirs *string 57 | excludeFileTypes *string 58 | reportType map[string]string 59 | depth *int 60 | versionQuery *bool 61 | groupOutput *string 62 | quiet *bool 63 | globbing *bool 64 | } 65 | 66 | type reporterFlags []string 67 | 68 | func (rf *reporterFlags) String() string { 69 | return fmt.Sprint(*rf) 70 | } 71 | 72 | func (rf *reporterFlags) Set(value string) error { 73 | *rf = append(*rf, value) 74 | return nil 75 | } 76 | 77 | // Custom Usage function to cover 78 | func validatorUsage() { 79 | fmt.Printf("Usage: validator [OPTIONS] [...]\n\n") 80 | fmt.Printf("positional arguments:\n") 81 | fmt.Printf( 82 | " search_path: The search path on the filesystem for configuration files. " + 83 | "Defaults to the current working directory if no search_path provided\n\n") 84 | fmt.Printf("optional flags:\n") 85 | flag.PrintDefaults() 86 | } 87 | 88 | // Assemble pretty formatted list of file types 89 | func getFileTypes() []string { 90 | options := make([]string, 0, len(filetype.FileTypes)) 91 | for _, typ := range filetype.FileTypes { 92 | for extName := range typ.Extensions { 93 | options = append(options, extName) 94 | } 95 | } 96 | sort.Strings(options) 97 | return options 98 | } 99 | 100 | // Given a slice of strings, validates each is a valid file type 101 | func validateFileTypeList(input []string) bool { 102 | types := getFileTypes() 103 | for _, t := range input { 104 | if len(t) == 0 { 105 | continue 106 | } 107 | if !slices.Contains(types, t) { 108 | return false 109 | } 110 | } 111 | return true 112 | } 113 | 114 | // Parses, validates, and returns the flags 115 | // flag.String returns a pointer 116 | // If a required parameter is missing the help 117 | // output will be displayed and the function 118 | // will return with exit = 1 119 | func getFlags() (validatorConfig, error) { 120 | flag.Usage = validatorUsage 121 | reporterConfigFlags := reporterFlags{} 122 | 123 | var ( 124 | depthPtr = flag.Int("depth", 0, "Depth of recursion for the provided search paths. Set depth to 0 to disable recursive path traversal") 125 | excludeDirsPtr = flag.String("exclude-dirs", "", "Subdirectories to exclude when searching for configuration files") 126 | excludeFileTypesPtr = flag.String("exclude-file-types", "", "A comma separated list of file types to ignore") 127 | versionPtr = flag.Bool("version", false, "Version prints the release version of validator") 128 | groupOutputPtr = flag.String("groupby", "", "Group output by filetype, directory, pass-fail. Supported for Standard and JSON reports") 129 | quietPtr = flag.Bool("quiet", false, "If quiet flag is set. It doesn't print any output to stdout.") 130 | globbingPrt = flag.Bool("globbing", false, "If globbing flag is set, check for glob patterns in the arguments.") 131 | ) 132 | flag.Var( 133 | &reporterConfigFlags, 134 | "reporter", 135 | `A string representing report format and optional output file path separated by colon if present. 136 | Usage: --reporter : 137 | Multiple reporters can be specified: --reporter json:file_path.json --reporter junit:another_file_path.xml 138 | Omit the file path to output to stdout: --reporter json or explicitly specify stdout using "-": --reporter json:- 139 | Supported formats: standard, json, junit, and sarif (default: "standard")`, 140 | ) 141 | 142 | flag.Parse() 143 | 144 | err := applyDefaultFlagsFromEnv() 145 | if err != nil { 146 | return validatorConfig{}, err 147 | } 148 | 149 | reporterConf, err := parseReporterFlags(reporterConfigFlags) 150 | if err != nil { 151 | return validatorConfig{}, err 152 | } 153 | 154 | if err := validateGlobbing(globbingPrt); err != nil { 155 | return validatorConfig{}, err 156 | } 157 | 158 | searchPaths, err := parseSearchPath(globbingPrt) 159 | if err != nil { 160 | return validatorConfig{}, err 161 | } 162 | 163 | err = validateReporterConf(reporterConf, groupOutputPtr) 164 | if err != nil { 165 | return validatorConfig{}, err 166 | } 167 | 168 | if depthPtr != nil && isFlagSet("depth") && *depthPtr < 0 { 169 | return validatorConfig{}, errors.New("Wrong parameter value for depth, value cannot be negative") 170 | } 171 | 172 | if *excludeFileTypesPtr != "" { 173 | *excludeFileTypesPtr = strings.ToLower(*excludeFileTypesPtr) 174 | if !validateFileTypeList(strings.Split(*excludeFileTypesPtr, ",")) { 175 | return validatorConfig{}, errors.New("Invalid exclude file type") 176 | } 177 | } 178 | 179 | err = validateGroupByConf(groupOutputPtr) 180 | if err != nil { 181 | return validatorConfig{}, err 182 | } 183 | 184 | config := validatorConfig{ 185 | searchPaths, 186 | excludeDirsPtr, 187 | excludeFileTypesPtr, 188 | reporterConf, 189 | depthPtr, 190 | versionPtr, 191 | groupOutputPtr, 192 | quietPtr, 193 | globbingPrt, 194 | } 195 | 196 | return config, nil 197 | } 198 | 199 | func validateReporterConf(conf map[string]string, groupBy *string) error { 200 | acceptedReportTypes := map[string]bool{"standard": true, "json": true, "junit": true, "sarif": true} 201 | groupOutputReportTypes := map[string]bool{"standard": true, "json": true} 202 | 203 | for reportType := range conf { 204 | _, ok := acceptedReportTypes[reportType] 205 | if !ok { 206 | return errors.New("Wrong parameter value for reporter, only supports standard, json, junit, or sarif") 207 | } 208 | 209 | if !groupOutputReportTypes[reportType] && groupBy != nil && *groupBy != "" { 210 | return errors.New("Wrong parameter value for reporter, groupby is only supported for standard and JSON reports") 211 | } 212 | } 213 | 214 | return nil 215 | } 216 | 217 | func validateGroupByConf(groupBy *string) error { 218 | groupByCleanString := cleanString("groupby") 219 | groupByUserInput := strings.Split(groupByCleanString, ",") 220 | groupByAllowedValues := []string{"filetype", "directory", "pass-fail"} 221 | seenValues := make(map[string]bool) 222 | 223 | // Check that the groupby values are valid and not duplicates 224 | if groupBy != nil && isFlagSet("groupby") { 225 | for _, groupBy := range groupByUserInput { 226 | if !slices.Contains(groupByAllowedValues, groupBy) { 227 | return errors.New("Wrong parameter value for groupby, only supports filetype, directory, pass-fail") 228 | } 229 | if _, ok := seenValues[groupBy]; ok { 230 | return errors.New("Wrong parameter value for groupby, duplicate values are not allowed") 231 | } 232 | seenValues[groupBy] = true 233 | } 234 | } 235 | 236 | return nil 237 | } 238 | 239 | func validateGlobbing(globbingPrt *bool) error { 240 | if *globbingPrt && (isFlagSet("exclude-dirs") || isFlagSet("exclude-file-types")) { 241 | return errors.New("the -globbing flag cannot be used with --exclude-dirs or --exclude-file-types") 242 | } 243 | return nil 244 | } 245 | 246 | func parseSearchPath(globbingPrt *bool) ([]string, error) { 247 | searchPaths := make([]string, 0) 248 | 249 | if flag.NArg() == 0 { 250 | searchPaths = append(searchPaths, ".") 251 | } else if *globbingPrt { 252 | return handleGlobbing(searchPaths) 253 | } else { 254 | searchPaths = append(searchPaths, flag.Args()...) 255 | } 256 | 257 | return searchPaths, nil 258 | } 259 | 260 | func handleGlobbing(searchPaths []string) ([]string, error) { 261 | for _, flagArg := range flag.Args() { 262 | if isGlobPattern(flagArg) { 263 | matches, err := doublestar.Glob(os.DirFS("."), flagArg) 264 | if err != nil { 265 | return nil, errors.New("Glob matching error") 266 | } 267 | searchPaths = append(searchPaths, matches...) 268 | } else { 269 | searchPaths = append(searchPaths, flagArg) 270 | } 271 | } 272 | return searchPaths, nil 273 | } 274 | 275 | func parseReporterFlags(flags reporterFlags) (map[string]string, error) { 276 | conf := make(map[string]string) 277 | for _, reportFlag := range flags { 278 | parts := strings.Split(reportFlag, ":") 279 | switch len(parts) { 280 | case 1: 281 | conf[parts[0]] = "" 282 | case 2: 283 | if parts[1] == "-" { 284 | conf[parts[0]] = "" 285 | } else { 286 | conf[parts[0]] = parts[1] 287 | } 288 | default: 289 | return nil, errors.New("Wrong parameter value format for reporter, expected format is `report_type:optional_file_path`") 290 | } 291 | } 292 | 293 | if len(conf) == 0 { 294 | conf["standard"] = "" 295 | } 296 | 297 | return conf, nil 298 | } 299 | 300 | // isFlagSet verifies if a given flag has been set or not 301 | func isFlagSet(flagName string) bool { 302 | var isSet bool 303 | 304 | flag.Visit(func(f *flag.Flag) { 305 | if f.Name == flagName { 306 | isSet = true 307 | } 308 | }) 309 | 310 | return isSet 311 | } 312 | 313 | func applyDefaultFlagsFromEnv() error { 314 | flagsEnvMap := map[string]string{ 315 | "depth": "CFV_DEPTH", 316 | "exclude-dirs": "CFV_EXCLUDE_DIRS", 317 | "exclude-file-types": "CFV_EXCLUDE_FILE_TYPES", 318 | "reporter": "CFV_REPORTER", 319 | "groupby": "CFV_GROUPBY", 320 | "quiet": "CFV_QUIET", 321 | "globbing": "CFV_GLOBBING", 322 | } 323 | 324 | for flagName, envVar := range flagsEnvMap { 325 | if err := setFlagFromEnvIfNotSet(flagName, envVar); err != nil { 326 | return err 327 | } 328 | } 329 | 330 | return nil 331 | } 332 | 333 | func setFlagFromEnvIfNotSet(flagName string, envVar string) error { 334 | if isFlagSet(flagName) { 335 | return nil 336 | } 337 | 338 | if envVarValue, ok := os.LookupEnv(envVar); ok { 339 | if err := flag.Set(flagName, envVarValue); err != nil { 340 | return err 341 | } 342 | } 343 | 344 | return nil 345 | } 346 | 347 | // Return the reporter associated with the 348 | // reportType string 349 | func getReporter(reportType, outputDest *string) reporter.Reporter { 350 | switch *reportType { 351 | case "junit": 352 | return reporter.NewJunitReporter(*outputDest) 353 | case "json": 354 | return reporter.NewJSONReporter(*outputDest) 355 | case "sarif": 356 | return reporter.NewSARIFReporter(*outputDest) 357 | default: 358 | return reporter.NewStdoutReporter(*outputDest) 359 | } 360 | } 361 | 362 | // cleanString takes a command string and a split string 363 | // and returns a cleaned string 364 | func cleanString(command string) string { 365 | cleanedString := flag.Lookup(command).Value.String() 366 | cleanedString = strings.ToLower(cleanedString) 367 | cleanedString = strings.TrimSpace(cleanedString) 368 | 369 | return cleanedString 370 | } 371 | 372 | // Function to check if a string is a glob pattern 373 | func isGlobPattern(s string) bool { 374 | return strings.ContainsAny(s, "*?[]") 375 | } 376 | 377 | func mainInit() int { 378 | validatorConfig, err := getFlags() 379 | if err != nil { 380 | fmt.Println(err.Error()) 381 | flag.Usage() 382 | return 1 383 | } 384 | 385 | if *validatorConfig.versionQuery { 386 | fmt.Println(configfilevalidator.GetVersion()) 387 | return 0 388 | } 389 | 390 | // since the exclude dirs are a comma separated string 391 | // it needs to be split into a slice of strings 392 | excludeDirs := strings.Split(*validatorConfig.excludeDirs, ",") 393 | 394 | chosenReporters := make([]reporter.Reporter, 0) 395 | for reportType, outputFile := range validatorConfig.reportType { 396 | rt, of := reportType, outputFile // avoid "Implicit memory aliasing in for loop" 397 | chosenReporters = append(chosenReporters, getReporter(&rt, &of)) 398 | } 399 | 400 | excludeFileTypes := getExcludeFileTypes(*validatorConfig.excludeFileTypes) 401 | groupOutput := strings.Split(*validatorConfig.groupOutput, ",") 402 | fsOpts := []finder.FSFinderOptions{ 403 | finder.WithPathRoots(validatorConfig.searchPaths...), 404 | finder.WithExcludeDirs(excludeDirs), 405 | finder.WithExcludeFileTypes(excludeFileTypes), 406 | } 407 | quiet := *validatorConfig.quiet 408 | 409 | if validatorConfig.depth != nil && isFlagSet("depth") { 410 | fsOpts = append(fsOpts, finder.WithDepth(*validatorConfig.depth)) 411 | } 412 | 413 | // Initialize a file system finder 414 | fileSystemFinder := finder.FileSystemFinderInit(fsOpts...) 415 | 416 | // Initialize the CLI 417 | c := cli.Init( 418 | cli.WithReporters(chosenReporters...), 419 | cli.WithFinder(fileSystemFinder), 420 | cli.WithGroupOutput(groupOutput), 421 | cli.WithQuiet(quiet), 422 | ) 423 | 424 | // Run the config file validation 425 | exitStatus, err := c.Run() 426 | if err != nil { 427 | log.Printf("An error occurred during CLI execution: %v", err) 428 | } 429 | 430 | return exitStatus 431 | } 432 | 433 | func getExcludeFileTypes(configExcludeFileTypes string) []string { 434 | excludeFileTypes := strings.Split(strings.ToLower(configExcludeFileTypes), ",") 435 | uniqueFileTypes := misc.ArrToMap(excludeFileTypes...) 436 | 437 | for _, ft := range filetype.FileTypes { 438 | for ext := range ft.Extensions { 439 | if _, ok := uniqueFileTypes[ext]; !ok { 440 | continue 441 | } 442 | 443 | for ext := range ft.Extensions { 444 | uniqueFileTypes[ext] = struct{}{} 445 | } 446 | break 447 | } 448 | } 449 | 450 | excludeFileTypes = make([]string, 0, len(uniqueFileTypes)) 451 | for ft := range uniqueFileTypes { 452 | excludeFileTypes = append(excludeFileTypes, ft) 453 | } 454 | 455 | return excludeFileTypes 456 | } 457 | 458 | func main() { 459 | os.Exit(mainInit()) 460 | } 461 | -------------------------------------------------------------------------------- /cmd/validator/validator_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_flags(t *testing.T) { 13 | // We manipuate the Args to set them up for the testcases 14 | // After this test we restore the initial args 15 | oldArgs := os.Args 16 | defer func() { os.Args = oldArgs }() 17 | cases := []struct { 18 | Name string 19 | Args []string 20 | ExpectedExit int 21 | }{ 22 | {"blank", []string{}, 0}, 23 | {"negative depth set", []string{"-depth=-1", "."}, 1}, 24 | {"depth set", []string{"-depth=1", "."}, 0}, 25 | {"flags set, wrong reporter", []string{"--exclude-dirs=subdir", "--reporter=wrong", "."}, 1}, 26 | {"flags set, json and junit reporter", []string{"--exclude-dirs=subdir", "--reporter=json:-", "--reporter=junit:-", "."}, 0}, 27 | {"flags set, json reporter", []string{"--exclude-dirs=subdir", "--reporter=json", "."}, 0}, 28 | {"flags set, junit reporter", []string{"--exclude-dirs=subdir", "--reporter=junit", "."}, 0}, 29 | {"flags set, sarif reporter", []string{"--exclude-dirs=subdir", "--reporter=sarif", "."}, 0}, 30 | {"bad path", []string{"/path/does/not/exit"}, 1}, 31 | {"exclude file types set", []string{"--exclude-file-types=json,yaml", "."}, 0}, 32 | {"multiple paths", []string{"../../test/fixtures/subdir/good.json", "../../test/fixtures/good.json"}, 0}, 33 | {"version", []string{"--version"}, 0}, 34 | {"output set", []string{"--reporter=json:../../test/output", "."}, 0}, 35 | {"output set with standard reporter", []string{"--reporter=standard:../../test/output", "."}, 0}, 36 | {"wrong output set with json reporter", []string{"--reporter", "json:/path/not/exist", "."}, 1}, 37 | {"incorrect reporter param format with json reporter", []string{"--reporter", "json:/path/not/exist:/some/other/non-existent/path", "."}, 1}, 38 | {"incorrect group", []string{"-groupby=badgroup", "."}, 1}, 39 | {"correct group", []string{"-groupby=directory", "."}, 0}, 40 | {"grouped junit", []string{"-groupby=directory", "--reporter=junit", "."}, 1}, 41 | {"grouped sarif", []string{"-groupby=directory", "--reporter=sarif", "."}, 1}, 42 | {"groupby duplicate", []string{"--groupby=directory,directory", "."}, 1}, 43 | {"quiet flag", []string{"--quiet=true", "."}, 0}, 44 | {"globbing flag set", []string{"--globbing=true", "."}, 0}, 45 | {"globbing flag with a pattern", []string{"--globbing=true", "../../test/**/[m-t]*.json"}, 0}, 46 | {"globbing flag with no matches", []string{"--globbing=true", "../../test/**/*.nomatch"}, 0}, 47 | {"globbing flag not set", []string{"test/**/*.json", "."}, 1}, 48 | {"globbing flag with exclude-dirs", []string{"-globbing", "--exclude-dirs=subdir", "test/**/*.json", "."}, 1}, 49 | {"globbing flag with exclude-file-types", []string{"-globbing", "--exclude-file-types=hcl", "test/**/*.json", "."}, 1}, 50 | } 51 | for _, tc := range cases { 52 | // this call is required because otherwise flags panics, 53 | // if args are set between flag.Parse call 54 | fmt.Printf("Testing args: %v = %v\n", tc.Name, tc.Args) 55 | flag.CommandLine = flag.NewFlagSet(tc.Name, flag.ExitOnError) 56 | // we need a value to set Args[0] to cause flag begins parsing at Args[1] 57 | os.Args = append([]string{tc.Name}, tc.Args...) 58 | actualExit := mainInit() 59 | if tc.ExpectedExit != actualExit { 60 | t.Errorf("Test Case %v: Wrong exit code, expected: %v, got: %v", tc.Name, tc.ExpectedExit, actualExit) 61 | } 62 | } 63 | } 64 | 65 | func Test_getExcludeFileTypes(t *testing.T) { 66 | type testCase struct { 67 | name string 68 | input string 69 | expectedExcludeFileTypes []string 70 | } 71 | 72 | tcases := []testCase{ 73 | { 74 | name: "exclude yaml", 75 | input: "yaml", 76 | expectedExcludeFileTypes: []string{"yaml", "yml"}, 77 | }, 78 | { 79 | name: "exclude yml", 80 | input: "yml", 81 | expectedExcludeFileTypes: []string{"yaml", "yml"}, 82 | }, 83 | { 84 | name: "exclude json", 85 | input: "json", 86 | expectedExcludeFileTypes: []string{"json"}, 87 | }, 88 | { 89 | name: "exclude json and yaml", 90 | input: "json,yaml", 91 | expectedExcludeFileTypes: []string{"json", "yaml", "yml"}, 92 | }, 93 | { 94 | name: "exclude jSon and YamL", 95 | input: "jSon,YamL", 96 | expectedExcludeFileTypes: []string{"json", "yaml", "yml"}, 97 | }, 98 | } 99 | 100 | for _, tcase := range tcases { 101 | t.Run(tcase.name, func(t *testing.T) { 102 | actual := getExcludeFileTypes(tcase.input) 103 | require.ElementsMatch(t, tcase.expectedExcludeFileTypes, actual) 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /functional_test.md: -------------------------------------------------------------------------------- 1 | # Functional Tests for the config-file-validator 2 | 3 | Manual procedures for functionally testing the config-file-validator. These will eventually be used as requirements for creating automated function tests 4 | 5 | ## Setup 6 | 7 | 1. Build the latest changes in a container `docker build . -t cfv:` tagging the local container with a tag that indicates what feature is being tested 8 | 2. Run the container and mount the test directory `docker run -it --rm -v $(pwd)/test:/test --entrypoint sh cfv:` 9 | 10 | ## Basic Functionality 11 | 12 | This section tests basic validator functionality 13 | 14 | | Test | Expected Result | Notes | 15 | | ---- | --------------- | ----- | 16 | | `cd /test && /validator` | 37 passed and 12 failed | | 17 | | `/validator /test` | 37 passed and 12 failed | | 18 | | `/validator /test/fixtures/subdir /test/fixtures/subdir2` | 3 passed and 12 failed | | 19 | | `/validator --help` | Help is displayed | | 20 | | `/validator --version` | Output should read "validator version unknown" | | 21 | | `/validator /badpath` | Error output should read: "An error occurred during CLI execution: Unable to find files: stat /badpath: no such file or directory" | | 22 | | `/validator -v` | Error "flag provided but not defined: -v" is displayed on the terminal in addition to the help output | | 23 | 24 | 25 | ## Reports 26 | 27 | This section validates report output 28 | 29 | | Test | Expected Result | Notes | 30 | | ---- | --------------- | ----- | 31 | | `cd /test && /validator --reporter=json:-` | JSON output is produced on the terminal and summary is `"summary": {"passed": 37,"failed": 12}` | | 32 | | `cd /test && /validator --reporter=standard:-` | Text output is produced on the screen | | 33 | | `cd /test && /validator --reporter=junit:-` | JUnit XML is produced on the terminal | | 34 | | `cd /test && /validator --reporter=sarif:-` | Sarif output is produced on the terminal | | 35 | | `cd /test && /validator --reporter=json:/json_report.json` | JSON is written to `/json_report.json` and summary in the contents of the file reads `"summary": {"passed": 37,"failed": 12}` | | 36 | | `cd /test && /validator --reporter=standard:/standard_report.txt` | Text is written to `/standard_report.txt` | | 37 | | `cd /test && /validator --reporter=junit:/junit_report.xml` | JUnit XML is written to `/junit_report.xml` | | 38 | | `cd /test && /validator --reporter=sarif:/sarif_report.sarif` | Sarif JSON is written to `/sarif_report.sarif` | This is currently failing as the sarif report is written to stdout in addition to the file | 39 | | `cd /test && /validator --reporter=json:/json_report_2.json --reporter=standard:-` | JSON is written to `/json_report_2.json` and standard text is output to the terminal | | 40 | | `cd /test && /validator --reporter=bad` | Error message "Wrong parameter value for reporter, only supports standard, json, junit, or sarif" should be displayed in addition to the help output | | 41 | | `cd /test && /validator --reporter=json --quiet` | Nothing is displayed to the terminal since the `--quiet` flag supresses the output | | 42 | | `cd /test && /validator --reporter=json:/json_report_3.json --quiet` | Nothing is displayed to the terminal since the `--quiet` flag supresses the output but the `/json_report_3.json` file is populated | | 43 | 44 | ## Grouping 45 | 46 | This section validates organization of the output 47 | 48 | | Test | Expected Result | Notes | 49 | | ---- | --------------- | ----- | 50 | | `cd /test && /validator --groupby=filetype` | Results are grouped by file type | | 51 | | `cd /test && /validator --groupby=pass-fail` | Results are grouped by pass/fail | Bug filed for duplicate summaries | 52 | | `cd /test && /validator --reporter=standard --groupby=pass-fail` | Results are grouped by pass/fail | Bug filed for duplicate summaries | 53 | | `cd /test && /validator --groupby=directory` | Results are grouped by directory | | 54 | | `cd /test && /validator --groupby=filetype,directory` | Results are grouped by file type, then directory | | 55 | | `cd /test && /validator --groupby=pass-fail,filetype` | Results are grouped by pass-fail, then filetype | | 56 | | `cd /test && /validator --groupby=pass-fail,directory` | Results are grouped by pass-fail, then directory | | 57 | | `cd /test && /validator --groupby=pass-fail,directory,filetype` | Results are grouped by pass-fail, then directory, then file type | | 58 | | `cd /test && /validator --groupby=pass-fail,pass-fail` | Error "Wrong parameter value for groupby, duplicate values are not allowed" is displayed with help output | | 59 | | `cd /test && /validator --reporter=json --groupby=filetype,directory` | JSON Results are grouped by file type, then directory | This does not work, bug filed | 60 | | `cd /test && /validator --reporter=junit --groupby=pass-fail` | Error "Wrong parameter value for reporter, groupby is only supported for standard and JSON reports" is displayed with help output | | 61 | | `cd /test && /validator --reporter=sarif --groupby=directory` | Error "Wrong parameter value for reporter, groupby is only supported for standard and JSON reports" is displayed with help output | | 62 | 63 | ## Depth 64 | | Test | Expected Result | Notes | 65 | | ---- | --------------- | ----- | 66 | | `cd / && /validator --depth=0 /test` | Nothing is displayed since there are no config files at the root of the `/test` directory and recursion is disabled with depth set to 0 | | 67 | | `cd / && /validator --depth=1 /test` | Files in `/test/fixtures/` are validated | | 68 | | `cd / && /validator --depth=2 /test` | Files in `/test/fixtures/*` directories are validated | | 69 | 70 | ## Globbing 71 | 72 | | Test | Expected Result | Notes | 73 | | ---- | --------------- | ----- | 74 | | `cd /test && /validator --globbing "fixtures/**/*.json"` | All json files in subdirectories are displayed | | 75 | | `cd /test && /validator --globbing "fixtures/**/*.json" /test/fixtures/subdir2` | All json files in subdirectories are displayed and other files in `/test/fixtures/subdir2` are displayed | | 76 | | `cd /test && /validator --groupby=pass-fail --globbing "fixtures/**/*.json"` | All json files in subdirectories are displayed and grouped by pass/fail | | 77 | | `cd /test && /validator --reporter=json --globbing "fixtures/**/*.json"` | All json files in subdirectories are displayed as a JSON report | | 78 | | `cd /test && /validator "fixtures/**/*.json"` | Error "An error occurred during CLI execution: Unable to find files: stat fixtures/**/*.json: no such file or directory" should be displayed when using glob patterns without flag enabling it | | 79 | | `cd /test && /validator --exclude-file-types=json --globbing "fixtures/**/*.json"` | Error "the -globbing flag cannot be used with --exclude-dirs or --exclude-file-types" is displayed | | 80 | | `cd /test && /validator --exclude-dirs=subdir2 --globbing "fixtures/**/*.json"` | Error "the -globbing flag cannot be used with --exclude-dirs or --exclude-file-types" is displayed | | 81 | 82 | 83 | ## Environment Variables 84 | 85 | Run `unset ` to unset the previous var before testing 86 | 87 | | Test | Expected Result | Notes | 88 | | ---- | --------------- | ----- | 89 | | `export CFV_REPORTER=json:- && /validator /test` | JSON output should display on the terminal | | 90 | | `export CFV_REPORTER=json:/test_env_var.json && /validator /test` | JSON output should be written to `/test_env_var.json` and NOT displayed on the terminal | | 91 | | `export CFV_GLOBBING=true && cd /test && /validator "fixtures/**/*.json"` | Results should include json files in all subdirectories for fixtures | This does not work and a bug has been filed | 92 | | `export CFV_QUIET=true && cd /test && /validator` | No output should be displayed on the terminal | | 93 | | `export CFV_QUIET=false && cd /test && /validator` | Output should be displayed on the terminal | | 94 | | `export CFV_GROUPBY=pass-fail && cd /test && /validator` | Output should be displayed on the terminal and grouped by pass-fail | | 95 | | `export CFV_DEPTH=0 && cd /test && /validator` | Output should only display config files at the root of `/test/fixtures` | | 96 | | `export CFV_EXCLUDE_DIRS=subdir2,subdir && cd /test && /validator` | `subdir` and `subdir2` directories should be excluded | | 97 | | `export CFV_EXCLUDE_FILE_TYPES=yml,yaml,json,toml,properties,hocon,csv,hcl,ini,env,plist,editorconfig,xml && cd /test && /validator` | No output since all types are excluded | | 98 | | `export CFV_EXCLUDE_FILE_TYPES=yml,yaml,json,toml,properties,hocon,csv,hcl,ini,env,plist,editorconfig && cd /test && /validator --exclude-file-types=""` | All config files should be displayed since the argument overrides the environment variable | | 99 | 100 | ## Exclude Dirs 101 | | Test | Expected Result | Notes | 102 | | ---- | --------------- | ----- | 103 | | `cd /test && /validator --exclude-dirs=baddir` | Non-existent subdirectory is ignored | | 104 | | `cd /test && /validator --exclude-dirs=subdir,subdir2` | `subdir` and `subdir2` directories are ignored | | 105 | | ` cd /test && /validator --exclude-dirs=test /` | `test` subdirectory is excluded from root directory search path `/` | | 106 | 107 | 108 | ## Exclude File Types 109 | 110 | | Test | Expected Result | Notes | 111 | | ---- | --------------- | ----- | 112 | | `cd /test && /validator --exclude-file-types=xml` | XML validation should be skipped | | 113 | | `cd /test && /validator --exclude-file-types=yml` | `.yaml` and `.yml` should be excluded from validation | | 114 | | `cd /test && /validator --exclude-file-types=YaML` | `.yaml` and `.yml` should be excluded from validation since argument values are not case sensitive | | 115 | 116 | 117 | ## Other Flags 118 | 119 | | Test | Expected Result | Notes | 120 | | ---- | --------------- | ----- | 121 | | `cd /test && /validator --quiet` | Nothing is displayed to the terminal since the `--quiet` flag supresses the output | | 122 | | `cd /test && /validator /badpath --quiet` | Error "An error occurred during CLI execution: Unable to find files: stat /badpath: no such file or directory" is output to the terminal even through `--quiet` was enabled | | 123 | 124 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Boeing/config-file-validator 2 | 3 | go 1.21.0 4 | 5 | require ( 6 | github.com/bmatcuk/doublestar/v4 v4.8.1 7 | github.com/editorconfig/editorconfig-core-go/v2 v2.6.2 8 | github.com/fatih/color v1.18.0 9 | github.com/gurkankaymak/hocon v1.2.21 10 | github.com/hashicorp/go-envparse v0.1.0 11 | github.com/hashicorp/hcl/v2 v2.23.0 12 | github.com/magiconair/properties v1.8.10 13 | github.com/pelletier/go-toml/v2 v2.2.4 14 | github.com/stretchr/testify v1.10.0 15 | gopkg.in/ini.v1 v1.67.0 16 | gopkg.in/yaml.v3 v3.0.1 17 | howett.net/plist v1.0.1 18 | ) 19 | 20 | require ( 21 | github.com/agext/levenshtein v1.2.1 // indirect 22 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect 23 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/mattn/go-colorable v0.1.13 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect 28 | github.com/pmezard/go-difflib v1.0.0 // indirect 29 | github.com/zclconf/go-cty v1.13.0 // indirect 30 | golang.org/x/mod v0.16.0 // indirect 31 | golang.org/x/sys v0.25.0 // indirect 32 | golang.org/x/text v0.11.0 // indirect 33 | golang.org/x/tools v0.13.0 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= 2 | github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 3 | github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= 4 | github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= 5 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= 6 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= 7 | github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= 8 | github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/editorconfig/editorconfig-core-go/v2 v2.6.2 h1:dKG8sc7n321deIVRcQtwlMNoBEra7j0qQ8RwxO8RN0w= 12 | github.com/editorconfig/editorconfig-core-go/v2 v2.6.2/go.mod h1:7dvD3GCm7eBw53xZ/lsiq72LqobdMg3ITbMBxnmJmqY= 13 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 14 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 15 | github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= 16 | github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 17 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 18 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 19 | github.com/gurkankaymak/hocon v1.2.21 h1:ykr1ptXWc4UfPjY5hT0hbRocLWTRAlVvPt1mYbuU+y4= 20 | github.com/gurkankaymak/hocon v1.2.21/go.mod h1:dQCfhnuDKlLqAZRGhFTd81HkAfMx7STHv0w2JkJ6iq4= 21 | github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY= 22 | github.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc= 23 | github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= 24 | github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= 25 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 26 | github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= 27 | github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 28 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 29 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 30 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 31 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 32 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 33 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= 34 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 35 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 36 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 37 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 40 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 41 | github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= 42 | github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= 43 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= 44 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= 45 | golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= 46 | golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 47 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 48 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 49 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 50 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 51 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 52 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 53 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= 54 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 55 | golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= 56 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 57 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 58 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 59 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 60 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 61 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= 62 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 63 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 64 | howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= 65 | howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= 66 | -------------------------------------------------------------------------------- /img/custom_recursion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Boeing/config-file-validator/fe4044f9d2449183d6e41ef6d65d383b1aad6748/img/custom_recursion.gif -------------------------------------------------------------------------------- /img/custom_reporter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Boeing/config-file-validator/fe4044f9d2449183d6e41ef6d65d383b1aad6748/img/custom_reporter.gif -------------------------------------------------------------------------------- /img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Boeing/config-file-validator/fe4044f9d2449183d6e41ef6d65d383b1aad6748/img/demo.gif -------------------------------------------------------------------------------- /img/docker_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Boeing/config-file-validator/fe4044f9d2449183d6e41ef6d65d383b1aad6748/img/docker_run.png -------------------------------------------------------------------------------- /img/exclude_dirs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Boeing/config-file-validator/fe4044f9d2449183d6e41ef6d65d383b1aad6748/img/exclude_dirs.gif -------------------------------------------------------------------------------- /img/exclude_file_types.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Boeing/config-file-validator/fe4044f9d2449183d6e41ef6d65d383b1aad6748/img/exclude_file_types.gif -------------------------------------------------------------------------------- /img/fork-me-on-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Boeing/config-file-validator/fe4044f9d2449183d6e41ef6d65d383b1aad6748/img/fork-me-on-github.png -------------------------------------------------------------------------------- /img/gb-filetype-and-pass-fail.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Boeing/config-file-validator/fe4044f9d2449183d6e41ef6d65d383b1aad6748/img/gb-filetype-and-pass-fail.gif -------------------------------------------------------------------------------- /img/gb-filetype.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Boeing/config-file-validator/fe4044f9d2449183d6e41ef6d65d383b1aad6748/img/gb-filetype.gif -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Boeing/config-file-validator/fe4044f9d2449183d6e41ef6d65d383b1aad6748/img/logo.png -------------------------------------------------------------------------------- /img/multiple_paths.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Boeing/config-file-validator/fe4044f9d2449183d6e41ef6d65d383b1aad6748/img/multiple_paths.gif -------------------------------------------------------------------------------- /img/standard_run.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Boeing/config-file-validator/fe4044f9d2449183d6e41ef6d65d383b1aad6748/img/standard_run.gif -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 8 | Fork me on GitHub 11 | 12 |
13 |
14 |
15 |

Config File Validator

16 |

Single cross-platform CLI tool to validate different configuration file types

17 |
18 |
19 |
20 | 21 |

22 | Code Coverage 23 | 24 | 25 | OpenSSF Scorecard 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Apache 2 License 34 | 35 | 36 | 37 | Awesome Go 38 | 39 | 40 | 41 | Go Reference 42 | 43 | 44 | 45 | Go Report Card 46 | 47 | 48 | 49 | Pipeline Status 50 | 51 |

52 | 53 | ## Supported config files formats: 54 | * Apple PList XML 55 | * CSV 56 | * EDITORCONFIG 57 | * ENV 58 | * HCL 59 | * HOCON 60 | * INI 61 | * JSON 62 | * Properties 63 | * TOML 64 | * XML 65 | * YAML 66 | 67 | ## Demo 68 | 69 | demo 70 | 71 | ## Installation 72 | There are several ways to install the config file validator tool 73 | 74 | ### Binary Releases 75 | Download and unpack from https://github.com/Boeing/config-file-validator/releases 76 | 77 | ### Aqua 78 | You can install the validator using [aqua](https://aquaproj.github.io/). 79 | 80 | ``` 81 | aqua g -i Boeing/config-file-validator 82 | ``` 83 | 84 | ### Scoop 85 | You can install the validator using [Scoop](https://scoop.sh/). 86 | 87 | ``` 88 | scoop install config-file-validator 89 | ``` 90 | 91 | ### Arch Linux 92 | We release an [AUR package](https://aur.archlinux.org/packages/config-file-validator) for the config-file-validator 93 | 94 | ``` 95 | git clone https://aur.archlinux.org/config-file-validator.git 96 | cd config-file-validator 97 | makepkg -si 98 | ``` 99 | 100 | ### `go install` 101 | If you have a go environment on your desktop you can use [go install](https://go.dev/doc/go-get-install-deprecation) to install the validator executable. The validator executable will be installed to the directory named by the GOBIN environment variable, which defaults to $GOPATH/bin or $HOME/go/bin if the GOPATH environment variable is not set. 102 | 103 | ``` 104 | go install github.com/Boeing/config-file-validator/cmd/validator@v1.8.0 105 | ``` 106 | 107 | ## Usage 108 | ``` 109 | Usage: validator [OPTIONS] [...] 110 | 111 | positional arguments: 112 | search_path: The search path on the filesystem for configuration files. Defaults to the current working directory if no search_path provided 113 | 114 | optional flags: 115 | -depth int 116 | Depth of recursion for the provided search paths. Set depth to 0 to disable recursive path traversal 117 | -exclude-dirs string 118 | Subdirectories to exclude when searching for configuration files 119 | -exclude-file-types string 120 | A comma separated list of file types to ignore 121 | -globbing 122 | If globbing flag is set, check for glob patterns in the arguments. 123 | -groupby string 124 | Group output by filetype, directory, pass-fail. Supported for Standard and JSON reports 125 | -quiet 126 | If quiet flag is set. It doesn't print any output to stdout. 127 | -reporter value 128 | A string representing report format and optional output file path separated by colon if present. 129 | Usage: --reporter : 130 | Multiple reporters can be specified: --reporter json:file_path.json --reporter junit:another_file_path.xml 131 | Omit the file path to output to stdout: --reporter json or explicitly specify stdout using "-": --reporter json:- 132 | Supported formats: standard, json, junit, and sarif (default: "standard") 133 | -version 134 | Version prints the release version of validator 135 | ``` 136 | 137 | ### Environment Variables 138 | 139 | The config-file-validator supports setting options via environment variables. If both command-line flags and environment variables are set, the command-line flags will take precedence. The supported environment variables are as follows: 140 | 141 | | Environment Variable | Equivalent Flag | 142 | |----------------------|-----------------| 143 | | `CFV_DEPTH` | `-depth` | 144 | | `CFV_EXCLUDE_DIRS` | `-exclude-dirs` | 145 | | `CFV_EXCLUDE_FILE_TYPES` | `-exclude-file-types` | 146 | | `CFV_REPORTER` | `-reporter` | 147 | | `CFV_GROUPBY` | `-groupby` | 148 | | `CFV_QUIET` | `-quiet` | 149 | | `CFV_GLOBBING` | `-globbing` | 150 | 151 | ### Examples 152 | #### Standard Run 153 | If the search path is omitted it will search the current directory 154 | ``` 155 | validator /path/to/search 156 | ``` 157 | 158 | ![Standard Run](./img/standard_run.gif) 159 | 160 | #### Multiple search paths 161 | Multiple search paths are supported, and the results will be merged into a single report 162 | ``` 163 | validator /path/to/search /another/path/to/search 164 | ``` 165 | 166 | ![Multiple Search Paths Run](./img/multiple_paths.gif) 167 | 168 | #### Exclude directories 169 | Exclude subdirectories in the search path 170 | 171 | ``` 172 | validator --exclude-dirs=/path/to/search/tests /path/to/search 173 | ``` 174 | 175 | ![Exclude Dirs Run](./img/exclude_dirs.gif) 176 | 177 | #### Exclude file types 178 | Exclude file types in the search path. Available file types are `csv`, `env`, `hcl`, `hocon`, `ini`, `json`, `plist`, `properties`, `toml`, `xml`, `yaml`, and `yml` 179 | 180 | ``` 181 | validator --exclude-file-types=json /path/to/search 182 | ``` 183 | 184 | ![Exclude File Types Run](./img/exclude_file_types.gif) 185 | 186 | #### Customize recursion depth 187 | By default there is no recursion limit. If desired, the recursion depth can be set to an integer value. If depth is set to `0` recursion will be disabled and only the files in the search path will be validated. 188 | 189 | ``` 190 | validator --depth=0 /path/to/search 191 | ``` 192 | 193 | ![Custom Recursion Run](./img/custom_recursion.gif) 194 | 195 | #### Customize report output 196 | You can customize the report output and save the results to a file (default name is result.{extension}). The available report types are `standard`, `junit`, `json`, and `sarif`. You can specify multiple report types by chaining the `--reporter` flags. 197 | 198 | You can specify a path to an output file for any reporter by appending `:` the the name of the reporter. Providing an output file is optional and the results will be printed to stdout by default. To explicitly direct the output to stdout, use `:-` as the file path. 199 | 200 | ``` 201 | validator --reporter=json:- /path/to/search 202 | validator --reporter=json:output.json --reporter=standard /path/to/search 203 | ``` 204 | 205 | ![Exclude File Types Run](./img/custom_reporter.gif) 206 | 207 | ### Group report output 208 | Group the report output by file type, directory, or pass-fail. Supports one or more groupings. 209 | 210 | ``` 211 | validator -groupby filetype 212 | ``` 213 | 214 | ![Groupby File Type](./img/gb-filetype.gif) 215 | 216 | #### Multiple groups 217 | ``` 218 | validator -groupby directory,pass-fail 219 | ``` 220 | 221 | ![Groupby File Type and Pass/Fail](./img/gb-filetype-and-pass-fail.gif) 222 | 223 | ### Output results to a file 224 | Output report results to a file (default name is `result.{extension}`). Must provide reporter flag with a supported extension format. Available options are `junit` and `json`. If an existing directory is provided, create a file named default name in the given directory. If a file name is provided, create a file named the given name at the current working directory. 225 | 226 | ``` 227 | validator --reporter=json --output=/path/to/dir 228 | ``` 229 | 230 | ### Suppress output 231 | Passing the `--quiet` flag suppresses all output to stdout. If there are invalid config files the validator tool will exit with 1. Any errors in execution such as an invalid path will still be displayed. 232 | 233 | ``` 234 | validator --quiet /path/to/search 235 | ``` 236 | 237 | ### Search files using a glob pattern 238 | 239 | Use the `-globbing` flag to validate files matching a specified pattern. Include the pattern as a positional argument in double quotes. Multiple glob patterns and direct file paths are supported. If invalid config files are detected, the validator tool exits with code 1, and errors (e.g., invalid patterns) are displayed. 240 | 241 | [Learn more about glob patterns](https://www.digitalocean.com/community/tools/glob) 242 | 243 | ``` 244 | # Validate all `.json` files in a directory 245 | validator -globbing "/path/to/files/*.json" 246 | 247 | # Recursively validate all `.json` files in subdirectories 248 | validator -globbing "/path/to/files/**/*.json" 249 | 250 | # Mix glob patterns and paths 251 | validator -globbing "/path/*.json" /path/to/search 252 | ``` 253 | 254 | ## Build 255 | The project can be downloaded and built from source using an environment with Go 1.21+ installed. After a successful build, the binary can be moved to a location on your operating system PATH. 256 | 257 | ### macOS 258 | #### Build 259 | ``` 260 | CGO_ENABLED=0 \ 261 | GOOS=darwin \ 262 | GOARCH=amd64 \ # for Apple Silicon use arm64 263 | go build \ 264 | -ldflags='-w -s -extldflags "-static"' \ 265 | -tags netgo \ 266 | -o validator \ 267 | cmd/validator/validator.go 268 | ``` 269 | 270 | #### Install 271 | ``` 272 | cp ./validator /usr/local/bin/ 273 | chmod +x /usr/local/bin/validator 274 | ``` 275 | 276 | ### Linux 277 | #### Build 278 | ``` 279 | CGO_ENABLED=0 \ 280 | GOOS=linux \ 281 | GOARCH=amd64 \ 282 | go build \ 283 | -ldflags='-w -s -extldflags "-static"' \ 284 | -tags netgo \ 285 | -o validator \ 286 | cmd/validator/validator.go 287 | ``` 288 | 289 | #### Install 290 | ``` 291 | cp ./validator /usr/local/bin/ 292 | chmod +x /usr/local/bin/validator 293 | ``` 294 | 295 | ### Windows 296 | #### Build 297 | ``` 298 | CGO_ENABLED=0 \ 299 | GOOS=windows \ 300 | GOARCH=amd64 \ 301 | go build \ 302 | -ldflags='-w -s -extldflags "-static"' \ 303 | -tags netgo \ 304 | -o validator.exe \ 305 | cmd/validator/validator.go 306 | ``` 307 | 308 | #### Install 309 | ```powershell 310 | mkdir -p 'C:\Program Files\validator' 311 | cp .\validator.exe 'C:\Program Files\validator' 312 | [Environment]::SetEnvironmentVariable("C:\Program Files\validator", $env:Path, [System.EnvironmentVariableTarget]::Machine) 313 | ``` 314 | 315 | ### Docker 316 | You can also use the provided Dockerfile to build the config file validator tool as a container 317 | 318 | ``` 319 | docker build . -t config-file-validator:v1.8.0 320 | ``` 321 | 322 | ## Contributors 323 | 324 | 325 | 326 | 327 | ## Contributing 328 | We welcome contributions! Please refer to our [contributing guide](./CONTRIBUTING.md) 329 | 330 | ## License 331 | The Config File Validator is released under the [Apache 2.0](./LICENSE) License 332 | -------------------------------------------------------------------------------- /pkg/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/Boeing/config-file-validator/pkg/finder" 8 | "github.com/Boeing/config-file-validator/pkg/reporter" 9 | ) 10 | 11 | // GroupOutput is a global variable that is used to 12 | // store the group by options that the user specifies 13 | var ( 14 | GroupOutput []string 15 | Quiet bool 16 | errorFound bool 17 | ) 18 | 19 | type CLI struct { 20 | // FileFinder interface to search for the files 21 | // in the SearchPath 22 | Finder finder.FileFinder 23 | // Reporter interface for outputting the results of 24 | // the CLI run 25 | Reporters []reporter.Reporter 26 | } 27 | 28 | // Implement the go options pattern to be able to 29 | // set options to the CLI struct using functional 30 | // programming 31 | type Option func(*CLI) 32 | 33 | // Set the CLI Finder 34 | func WithFinder(f finder.FileFinder) Option { 35 | return func(c *CLI) { 36 | c.Finder = f 37 | } 38 | } 39 | 40 | // Set the reporter types 41 | func WithReporters(r ...reporter.Reporter) Option { 42 | return func(c *CLI) { 43 | c.Reporters = r 44 | } 45 | } 46 | 47 | func WithGroupOutput(groupOutput []string) Option { 48 | return func(_ *CLI) { 49 | GroupOutput = groupOutput 50 | } 51 | } 52 | 53 | func WithQuiet(quiet bool) Option { 54 | return func(_ *CLI) { 55 | Quiet = quiet 56 | } 57 | } 58 | 59 | // Initialize the CLI object 60 | func Init(opts ...Option) *CLI { 61 | defaultFsFinder := finder.FileSystemFinderInit() 62 | defaultReporter := reporter.NewStdoutReporter("") 63 | 64 | cli := &CLI{ 65 | defaultFsFinder, 66 | []reporter.Reporter{defaultReporter}, 67 | } 68 | 69 | for _, opt := range opts { 70 | opt(cli) 71 | } 72 | 73 | return cli 74 | } 75 | 76 | // The Run method performs the following actions: 77 | // - Finds the calls the Find method from the Finder interface to 78 | // return a list of files 79 | // - Reads each file that was found 80 | // - Calls the Validate method from the Validator interface to validate the file 81 | // - Outputs the results using the Reporters 82 | func (c CLI) Run() (int, error) { 83 | errorFound = false 84 | var reports []reporter.Report 85 | foundFiles, err := c.Finder.Find() 86 | if err != nil { 87 | return 1, fmt.Errorf("Unable to find files: %w", err) 88 | } 89 | 90 | for _, fileToValidate := range foundFiles { 91 | // read it 92 | fileContent, err := os.ReadFile(fileToValidate.Path) 93 | if err != nil { 94 | return 1, fmt.Errorf("unable to read file: %w", err) 95 | } 96 | 97 | isValid, err := fileToValidate.FileType.Validator.Validate(fileContent) 98 | if !isValid { 99 | errorFound = true 100 | } 101 | report := reporter.Report{ 102 | FileName: fileToValidate.Name, 103 | FilePath: fileToValidate.Path, 104 | IsValid: isValid, 105 | ValidationError: err, 106 | IsQuiet: Quiet, 107 | } 108 | reports = append(reports, report) 109 | 110 | } 111 | 112 | err = c.printReports(reports) 113 | if err != nil { 114 | return 1, err 115 | } 116 | 117 | if errorFound { 118 | return 1, nil 119 | } 120 | 121 | return 0, nil 122 | } 123 | 124 | // printReports prints the reports based on the specified grouping and reporter type. 125 | // It returns any error encountered during the printing process. 126 | func (c CLI) printReports(reports []reporter.Report) error { 127 | if len(GroupOutput) == 1 && GroupOutput[0] != "" { 128 | return c.printGroupSingle(reports) 129 | } else if len(GroupOutput) == 2 { 130 | return c.printGroupDouble(reports) 131 | } else if len(GroupOutput) == 3 { 132 | return c.printGroupTriple(reports) 133 | } 134 | 135 | for _, reporterObj := range c.Reporters { 136 | err := reporterObj.Print(reports) 137 | if err != nil { 138 | fmt.Println("failed to report:", err) 139 | errorFound = true 140 | } 141 | } 142 | 143 | return nil 144 | } 145 | 146 | func (c CLI) printGroupSingle(reports []reporter.Report) error { 147 | reportGroup, err := GroupBySingle(reports, GroupOutput[0]) 148 | if err != nil { 149 | return fmt.Errorf("unable to group by single value: %w", err) 150 | } 151 | 152 | // Check reporter type to determine how to print 153 | for _, reporterObj := range c.Reporters { 154 | if _, ok := reporterObj.(*reporter.JSONReporter); ok { 155 | return reporter.PrintSingleGroupJSON(reportGroup) 156 | } 157 | } 158 | 159 | return reporter.PrintSingleGroupStdout(reportGroup) 160 | } 161 | 162 | func (c CLI) printGroupDouble(reports []reporter.Report) error { 163 | reportGroup, err := GroupByDouble(reports, GroupOutput) 164 | if err != nil { 165 | return fmt.Errorf("unable to group by double value: %w", err) 166 | } 167 | 168 | // Check reporter type to determine how to print 169 | for _, reporterObj := range c.Reporters { 170 | if _, ok := reporterObj.(*reporter.JSONReporter); ok { 171 | return reporter.PrintDoubleGroupJSON(reportGroup) 172 | } 173 | } 174 | 175 | return reporter.PrintDoubleGroupStdout(reportGroup) 176 | } 177 | 178 | func (c CLI) printGroupTriple(reports []reporter.Report) error { 179 | reportGroup, err := GroupByTriple(reports, GroupOutput) 180 | if err != nil { 181 | return fmt.Errorf("unable to group by triple value: %w", err) 182 | } 183 | 184 | for _, reporterObj := range c.Reporters { 185 | if _, ok := reporterObj.(*reporter.JSONReporter); ok { 186 | return reporter.PrintTripleGroupJSON(reportGroup) 187 | } 188 | } 189 | 190 | return reporter.PrintTripleGroupStdout(reportGroup) 191 | } 192 | -------------------------------------------------------------------------------- /pkg/cli/cli_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/Boeing/config-file-validator/pkg/finder" 10 | "github.com/Boeing/config-file-validator/pkg/reporter" 11 | ) 12 | 13 | func Test_CLI(t *testing.T) { 14 | searchPath := "../../test" 15 | excludeDirs := []string{"subdir", "subdir2"} 16 | groupOutput := []string{""} 17 | stdoutReporter := reporter.NewStdoutReporter("") 18 | 19 | fsFinder := finder.FileSystemFinderInit( 20 | finder.WithPathRoots(searchPath), 21 | finder.WithExcludeDirs(excludeDirs), 22 | ) 23 | cli := Init( 24 | WithFinder(fsFinder), 25 | WithReporters(stdoutReporter), 26 | WithGroupOutput(groupOutput), 27 | ) 28 | exitStatus, err := cli.Run() 29 | if err != nil { 30 | t.Errorf("An error was returned: %v", err) 31 | } 32 | 33 | if exitStatus != 0 { 34 | t.Errorf("Exit status was not 0") 35 | } 36 | } 37 | 38 | func Test_CLIWithMultipleReporters(t *testing.T) { 39 | searchPath := "../../test" 40 | excludeDirs := []string{"subdir", "subdir2"} 41 | groupOutput := []string{""} 42 | output := "../../test/output/validator_result.json" 43 | reporters := []reporter.Reporter{ 44 | reporter.NewJSONReporter(output), 45 | reporter.JunitReporter{}, 46 | } 47 | 48 | fsFinder := finder.FileSystemFinderInit( 49 | finder.WithPathRoots(searchPath), 50 | finder.WithExcludeDirs(excludeDirs), 51 | ) 52 | cli := Init( 53 | WithFinder(fsFinder), 54 | WithReporters(reporters...), 55 | WithGroupOutput(groupOutput), 56 | ) 57 | exitStatus, err := cli.Run() 58 | if err != nil { 59 | t.Errorf("An error was returned: %v", err) 60 | } 61 | 62 | if exitStatus != 0 { 63 | t.Errorf("Exit status was not 0") 64 | } 65 | 66 | err = os.Remove(output) 67 | require.NoError(t, err) 68 | } 69 | 70 | func Test_CLIWithFailedValidation(t *testing.T) { 71 | searchPath := "../../test" 72 | excludeDirs := []string{"subdir"} 73 | fsFinder := finder.FileSystemFinderInit( 74 | finder.WithPathRoots(searchPath), 75 | finder.WithExcludeDirs(excludeDirs), 76 | ) 77 | cli := Init( 78 | WithFinder(fsFinder), 79 | ) 80 | exitStatus, err := cli.Run() 81 | if err != nil { 82 | t.Errorf("An error was returned: %v", err) 83 | } 84 | 85 | if exitStatus != 1 { 86 | t.Errorf("Exit status was not 1") 87 | } 88 | } 89 | 90 | func Test_CLIBadPath(t *testing.T) { 91 | searchPath := "/bad/path" 92 | fsFinder := finder.FileSystemFinderInit( 93 | finder.WithPathRoots(searchPath), 94 | ) 95 | cli := Init( 96 | WithFinder(fsFinder), 97 | ) 98 | exitStatus, err := cli.Run() 99 | 100 | if err == nil { 101 | t.Errorf("A nil error was returned") 102 | } 103 | 104 | if exitStatus == 0 { 105 | t.Errorf("Exit status was not 1") 106 | } 107 | } 108 | 109 | func Test_CLIWithGroup(t *testing.T) { 110 | searchPath := "../../test" 111 | excludeDirs := []string{"subdir", "subdir2"} 112 | groupOutput := []string{"pass-fail", "directory"} 113 | stdoutReporter := reporter.NewStdoutReporter("") 114 | 115 | fsFinder := finder.FileSystemFinderInit( 116 | finder.WithPathRoots(searchPath), 117 | finder.WithExcludeDirs(excludeDirs), 118 | ) 119 | cli := Init( 120 | WithFinder(fsFinder), 121 | WithReporters(stdoutReporter), 122 | WithGroupOutput(groupOutput), 123 | ) 124 | exitStatus, err := cli.Run() 125 | if err != nil { 126 | t.Errorf("An error was returned: %v", err) 127 | } 128 | 129 | if exitStatus != 0 { 130 | t.Errorf("Exit status was not 0") 131 | } 132 | } 133 | 134 | func Test_CLIReportErr(t *testing.T) { 135 | searchPath := "../../test" 136 | excludeDirs := []string{"subdir", "subdir2"} 137 | groupOutput := []string{""} 138 | output := "./wrong/path" 139 | jsonReporter := reporter.NewJSONReporter(output) 140 | 141 | fsFinder := finder.FileSystemFinderInit( 142 | finder.WithPathRoots(searchPath), 143 | finder.WithExcludeDirs(excludeDirs), 144 | ) 145 | cli := Init( 146 | WithFinder(fsFinder), 147 | WithReporters(jsonReporter), 148 | WithGroupOutput(groupOutput), 149 | ) 150 | exitStatus, err := cli.Run() 151 | if err != nil { 152 | t.Errorf("An error returned: %v", err) 153 | } 154 | 155 | if exitStatus == 0 { 156 | t.Errorf("should return err status code: %d", exitStatus) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /pkg/cli/group_output.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/Boeing/config-file-validator/pkg/reporter" 8 | ) 9 | 10 | // Group Reports by File Type 11 | func GroupByFileType(reports []reporter.Report) map[string][]reporter.Report { 12 | reportByFile := make(map[string][]reporter.Report) 13 | 14 | for _, report := range reports { 15 | fileType := strings.Split(report.FileName, ".")[1] 16 | fileType = strings.ToLower(fileType) 17 | if fileType == "yml" { 18 | fileType = "yaml" 19 | } 20 | if reportByFile[fileType] == nil { 21 | reportByFile[fileType] = []reporter.Report{report} 22 | } else { 23 | reportByFile[fileType] = append(reportByFile[fileType], report) 24 | } 25 | } 26 | 27 | return reportByFile 28 | } 29 | 30 | // Group Reports by Pass-Fail 31 | func GroupByPassFail(reports []reporter.Report) map[string][]reporter.Report { 32 | reportByPassOrFail := make(map[string][]reporter.Report) 33 | 34 | for _, report := range reports { 35 | if report.IsValid { 36 | if reportByPassOrFail["Passed"] == nil { 37 | reportByPassOrFail["Passed"] = []reporter.Report{report} 38 | } else { 39 | reportByPassOrFail["Passed"] = append(reportByPassOrFail["Passed"], report) 40 | } 41 | } else if reportByPassOrFail["Failed"] == nil { 42 | reportByPassOrFail["Failed"] = []reporter.Report{report} 43 | } else { 44 | reportByPassOrFail["Failed"] = append(reportByPassOrFail["Failed"], report) 45 | } 46 | } 47 | 48 | return reportByPassOrFail 49 | } 50 | 51 | // Group Reports by Directory 52 | func GroupByDirectory(reports []reporter.Report) map[string][]reporter.Report { 53 | reportByDirectory := make(map[string][]reporter.Report) 54 | for _, report := range reports { 55 | directory := "" 56 | // Check if the filepath is in Windows format 57 | if strings.Contains(report.FilePath, "\\") { 58 | directoryPath := strings.Split(report.FilePath, "\\") 59 | directory = strings.Join(directoryPath[:len(directoryPath)-1], "\\") 60 | directory = directory + "\\" 61 | } else { 62 | directoryPath := strings.Split(report.FilePath, "/") 63 | directory = strings.Join(directoryPath[:len(directoryPath)-1], "/") 64 | directory = directory + "/" 65 | } 66 | 67 | if reportByDirectory[directory] == nil { 68 | reportByDirectory[directory] = []reporter.Report{report} 69 | } else { 70 | reportByDirectory[directory] = append(reportByDirectory[directory], report) 71 | } 72 | } 73 | 74 | return reportByDirectory 75 | } 76 | 77 | // Group Reports by single grouping 78 | func GroupBySingle(reports []reporter.Report, groupBy string) (map[string][]reporter.Report, error) { 79 | var groupReport map[string][]reporter.Report 80 | 81 | // Group by the groupings in reverse order 82 | // This allows for the first grouping to be the outermost grouping 83 | for i := len(groupBy) - 1; i >= 0; i-- { 84 | switch groupBy { 85 | case "pass-fail": 86 | groupReport = GroupByPassFail(reports) 87 | case "filetype": 88 | groupReport = GroupByFileType(reports) 89 | case "directory": 90 | groupReport = GroupByDirectory(reports) 91 | default: 92 | return nil, fmt.Errorf("unable to group by %s", groupBy) 93 | } 94 | } 95 | return groupReport, nil 96 | } 97 | 98 | // Group Reports for two groupings 99 | func GroupByDouble(reports []reporter.Report, groupBy []string) (map[string]map[string][]reporter.Report, error) { 100 | groupReport := make(map[string]map[string][]reporter.Report) 101 | 102 | firstGroup, err := GroupBySingle(reports, groupBy[0]) 103 | if err != nil { 104 | return nil, err 105 | } 106 | for key := range firstGroup { 107 | groupReport[key] = make(map[string][]reporter.Report) 108 | groupReport[key], err = GroupBySingle(firstGroup[key], groupBy[1]) 109 | if err != nil { 110 | return nil, err 111 | } 112 | } 113 | 114 | return groupReport, nil 115 | } 116 | 117 | // Group Reports for three groupings 118 | func GroupByTriple(reports []reporter.Report, groupBy []string) (map[string]map[string]map[string][]reporter.Report, error) { 119 | groupReport := make(map[string]map[string]map[string][]reporter.Report) 120 | 121 | firstGroup, err := GroupBySingle(reports, groupBy[0]) 122 | if err != nil { 123 | return nil, err 124 | } 125 | for key := range firstGroup { 126 | groupReport[key] = make(map[string]map[string][]reporter.Report) 127 | groupReport[key], err = GroupByDouble(firstGroup[key], groupBy[1:]) 128 | if err != nil { 129 | return nil, err 130 | } 131 | } 132 | 133 | return groupReport, nil 134 | } 135 | -------------------------------------------------------------------------------- /pkg/cli/group_output_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Boeing/config-file-validator/pkg/finder" 7 | "github.com/Boeing/config-file-validator/pkg/reporter" 8 | ) 9 | 10 | func Test_NoGroupOutput(t *testing.T) { 11 | searchPath := "../../test" 12 | excludeDirs := []string{"subdir", "subdir2"} 13 | groupOutput := map[string][]string{ 14 | "test": {}, 15 | "test2": {}, 16 | "test3": {}, 17 | } 18 | stdoutReporter := reporter.NewStdoutReporter("") 19 | 20 | for i := range groupOutput { 21 | fsFinder := finder.FileSystemFinderInit( 22 | finder.WithPathRoots(searchPath), 23 | finder.WithExcludeDirs(excludeDirs), 24 | ) 25 | cli := Init( 26 | WithFinder(fsFinder), 27 | WithReporters(stdoutReporter), 28 | WithGroupOutput(groupOutput[i]), 29 | ) 30 | exitStatus, err := cli.Run() 31 | if err != nil { 32 | t.Errorf("An error was returned: %v", err) 33 | } 34 | 35 | if exitStatus != 0 { 36 | t.Errorf("Exit status was not 0") 37 | } 38 | } 39 | } 40 | 41 | func Test_SingleGroupOutput(t *testing.T) { 42 | searchPath := "../../test" 43 | excludeDirs := []string{"subdir", "subdir2"} 44 | groupOutput := map[string][]string{ 45 | "test": {"directory"}, 46 | "test2": {"filetype"}, 47 | "test3": {"pass-fail"}, 48 | } 49 | stdoutReporter := reporter.NewStdoutReporter("") 50 | 51 | for i := range groupOutput { 52 | fsFinder := finder.FileSystemFinderInit( 53 | finder.WithPathRoots(searchPath), 54 | finder.WithExcludeDirs(excludeDirs), 55 | ) 56 | cli := Init( 57 | WithFinder(fsFinder), 58 | WithReporters(stdoutReporter), 59 | WithGroupOutput(groupOutput[i]), 60 | ) 61 | exitStatus, err := cli.Run() 62 | if err != nil { 63 | t.Errorf("An error was returned: %v", err) 64 | } 65 | 66 | if exitStatus != 0 { 67 | t.Errorf("Exit status was not 0") 68 | } 69 | } 70 | } 71 | 72 | func Test_WindowsDirectoryGroupBy(t *testing.T) { 73 | reports := []reporter.Report{ 74 | { 75 | FileName: "test", 76 | FilePath: "test\\test\\test", 77 | }, 78 | { 79 | FileName: "test2", 80 | FilePath: "test2\\test2\\test2", 81 | }, 82 | } 83 | 84 | groupDirectory := GroupByDirectory(reports) 85 | 86 | if len(groupDirectory) != 2 { 87 | t.Errorf("GroupByDirectory did not group correctly") 88 | } 89 | } 90 | 91 | func Test_DirectoryGroupBy(t *testing.T) { 92 | reports := []reporter.Report{ 93 | { 94 | FileName: "test", 95 | FilePath: "test/test/test", 96 | }, 97 | { 98 | FileName: "test2", 99 | FilePath: "test2/test2/test2", 100 | }, 101 | } 102 | 103 | groupDirectory := GroupByDirectory(reports) 104 | 105 | if len(groupDirectory) != 2 { 106 | t.Errorf("GroupByDirectory did not group correctly") 107 | } 108 | } 109 | 110 | func Test_DoubleGroupOutput(t *testing.T) { 111 | searchPath := "../../test" 112 | excludeDirs := []string{"subdir", "subdir2"} 113 | groupOutput := map[string][]string{ 114 | "test": {"directory", "pass-fail"}, 115 | "test2": {"filetype", "directory"}, 116 | "test3": {"pass-fail", "filetype"}, 117 | } 118 | stdoutReporter := reporter.NewStdoutReporter("") 119 | 120 | for i := range groupOutput { 121 | fsFinder := finder.FileSystemFinderInit( 122 | finder.WithPathRoots(searchPath), 123 | finder.WithExcludeDirs(excludeDirs), 124 | ) 125 | cli := Init( 126 | WithFinder(fsFinder), 127 | WithReporters(stdoutReporter), 128 | WithGroupOutput(groupOutput[i]), 129 | ) 130 | exitStatus, err := cli.Run() 131 | if err != nil { 132 | t.Errorf("An error was returned: %v", err) 133 | } 134 | 135 | if exitStatus != 0 { 136 | t.Errorf("Exit status was not 0") 137 | } 138 | } 139 | } 140 | 141 | func Test_TripleGroupOutput(t *testing.T) { 142 | searchPath := "../../test" 143 | excludeDirs := []string{"subdir", "subdir2"} 144 | groupOutput := map[string][]string{ 145 | "test": {"directory", "pass-fail", "filetype"}, 146 | "test2": {"filetype", "directory", "pass-fail"}, 147 | "test3": {"pass-fail", "filetype", "directory"}, 148 | } 149 | stdoutReporter := reporter.NewStdoutReporter("") 150 | 151 | for i := range groupOutput { 152 | fsFinder := finder.FileSystemFinderInit( 153 | finder.WithPathRoots(searchPath), 154 | finder.WithExcludeDirs(excludeDirs), 155 | ) 156 | cli := Init( 157 | WithFinder(fsFinder), 158 | WithReporters(stdoutReporter), 159 | WithGroupOutput(groupOutput[i]), 160 | ) 161 | exitStatus, err := cli.Run() 162 | if err != nil { 163 | t.Errorf("An error was returned: %v", err) 164 | } 165 | 166 | if exitStatus != 0 { 167 | t.Errorf("Exit status was not 0") 168 | } 169 | } 170 | } 171 | 172 | func Test_IncorrectSingleGroupOutput(t *testing.T) { 173 | searchPath := "../../test" 174 | excludeDirs := []string{"subdir", "subdir2"} 175 | groupOutput := map[string][]string{ 176 | "test": {"bad"}, 177 | "test2": {"more bad"}, 178 | "test3": {"most bad"}, 179 | } 180 | stdoutReporter := reporter.NewStdoutReporter("") 181 | 182 | for i := range groupOutput { 183 | fsFinder := finder.FileSystemFinderInit( 184 | finder.WithPathRoots(searchPath), 185 | finder.WithExcludeDirs(excludeDirs), 186 | ) 187 | cli := Init( 188 | WithFinder(fsFinder), 189 | WithReporters(stdoutReporter), 190 | WithGroupOutput(groupOutput[i]), 191 | ) 192 | exitStatus, err := cli.Run() 193 | 194 | if err == nil { 195 | t.Errorf("An error was not returned") 196 | } 197 | 198 | if exitStatus != 1 { 199 | t.Errorf("Exit status was not 1") 200 | } 201 | } 202 | } 203 | 204 | func Test_IncorrectDoubleGroupOutput(t *testing.T) { 205 | searchPath := "../../test" 206 | excludeDirs := []string{"subdir", "subdir2"} 207 | groupOutput := map[string][]string{ 208 | "test": {"directory", "bad"}, 209 | "test2": {"bad", "directory"}, 210 | "test3": {"pass-fail", "bad"}, 211 | } 212 | stdoutReporter := reporter.NewStdoutReporter("") 213 | 214 | for i := range groupOutput { 215 | fsFinder := finder.FileSystemFinderInit( 216 | finder.WithPathRoots(searchPath), 217 | finder.WithExcludeDirs(excludeDirs), 218 | ) 219 | cli := Init( 220 | WithFinder(fsFinder), 221 | WithReporters(stdoutReporter), 222 | WithGroupOutput(groupOutput[i]), 223 | ) 224 | exitStatus, err := cli.Run() 225 | 226 | if err == nil { 227 | t.Errorf("An error was not returned") 228 | } 229 | 230 | if exitStatus != 1 { 231 | t.Errorf("Exit status was not 1") 232 | } 233 | } 234 | } 235 | 236 | func Test_IncorrectTripleGroupOutput(t *testing.T) { 237 | searchPath := "../../test" 238 | excludeDirs := []string{"subdir", "subdir2"} 239 | groupOutput := map[string][]string{ 240 | "test": {"bad", "pass-fail", "filetype"}, 241 | "test2": {"filetype", "bad", "directory"}, 242 | "test3": {"pass-fail", "filetype", "bad"}, 243 | } 244 | stdoutReporter := reporter.NewStdoutReporter("") 245 | 246 | for i := range groupOutput { 247 | fsFinder := finder.FileSystemFinderInit( 248 | finder.WithPathRoots(searchPath), 249 | finder.WithExcludeDirs(excludeDirs), 250 | ) 251 | cli := Init( 252 | WithFinder(fsFinder), 253 | WithReporters(stdoutReporter), 254 | WithGroupOutput(groupOutput[i]), 255 | ) 256 | exitStatus, err := cli.Run() 257 | 258 | if err == nil { 259 | t.Errorf("An error was not returned") 260 | } 261 | 262 | if exitStatus != 1 { 263 | t.Errorf("Exit status was not 0") 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /pkg/filetype/file_type.go: -------------------------------------------------------------------------------- 1 | package filetype 2 | 3 | import ( 4 | "github.com/Boeing/config-file-validator/pkg/misc" 5 | "github.com/Boeing/config-file-validator/pkg/validator" 6 | ) 7 | 8 | // The FileType object stores information 9 | // about a file type including name, extensions, 10 | // as well as an instance of the file type's validator 11 | // to be able to validate the file 12 | type FileType struct { 13 | Name string 14 | Extensions map[string]struct{} 15 | Validator validator.Validator 16 | } 17 | 18 | // Instance of the FileType object to 19 | // represent a JSON file 20 | var JSONFileType = FileType{ 21 | "json", 22 | misc.ArrToMap("json"), 23 | validator.JSONValidator{}, 24 | } 25 | 26 | // Instance of the FileType object to 27 | // represent a YAML file 28 | var YAMLFileType = FileType{ 29 | "yaml", 30 | misc.ArrToMap("yml", "yaml"), 31 | validator.YAMLValidator{}, 32 | } 33 | 34 | // Instance of FileType object to 35 | // represent a XML file 36 | var XMLFileType = FileType{ 37 | "xml", 38 | misc.ArrToMap("xml"), 39 | validator.XMLValidator{}, 40 | } 41 | 42 | // Instance of FileType object to 43 | // represent a Toml file 44 | var TomlFileType = FileType{ 45 | "toml", 46 | misc.ArrToMap("toml"), 47 | validator.TomlValidator{}, 48 | } 49 | 50 | // Instance of FileType object to 51 | // represent a Ini file 52 | var IniFileType = FileType{ 53 | "ini", 54 | misc.ArrToMap("ini"), 55 | validator.IniValidator{}, 56 | } 57 | 58 | // Instance of FileType object to 59 | // represent a Properties file 60 | var PropFileType = FileType{ 61 | "properties", 62 | misc.ArrToMap("properties"), 63 | validator.PropValidator{}, 64 | } 65 | 66 | // Instance of the FileType object to 67 | // represent a HCL file 68 | var HclFileType = FileType{ 69 | "hcl", 70 | misc.ArrToMap("hcl"), 71 | validator.HclValidator{}, 72 | } 73 | 74 | // Instance of the FileType object to 75 | // represent a Plist file 76 | var PlistFileType = FileType{ 77 | "plist", 78 | misc.ArrToMap("plist"), 79 | validator.PlistValidator{}, 80 | } 81 | 82 | // Instance of the FileType object to 83 | // represent a CSV file 84 | var CsvFileType = FileType{ 85 | "csv", 86 | misc.ArrToMap("csv"), 87 | validator.CsvValidator{}, 88 | } 89 | 90 | // Instance of the FileType object to 91 | // represent a HOCON file 92 | var HoconFileType = FileType{ 93 | "hocon", 94 | misc.ArrToMap("hocon"), 95 | validator.HoconValidator{}, 96 | } 97 | 98 | // Instance of the FileType object to 99 | // represent a ENV file 100 | var EnvFileType = FileType{ 101 | "env", 102 | misc.ArrToMap("env"), 103 | validator.EnvValidator{}, 104 | } 105 | 106 | // Instance of the FileType object to 107 | // represent an EDITORCONFIG file 108 | var EditorConfigFileType = FileType{ 109 | "editorconfig", 110 | misc.ArrToMap("editorconfig"), 111 | validator.EditorConfigValidator{}, 112 | } 113 | 114 | // An array of files types that are supported 115 | // by the validator 116 | var FileTypes = []FileType{ 117 | JSONFileType, 118 | YAMLFileType, 119 | XMLFileType, 120 | TomlFileType, 121 | IniFileType, 122 | PropFileType, 123 | HclFileType, 124 | PlistFileType, 125 | CsvFileType, 126 | HoconFileType, 127 | EnvFileType, 128 | EditorConfigFileType, 129 | } 130 | -------------------------------------------------------------------------------- /pkg/finder/finder.go: -------------------------------------------------------------------------------- 1 | package finder 2 | 3 | import ( 4 | "github.com/Boeing/config-file-validator/pkg/filetype" 5 | ) 6 | 7 | // The File Metadata object stores the 8 | // name and the path of the file and the type 9 | // of file that it is, example: json, yml, etc 10 | type FileMetadata struct { 11 | Name string 12 | Path string 13 | FileType filetype.FileType 14 | } 15 | 16 | // FileFinder is the interface that wraps the Find method 17 | 18 | // Find will return an array of FileMetadata objects from 19 | // a provided path and array of FileTypes. Any files in 20 | // subdirectories defined in excludeDirs will not be returned 21 | type FileFinder interface { 22 | Find() ([]FileMetadata, error) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/finder/finder_test.go: -------------------------------------------------------------------------------- 1 | package finder 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/Boeing/config-file-validator/pkg/filetype" 9 | "github.com/Boeing/config-file-validator/pkg/misc" 10 | "github.com/Boeing/config-file-validator/pkg/validator" 11 | ) 12 | 13 | func Test_fsFinder(t *testing.T) { 14 | fsFinder := FileSystemFinderInit( 15 | WithPathRoots("../../test/fixtures"), 16 | ) 17 | 18 | files, err := fsFinder.Find() 19 | 20 | if len(files) < 1 { 21 | t.Errorf("Unable to find files") 22 | } 23 | 24 | if err != nil { 25 | t.Errorf("Unable to find files") 26 | } 27 | } 28 | 29 | func Test_fsFinderExcludeDirs(t *testing.T) { 30 | fsFinder := FileSystemFinderInit( 31 | WithPathRoots("../../test/fixtures"), 32 | WithExcludeDirs([]string{"subdir"}), 33 | ) 34 | 35 | files, err := fsFinder.Find() 36 | 37 | if len(files) < 1 { 38 | t.Errorf("Unable to find files") 39 | } 40 | 41 | if err != nil { 42 | t.Errorf("Unable to find files") 43 | } 44 | } 45 | 46 | func Test_fsFinderExcludeFileTypes(t *testing.T) { 47 | fsFinder := FileSystemFinderInit( 48 | WithPathRoots("../../test/fixtures/exclude-file-types"), 49 | WithExcludeFileTypes([]string{"json"}), 50 | ) 51 | 52 | files, err := fsFinder.Find() 53 | 54 | if len(files) != 1 { 55 | fmt.Println(files) 56 | t.Errorf("Wrong amount of files, expected 1 got %d", len(files)) 57 | } 58 | 59 | if err != nil { 60 | t.Errorf("Unable to find files") 61 | } 62 | } 63 | 64 | func Test_fsFinderWithDepth(t *testing.T) { 65 | type test struct { 66 | name string 67 | inputDepth int 68 | inputPathRoot string 69 | expectedFilesCount int 70 | } 71 | 72 | tests := []test{ 73 | { 74 | name: "recursion disabled", 75 | inputDepth: 0, 76 | inputPathRoot: "../", 77 | expectedFilesCount: 0, 78 | }, 79 | { 80 | name: "recursion enabled", 81 | inputDepth: 4, 82 | inputPathRoot: "../../test/fixtures/with-depth", 83 | expectedFilesCount: 2, 84 | }, 85 | { 86 | name: "recursion enabled with lesser depth in the folder structure", 87 | inputDepth: 9, 88 | inputPathRoot: "../../test/fixtures/with-depth", 89 | expectedFilesCount: 2, 90 | }, 91 | } 92 | 93 | for _, tt := range tests { 94 | fsFinder := FileSystemFinderInit( 95 | WithPathRoots(tt.inputPathRoot), 96 | WithDepth(tt.inputDepth), 97 | ) 98 | 99 | files, err := fsFinder.Find() 100 | 101 | if len(files) != tt.expectedFilesCount { 102 | t.Errorf("Wrong amount of files, expected %d got %d", tt.expectedFilesCount, len(files)) 103 | } 104 | 105 | if err != nil { 106 | t.Errorf("Unable to find files") 107 | } 108 | } 109 | } 110 | 111 | func Test_fsFinderCustomTypes(t *testing.T) { 112 | jsonFileType := filetype.FileType{ 113 | Name: "json", 114 | Extensions: misc.ArrToMap("json"), 115 | Validator: validator.JSONValidator{}, 116 | } 117 | fsFinder := FileSystemFinderInit( 118 | WithPathRoots("../../test/fixtures"), 119 | WithExcludeDirs([]string{"subdir"}), 120 | WithFileTypes([]filetype.FileType{jsonFileType}), 121 | ) 122 | 123 | files, err := fsFinder.Find() 124 | 125 | if len(files) < 1 { 126 | t.Errorf("Unable to find files") 127 | } 128 | 129 | if err != nil { 130 | t.Errorf("Unable to find files") 131 | } 132 | } 133 | 134 | func Test_fsFinderPathNoExist(t *testing.T) { 135 | fsFinder := FileSystemFinderInit( 136 | WithPathRoots("/bad/path"), 137 | ) 138 | 139 | _, err := fsFinder.Find() 140 | 141 | if err == nil { 142 | t.Errorf("Error not returned") 143 | } 144 | } 145 | 146 | func Test_FileSystemFinderMultipleFinder(t *testing.T) { 147 | fsFinder := FileSystemFinderInit( 148 | WithPathRoots( 149 | "../../test/fixtures/subdir/good.json", 150 | "../../test/fixtures/good.json", 151 | "./", 152 | ), 153 | ) 154 | 155 | files, err := fsFinder.Find() 156 | 157 | if len(files) != 2 { 158 | t.Errorf("No. files found don't match got:%v, want:%v", len(files), 2) 159 | } 160 | 161 | if err != nil { 162 | t.Errorf("Unable to find files") 163 | } 164 | } 165 | 166 | func Test_FileSystemFinderDuplicateFiles(t *testing.T) { 167 | fsFinder := FileSystemFinderInit( 168 | WithPathRoots( 169 | "../../test/fixtures/subdir/", 170 | ), 171 | ) 172 | 173 | files, err := fsFinder.Find() 174 | 175 | if len(files) != 4 { 176 | t.Errorf("No. files found don't match got:%v, want:%v", len(files), 4) 177 | } 178 | 179 | if err != nil { 180 | t.Errorf("Unable to find files") 181 | } 182 | } 183 | 184 | func Test_FileSystemFinderAbsPath(t *testing.T) { 185 | path := "../../test/fixtures/subdir/good.json" 186 | absPath, err := filepath.Abs(path) 187 | if err != nil { 188 | t.Fatal("Cannot form absolute path") 189 | } 190 | fsFinder := FileSystemFinderInit( 191 | WithPathRoots(path, absPath), 192 | ) 193 | 194 | files, err := fsFinder.Find() 195 | 196 | if len(files) != 1 { 197 | t.Errorf("No. files found don't match got:%v, want:%v", len(files), 1) 198 | } 199 | 200 | if err != nil { 201 | t.Errorf("Unable to find files") 202 | } 203 | } 204 | 205 | func Test_FileSystemFinderUpperCaseExtension(t *testing.T) { 206 | fsFinder := FileSystemFinderInit( 207 | WithPathRoots("../../test/fixtures/uppercase-extension"), 208 | ) 209 | 210 | files, err := fsFinder.Find() 211 | 212 | if len(files) < 1 { 213 | t.Errorf("Unable to find files") 214 | } 215 | 216 | if err != nil { 217 | t.Errorf("Unable to find files") 218 | } 219 | } 220 | 221 | func Test_FileSystemFinderMixedCaseExtension(t *testing.T) { 222 | fsFinder := FileSystemFinderInit( 223 | WithPathRoots("../../test/fixtures/mixedcase-extension"), 224 | ) 225 | 226 | files, err := fsFinder.Find() 227 | 228 | if len(files) < 1 { 229 | t.Errorf("Unable to find files") 230 | } 231 | 232 | if err != nil { 233 | t.Errorf("Unable to find files") 234 | } 235 | } 236 | 237 | func Test_FileFinderBadPath(t *testing.T) { 238 | fsFinder := FileSystemFinderInit( 239 | WithPathRoots( 240 | "../../test/fixtures/subdir", 241 | "/bad/path", 242 | ), 243 | ) 244 | 245 | _, err := fsFinder.Find() 246 | 247 | if err == nil { 248 | t.Errorf("Error should be thrown for bad path") 249 | } 250 | } 251 | 252 | func Test_FileFinderPathWithWhitespaces(t *testing.T) { 253 | tests := []struct { 254 | name string 255 | path string 256 | expectErr bool 257 | }{ 258 | { 259 | name: "no whitespace", 260 | path: "../../test/fixtures/subdir", 261 | }, 262 | { 263 | name: "leading whitespace", 264 | path: " ../../test/fixtures/subdir", 265 | }, 266 | { 267 | name: "trailing whitespace", 268 | path: "../../test/fixtures/subdir ", 269 | }, 270 | { 271 | name: "leading and trailing whitespace", 272 | path: " ../../test/fixtures/subdir ", 273 | }, 274 | { 275 | name: "whitespace in middle of path", 276 | path: "../../test/ fixtures /subdir", 277 | expectErr: true, 278 | }, 279 | { 280 | name: "leading whitespace + whitespace in middle of path", 281 | path: " ../../test/ fixtures /subdir", 282 | expectErr: true, 283 | }, 284 | { 285 | name: "trailing whitespace + whitespace in middle of path", 286 | path: "../../test/ fixtures /subdir ", 287 | expectErr: true, 288 | }, 289 | { 290 | name: "leading and trailing whitespace + whitespace in middle of path", 291 | path: " ../../test/ fixtures /subdir ", 292 | expectErr: true, 293 | }, 294 | } 295 | 296 | for _, tt := range tests { 297 | t.Run(tt.name, func(t *testing.T) { 298 | fsFinder := FileSystemFinderInit( 299 | WithPathRoots(tt.path), 300 | ) 301 | 302 | files, err := fsFinder.Find() 303 | 304 | if tt.expectErr { 305 | if err == nil { 306 | t.Errorf("Error should be thrown for bad path") 307 | } 308 | } else { 309 | if len(files) < 1 { 310 | t.Errorf("Unable to find file") 311 | } 312 | 313 | if err != nil { 314 | t.Errorf("Unable to find file") 315 | } 316 | } 317 | }) 318 | } 319 | } 320 | 321 | func Benchmark_Finder(b *testing.B) { 322 | fsFinder := FileSystemFinderInit( 323 | WithPathRoots("../../test/fixtures/"), 324 | ) 325 | 326 | b.ResetTimer() 327 | 328 | for i := 0; i < b.N; i++ { 329 | _, _ = fsFinder.Find() 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /pkg/finder/fsfinder.go: -------------------------------------------------------------------------------- 1 | package finder 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/Boeing/config-file-validator/pkg/filetype" 10 | "github.com/Boeing/config-file-validator/pkg/misc" 11 | ) 12 | 13 | type FileSystemFinder struct { 14 | PathRoots []string 15 | FileTypes []filetype.FileType 16 | ExcludeDirs map[string]struct{} 17 | ExcludeFileTypes map[string]struct{} 18 | Depth *int 19 | } 20 | 21 | type FSFinderOptions func(*FileSystemFinder) 22 | 23 | // Set the CLI SearchPath 24 | func WithPathRoots(paths ...string) FSFinderOptions { 25 | return func(fsf *FileSystemFinder) { 26 | fsf.PathRoots = paths 27 | } 28 | } 29 | 30 | // Add a custom list of file types to the FSFinder 31 | func WithFileTypes(fileTypes []filetype.FileType) FSFinderOptions { 32 | return func(fsf *FileSystemFinder) { 33 | fsf.FileTypes = fileTypes 34 | } 35 | } 36 | 37 | // Add a custom list of file types to the FSFinder 38 | func WithExcludeDirs(excludeDirs []string) FSFinderOptions { 39 | return func(fsf *FileSystemFinder) { 40 | fsf.ExcludeDirs = misc.ArrToMap(excludeDirs...) 41 | } 42 | } 43 | 44 | // WithExcludeFileTypes adds excluded file types to FSFinder. 45 | func WithExcludeFileTypes(types []string) FSFinderOptions { 46 | return func(fsf *FileSystemFinder) { 47 | fsf.ExcludeFileTypes = misc.ArrToMap(types...) 48 | } 49 | } 50 | 51 | // WithDepth adds the depth for search recursion to FSFinder 52 | func WithDepth(depthVal int) FSFinderOptions { 53 | return func(fsf *FileSystemFinder) { 54 | fsf.Depth = &depthVal 55 | } 56 | } 57 | 58 | func FileSystemFinderInit(opts ...FSFinderOptions) *FileSystemFinder { 59 | defaultExcludeDirs := make(map[string]struct{}) 60 | defaultExcludeFileTypes := make(map[string]struct{}) 61 | defaultPathRoots := []string{"."} 62 | 63 | fsfinder := &FileSystemFinder{ 64 | PathRoots: defaultPathRoots, 65 | FileTypes: filetype.FileTypes, 66 | ExcludeDirs: defaultExcludeDirs, 67 | ExcludeFileTypes: defaultExcludeFileTypes, 68 | } 69 | 70 | for _, opt := range opts { 71 | opt(fsfinder) 72 | } 73 | 74 | return fsfinder 75 | } 76 | 77 | // Find implements the FileFinder interface by calling findOne on 78 | // all the PathRoots and providing the aggregated FileMetadata after 79 | // ignoring all the duplicate files 80 | func (fsf FileSystemFinder) Find() ([]FileMetadata, error) { 81 | seen := make(map[string]struct{}, 0) 82 | uniqueMatches := make([]FileMetadata, 0) 83 | for _, pathRoot := range fsf.PathRoots { 84 | // remove all leading and trailing whitespace 85 | trimmedPathRoot := strings.TrimSpace(pathRoot) 86 | matches, err := fsf.findOne(trimmedPathRoot, seen) 87 | if err != nil { 88 | return nil, err 89 | } 90 | uniqueMatches = append(uniqueMatches, matches...) 91 | } 92 | return uniqueMatches, nil 93 | } 94 | 95 | // findOne recursively walks through all subdirectories (excluding the excluded subdirectories) 96 | // and identifying if the file matches a type defined in the fileTypes array for a 97 | // single path and returns the file metadata. 98 | func (fsf FileSystemFinder) findOne(pathRoot string, seenMap map[string]struct{}) ([]FileMetadata, error) { 99 | var matchingFiles []FileMetadata 100 | 101 | // check that the path exists before walking it or the error returned 102 | // from filepath.Walk will be very confusing and undescriptive 103 | if _, err := os.Stat(pathRoot); os.IsNotExist(err) { 104 | return nil, err 105 | } 106 | 107 | var depth int 108 | if fsf.Depth != nil { 109 | depth = *fsf.Depth 110 | } 111 | 112 | maxDepth := strings.Count(pathRoot, string(os.PathSeparator)) + depth 113 | 114 | err := filepath.WalkDir(pathRoot, 115 | func(path string, dirEntry fs.DirEntry, err error) error { 116 | if err != nil { 117 | return err 118 | } 119 | 120 | // determine if directory is in the excludeDirs list or if the depth is greater than the maxDepth 121 | if dirEntry.IsDir() { 122 | _, isExcluded := fsf.ExcludeDirs[dirEntry.Name()] 123 | if isExcluded || (fsf.Depth != nil && strings.Count(path, string(os.PathSeparator)) > maxDepth) { 124 | return filepath.SkipDir 125 | } 126 | } 127 | 128 | if !dirEntry.IsDir() { 129 | // filepath.Ext() returns the extension name with a dot so it 130 | // needs to be removed. 131 | walkFileExtension := strings.TrimPrefix(filepath.Ext(path), ".") 132 | extensionLowerCase := strings.ToLower(walkFileExtension) 133 | 134 | if _, isExcluded := fsf.ExcludeFileTypes[extensionLowerCase]; isExcluded { 135 | return nil 136 | } 137 | 138 | for _, fileType := range fsf.FileTypes { 139 | if _, isMatched := fileType.Extensions[extensionLowerCase]; isMatched { 140 | absPath, err := filepath.Abs(path) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | if _, seen := seenMap[absPath]; !seen { 146 | fileMetadata := FileMetadata{dirEntry.Name(), absPath, fileType} 147 | matchingFiles = append(matchingFiles, fileMetadata) 148 | seenMap[absPath] = struct{}{} 149 | } 150 | 151 | return nil 152 | } 153 | } 154 | fsf.ExcludeFileTypes[extensionLowerCase] = struct{}{} 155 | } 156 | 157 | return nil 158 | }) 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | return matchingFiles, nil 164 | } 165 | -------------------------------------------------------------------------------- /pkg/misc/arrToMap.go: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | // ArrToMap converts a string array 4 | // to a map with keys from the array 5 | // and empty struct values, optimizing string presence checks. 6 | func ArrToMap(arg ...string) map[string]struct{} { 7 | m := make(map[string]struct{}, 0) 8 | for _, item := range arg { 9 | m[item] = struct{}{} 10 | } 11 | return m 12 | } 13 | -------------------------------------------------------------------------------- /pkg/reporter/json_reporter.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type JSONReporter struct { 10 | outputDest string 11 | } 12 | 13 | func NewJSONReporter(outputDest string) *JSONReporter { 14 | return &JSONReporter{ 15 | outputDest: outputDest, 16 | } 17 | } 18 | 19 | type fileStatus struct { 20 | Path string `json:"path"` 21 | Status string `json:"status"` 22 | Error string `json:"error,omitempty"` 23 | } 24 | 25 | type summary struct { 26 | Passed int `json:"passed"` 27 | Failed int `json:"failed"` 28 | } 29 | 30 | type reportJSON struct { 31 | Files []fileStatus `json:"files"` 32 | Summary summary `json:"summary"` 33 | } 34 | 35 | type groupReportJSON struct { 36 | Files map[string][]fileStatus `json:"files"` 37 | Summary map[string][]summary `json:"summary"` 38 | TotalPassed int `json:"totalPassed"` 39 | TotalFailed int `json:"totalFailed"` 40 | } 41 | 42 | type doubleGroupReportJSON struct { 43 | Files map[string]map[string][]fileStatus `json:"files"` 44 | Summary map[string]map[string][]summary `json:"summary"` 45 | TotalPassed int `json:"totalPassed"` 46 | TotalFailed int `json:"totalFailed"` 47 | } 48 | 49 | type tripleGroupReportJSON struct { 50 | Files map[string]map[string]map[string][]fileStatus `json:"files"` 51 | Summary map[string]map[string]map[string][]summary `json:"summary"` 52 | TotalPassed int `json:"totalPassed"` 53 | TotalFailed int `json:"totalFailed"` 54 | } 55 | 56 | // Print implements the Reporter interface by outputting 57 | // the report content to stdout as JSON 58 | // if outputDest flag is provided, output results to a file. 59 | func (jr JSONReporter) Print(reports []Report) error { 60 | report, err := createJSONReport(reports) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | jsonBytes, err := json.MarshalIndent(report, "", " ") 66 | if err != nil { 67 | return err 68 | } 69 | 70 | jsonBytes = append(jsonBytes, '\n') 71 | 72 | if jr.outputDest != "" { 73 | return outputBytesToFile(jr.outputDest, "result", "json", jsonBytes) 74 | } 75 | 76 | if len(reports) > 0 && !reports[0].IsQuiet { 77 | fmt.Print(string(jsonBytes)) 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // Prints the report for when one group is passed in the groupby flag 84 | func PrintSingleGroupJSON(groupReports map[string][]Report) error { 85 | var jsonReport groupReportJSON 86 | totalPassed := 0 87 | totalFailed := 0 88 | jsonReport.Files = make(map[string][]fileStatus) 89 | jsonReport.Summary = make(map[string][]summary) 90 | 91 | for group, reports := range groupReports { 92 | report, err := createJSONReport(reports) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | jsonReport.Files[group] = report.Files 98 | jsonReport.Summary[group] = append(jsonReport.Summary[group], report.Summary) 99 | 100 | totalPassed += report.Summary.Passed 101 | totalFailed += report.Summary.Failed 102 | 103 | } 104 | 105 | jsonReport.TotalPassed = totalPassed 106 | jsonReport.TotalFailed = totalFailed 107 | 108 | jsonBytes, err := json.MarshalIndent(jsonReport, "", " ") 109 | if err != nil { 110 | return err 111 | } 112 | 113 | fmt.Println(string(jsonBytes)) 114 | return nil 115 | } 116 | 117 | // Prints the report for when two groups are passed in the groupby flag 118 | func PrintDoubleGroupJSON(groupReports map[string]map[string][]Report) error { 119 | var jsonReport doubleGroupReportJSON 120 | totalPassed := 0 121 | totalFailed := 0 122 | jsonReport.Files = make(map[string]map[string][]fileStatus) 123 | jsonReport.Summary = make(map[string]map[string][]summary) 124 | 125 | for group, group2 := range groupReports { 126 | jsonReport.Files[group] = make(map[string][]fileStatus, 0) 127 | jsonReport.Summary[group] = make(map[string][]summary, 0) 128 | for group2, reports := range group2 { 129 | report, err := createJSONReport(reports) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | jsonReport.Files[group][group2] = report.Files 135 | jsonReport.Summary[group][group2] = append(jsonReport.Summary[group][group2], report.Summary) 136 | 137 | totalPassed += report.Summary.Passed 138 | totalFailed += report.Summary.Failed 139 | 140 | } 141 | } 142 | 143 | jsonReport.TotalPassed = totalPassed 144 | jsonReport.TotalFailed = totalFailed 145 | 146 | jsonBytes, err := json.MarshalIndent(jsonReport, "", " ") 147 | if err != nil { 148 | return err 149 | } 150 | fmt.Println(string(jsonBytes)) 151 | return nil 152 | } 153 | 154 | // Prints the report for when three groups are passed in the groupby flag 155 | func PrintTripleGroupJSON(groupReports map[string]map[string]map[string][]Report) error { 156 | var jsonReport tripleGroupReportJSON 157 | totalPassed := 0 158 | totalFailed := 0 159 | jsonReport.Files = make(map[string]map[string]map[string][]fileStatus) 160 | jsonReport.Summary = make(map[string]map[string]map[string][]summary) 161 | 162 | for group, group2 := range groupReports { 163 | jsonReport.Files[group] = make(map[string]map[string][]fileStatus, 0) 164 | jsonReport.Summary[group] = make(map[string]map[string][]summary, 0) 165 | 166 | for group2, group3 := range group2 { 167 | jsonReport.Files[group][group2] = make(map[string][]fileStatus, 0) 168 | jsonReport.Summary[group][group2] = make(map[string][]summary, 0) 169 | 170 | for group3, reports := range group3 { 171 | report, err := createJSONReport(reports) 172 | if err != nil { 173 | return err 174 | } 175 | 176 | jsonReport.Files[group][group2][group3] = report.Files 177 | jsonReport.Summary[group][group2][group3] = append(jsonReport.Summary[group][group2][group3], report.Summary) 178 | 179 | totalPassed += report.Summary.Passed 180 | totalFailed += report.Summary.Failed 181 | 182 | } 183 | 184 | } 185 | } 186 | 187 | jsonReport.TotalPassed = totalPassed 188 | jsonReport.TotalFailed = totalFailed 189 | 190 | jsonBytes, err := json.MarshalIndent(jsonReport, "", " ") 191 | if err != nil { 192 | return err 193 | } 194 | 195 | fmt.Println(string(jsonBytes)) 196 | return nil 197 | } 198 | 199 | // Creates the json report 200 | func createJSONReport(reports []Report) (reportJSON, error) { 201 | var jsonReport reportJSON 202 | 203 | for _, report := range reports { 204 | status := "passed" 205 | errorStr := "" 206 | if !report.IsValid { 207 | status = "failed" 208 | errorStr = report.ValidationError.Error() 209 | } 210 | 211 | // Convert Windows-style file paths. 212 | if strings.Contains(report.FilePath, "\\") { 213 | report.FilePath = strings.ReplaceAll(report.FilePath, "\\", "/") 214 | } 215 | 216 | jsonReport.Files = append(jsonReport.Files, fileStatus{ 217 | Path: report.FilePath, 218 | Status: status, 219 | Error: errorStr, 220 | }) 221 | 222 | currentPassed := 0 223 | currentFailed := 0 224 | for _, f := range jsonReport.Files { 225 | if f.Status == "passed" { 226 | currentPassed++ 227 | } else { 228 | currentFailed++ 229 | } 230 | } 231 | 232 | jsonReport.Summary.Passed = currentPassed 233 | jsonReport.Summary.Failed = currentFailed 234 | } 235 | 236 | return jsonReport, nil 237 | } 238 | -------------------------------------------------------------------------------- /pkg/reporter/junit_reporter.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type JunitReporter struct { 11 | outputDest string 12 | } 13 | 14 | func NewJunitReporter(outputDest string) *JunitReporter { 15 | return &JunitReporter{ 16 | outputDest: outputDest, 17 | } 18 | } 19 | 20 | const ( 21 | Header = `` + "\n" 22 | ) 23 | 24 | type Message struct { 25 | InnerXML string `xml:",innerxml"` 26 | } 27 | 28 | // https://github.com/testmoapp/junitxml#basic-junit-xml-structure 29 | type Testsuites struct { 30 | XMLName xml.Name `xml:"testsuites"` 31 | Name string `xml:"name,attr,omitempty"` 32 | Tests int `xml:"tests,attr,omitempty"` 33 | Failures int `xml:"failures,attr,omitempty"` 34 | Errors int `xml:"errors,attr,omitempty"` 35 | Skipped int `xml:"skipped,attr,omitempty"` 36 | Assertions int `xml:"assertions,attr,omitempty"` 37 | Time float32 `xml:"time,attr,omitempty"` 38 | Timestamp *time.Time `xml:"timestamp,attr,omitempty"` 39 | Testsuites []Testsuite `xml:"testsuite"` 40 | } 41 | 42 | type Testsuite struct { 43 | XMLName xml.Name `xml:"testsuite"` 44 | Name string `xml:"name,attr"` 45 | Tests int `xml:"tests,attr,omitempty"` 46 | Failures int `xml:"failures,attr,omitempty"` 47 | Errors int `xml:"errors,attr,omitempty"` 48 | Skipped int `xml:"skipped,attr,omitempty"` 49 | Assertions int `xml:"assertions,attr,omitempty"` 50 | Time float32 `xml:"time,attr,omitempty"` 51 | Timestamp *time.Time `xml:"timestamp,attr,omitempty"` 52 | File string `xml:"file,attr,omitempty"` 53 | Testcases *[]Testcase `xml:"testcase,omitempty"` 54 | Properties *[]Property `xml:"properties>property,omitempty"` 55 | SystemOut *SystemOut `xml:"system-out,omitempty"` 56 | SystemErr *SystemErr `xml:"system-err,omitempty"` 57 | } 58 | 59 | type Testcase struct { 60 | XMLName xml.Name `xml:"testcase"` 61 | Name string `xml:"name,attr"` 62 | ClassName string `xml:"classname,attr"` 63 | Assertions int `xml:"assertions,attr,omitempty"` 64 | Time float32 `xml:"time,attr,omitempty"` 65 | File string `xml:"file,attr,omitempty"` 66 | Line int `xml:"line,attr,omitempty"` 67 | Skipped *Skipped `xml:"skipped,attr,omitempty"` 68 | Properties *[]Property `xml:"properties>property,omitempty"` 69 | TestcaseError *TestcaseError `xml:"error,omitempty"` 70 | TestcaseFailure *TestcaseFailure `xml:"failure,omitempty"` 71 | } 72 | 73 | type Skipped struct { 74 | XMLName xml.Name `xml:"skipped"` 75 | Message string `xml:"message,attr"` 76 | } 77 | 78 | type TestcaseError struct { 79 | XMLName xml.Name `xml:"error"` 80 | Message Message 81 | Type string `xml:"type,omitempty"` 82 | TextValue string `xml:",chardata"` 83 | } 84 | 85 | type TestcaseFailure struct { 86 | XMLName xml.Name `xml:"failure"` 87 | // Message string `xml:"message,omitempty"` 88 | Message Message 89 | Type string `xml:"type,omitempty"` 90 | TextValue string `xml:",chardata"` 91 | } 92 | 93 | type SystemOut struct { 94 | XMLName xml.Name `xml:"system-out"` 95 | TextValue string `xml:",chardata"` 96 | } 97 | 98 | type SystemErr struct { 99 | XMLName xml.Name `xml:"system-err"` 100 | TextValue string `xml:",chardata"` 101 | } 102 | 103 | type Property struct { 104 | XMLName xml.Name `xml:"property"` 105 | TextValue string `xml:",chardata"` 106 | Name string `xml:"name,attr"` 107 | Value string `xml:"value,attr,omitempty"` 108 | } 109 | 110 | func checkProperty(property Property, xmlElementName string, name string) error { 111 | if property.Value != "" && property.TextValue != "" { 112 | return fmt.Errorf("property %s in %s %s should contain value or a text value, not both", 113 | property.Name, xmlElementName, name) 114 | } 115 | return nil 116 | } 117 | 118 | func checkTestCase(testcase Testcase) (err error) { 119 | if testcase.Properties != nil { 120 | for propidx := range *testcase.Properties { 121 | property := (*testcase.Properties)[propidx] 122 | if err = checkProperty(property, "testcase", testcase.Name); err != nil { 123 | return err 124 | } 125 | } 126 | } 127 | return nil 128 | } 129 | 130 | func checkTestSuite(testsuite Testsuite) (err error) { 131 | if testsuite.Properties != nil { 132 | for pridx := range *testsuite.Properties { 133 | property := (*testsuite.Properties)[pridx] 134 | if err = checkProperty(property, "testsuite", testsuite.Name); err != nil { 135 | return err 136 | } 137 | } 138 | } 139 | 140 | if testsuite.Testcases != nil { 141 | for tcidx := range *testsuite.Testcases { 142 | testcase := (*testsuite.Testcases)[tcidx] 143 | if err = checkTestCase(testcase); err != nil { 144 | return err 145 | } 146 | } 147 | } 148 | return nil 149 | } 150 | 151 | func (ts Testsuites) checkPropertyValidity() (err error) { 152 | for tsidx := range ts.Testsuites { 153 | testsuite := ts.Testsuites[tsidx] 154 | if err = checkTestSuite(testsuite); err != nil { 155 | return err 156 | } 157 | } 158 | return nil 159 | } 160 | 161 | func (ts Testsuites) getReport() ([]byte, error) { 162 | err := ts.checkPropertyValidity() 163 | if err != nil { 164 | return []byte{}, err 165 | } 166 | 167 | data, err := xml.MarshalIndent(ts, " ", " ") 168 | if err != nil { 169 | return []byte{}, err 170 | } 171 | 172 | data = append(data, '\n') 173 | return data, nil 174 | } 175 | 176 | func (jr JunitReporter) Print(reports []Report) error { 177 | testcases := []Testcase{} 178 | testErrors := 0 179 | 180 | for _, r := range reports { 181 | if strings.Contains(r.FilePath, "\\") { 182 | r.FilePath = strings.ReplaceAll(r.FilePath, "\\", "/") 183 | } 184 | tc := Testcase{Name: fmt.Sprintf("%s validation", r.FilePath), File: r.FilePath, ClassName: "config-file-validator"} 185 | if !r.IsValid { 186 | testErrors++ 187 | tc.TestcaseFailure = &TestcaseFailure{Message: Message{InnerXML: r.ValidationError.Error()}} 188 | } 189 | testcases = append(testcases, tc) 190 | } 191 | testsuite := Testsuite{Name: "config-file-validator", Testcases: &testcases, Errors: testErrors} 192 | testsuiteBatch := []Testsuite{testsuite} 193 | ts := Testsuites{Name: "config-file-validator", Tests: len(reports), Testsuites: testsuiteBatch} 194 | 195 | data, err := ts.getReport() 196 | if err != nil { 197 | return err 198 | } 199 | 200 | results := Header + string(data) 201 | 202 | if jr.outputDest != "" { 203 | return outputBytesToFile(jr.outputDest, "result", "xml", []byte(results)) 204 | } 205 | 206 | if len(reports) > 0 && !reports[0].IsQuiet { 207 | fmt.Println(results) 208 | } 209 | 210 | return nil 211 | } 212 | -------------------------------------------------------------------------------- /pkg/reporter/reporter.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | // The Report object stores information about the report 4 | // and the results of the validation 5 | type Report struct { 6 | FileName string 7 | FilePath string 8 | IsValid bool 9 | ValidationError error 10 | IsQuiet bool 11 | } 12 | 13 | // Reporter is the interface that wraps the Print method 14 | 15 | // Print accepts an array of Report objects and determines 16 | // how to output the contents. Output could be stdout, 17 | // files, etc 18 | type Reporter interface { 19 | Print(reports []Report) error 20 | } 21 | -------------------------------------------------------------------------------- /pkg/reporter/sarif_reporter.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | const SARIFVersion = "2.1.0" 10 | const SARIFSchema = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json" 11 | const DriverName = "config-file-validator" 12 | const DriverInfoURI = "https://github.com/Boeing/config-file-validator" 13 | const DriverVersion = "1.8.0" 14 | 15 | type SARIFReporter struct { 16 | outputDest string 17 | } 18 | 19 | type SARIFLog struct { 20 | Version string `json:"version"` 21 | Schema string `json:"$schema"` 22 | Runs []runs `json:"runs"` 23 | } 24 | 25 | type runs struct { 26 | Tool tool `json:"tool"` 27 | Results []result `json:"results"` 28 | } 29 | 30 | type tool struct { 31 | Driver driver `json:"driver"` 32 | } 33 | 34 | type driver struct { 35 | Name string `json:"name"` 36 | InfoURI string `json:"informationUri"` 37 | Version string `json:"version"` 38 | } 39 | 40 | type result struct { 41 | Kind string `json:"kind"` 42 | Level string `json:"level"` 43 | Message message `json:"message"` 44 | Locations []location `json:"locations"` 45 | } 46 | 47 | type message struct { 48 | Text string `json:"text"` 49 | } 50 | 51 | type location struct { 52 | PhysicalLocation physicalLocation `json:"physicalLocation"` 53 | } 54 | 55 | type physicalLocation struct { 56 | ArtifactLocation artifactLocation `json:"artifactLocation"` 57 | } 58 | 59 | type artifactLocation struct { 60 | URI string `json:"uri"` 61 | } 62 | 63 | func NewSARIFReporter(outputDest string) *SARIFReporter { 64 | return &SARIFReporter{ 65 | outputDest: outputDest, 66 | } 67 | } 68 | 69 | func createSARIFReport(reports []Report) (*SARIFLog, error) { 70 | var log SARIFLog 71 | 72 | n := len(reports) 73 | 74 | log.Version = SARIFVersion 75 | log.Schema = SARIFSchema 76 | 77 | log.Runs = make([]runs, 1) 78 | runs := &log.Runs[0] 79 | 80 | runs.Tool.Driver.Name = DriverName 81 | runs.Tool.Driver.InfoURI = DriverInfoURI 82 | runs.Tool.Driver.Version = DriverVersion 83 | 84 | runs.Results = make([]result, n) 85 | 86 | for i, report := range reports { 87 | if strings.Contains(report.FilePath, "\\") { 88 | report.FilePath = strings.ReplaceAll(report.FilePath, "\\", "/") 89 | } 90 | 91 | result := &runs.Results[i] 92 | if !report.IsValid { 93 | result.Kind = "fail" 94 | result.Level = "error" 95 | result.Message.Text = report.ValidationError.Error() 96 | } else { 97 | result.Kind = "pass" 98 | result.Level = "none" 99 | result.Message.Text = "No errors detected" 100 | } 101 | 102 | result.Locations = make([]location, 1) 103 | location := &result.Locations[0] 104 | 105 | location.PhysicalLocation.ArtifactLocation.URI = "file:///" + report.FilePath 106 | } 107 | 108 | return &log, nil 109 | } 110 | 111 | func (sr SARIFReporter) Print(reports []Report) error { 112 | report, err := createSARIFReport(reports) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | sarifBytes, err := json.MarshalIndent(report, "", " ") 118 | if err != nil { 119 | return err 120 | } 121 | 122 | sarifBytes = append(sarifBytes, '\n') 123 | 124 | if sr.outputDest != "" { 125 | return outputBytesToFile(sr.outputDest, "result", "sarif", sarifBytes) 126 | } 127 | 128 | if len(reports) > 0 && !reports[0].IsQuiet { 129 | fmt.Print(string(sarifBytes)) 130 | } 131 | 132 | return nil 133 | } 134 | -------------------------------------------------------------------------------- /pkg/reporter/stdout_reporter.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/fatih/color" 8 | ) 9 | 10 | type StdoutReporter struct { 11 | outputDest string 12 | } 13 | 14 | type reportStdout struct { 15 | Text string 16 | Summary summary 17 | } 18 | 19 | func NewStdoutReporter(outputDest string) *StdoutReporter { 20 | return &StdoutReporter{ 21 | outputDest: outputDest, 22 | } 23 | } 24 | 25 | // Print implements the Reporter interface by outputting 26 | // the report content to stdout 27 | func (sr StdoutReporter) Print(reports []Report) error { 28 | stdoutReport := createStdoutReport(reports, 1) 29 | 30 | if sr.outputDest != "" { 31 | return outputBytesToFile(sr.outputDest, "result", "txt", []byte(stdoutReport.Text)) 32 | } 33 | 34 | if len(reports) > 0 && !reports[0].IsQuiet { 35 | fmt.Print(stdoutReport.Text) 36 | } 37 | 38 | return nil 39 | } 40 | 41 | // There is repeated code in the following two functions. Trying to consolidate 42 | // the code into one function is difficult because of the output format 43 | func PrintSingleGroupStdout(groupReport map[string][]Report) error { 44 | totalSuccessCount := 0 45 | totalFailureCount := 0 46 | 47 | for group, reports := range groupReport { 48 | fmt.Printf("%s\n", group) 49 | stdoutReport := createStdoutReport(reports, 1) 50 | totalSuccessCount += stdoutReport.Summary.Passed 51 | totalFailureCount += stdoutReport.Summary.Failed 52 | fmt.Println(stdoutReport.Text) 53 | if checkGroupsForPassFail(group) { 54 | fmt.Printf("Summary: %d succeeded, %d failed\n\n", stdoutReport.Summary.Passed, stdoutReport.Summary.Failed) 55 | } 56 | } 57 | 58 | fmt.Printf("Total Summary: %d succeeded, %d failed\n", totalSuccessCount, totalFailureCount) 59 | return nil 60 | } 61 | 62 | // Prints the report for when two groups are passed in the groupby flag 63 | func PrintDoubleGroupStdout(groupReport map[string]map[string][]Report) error { 64 | totalSuccessCount := 0 65 | totalFailureCount := 0 66 | 67 | for group, reports := range groupReport { 68 | fmt.Printf("%s\n", group) 69 | for group2, reports2 := range reports { 70 | fmt.Printf(" %s\n", group2) 71 | stdoutReport := createStdoutReport(reports2, 2) 72 | totalSuccessCount += stdoutReport.Summary.Passed 73 | totalFailureCount += stdoutReport.Summary.Failed 74 | fmt.Println(stdoutReport.Text) 75 | if checkGroupsForPassFail(group, group2) { 76 | fmt.Printf(" Summary: %d succeeded, %d failed\n\n", stdoutReport.Summary.Passed, stdoutReport.Summary.Failed) 77 | } 78 | } 79 | } 80 | 81 | fmt.Printf("Total Summary: %d succeeded, %d failed\n", totalSuccessCount, totalFailureCount) 82 | 83 | return nil 84 | } 85 | 86 | // Prints the report for when three groups are passed in the groupby flag 87 | func PrintTripleGroupStdout(groupReport map[string]map[string]map[string][]Report) error { 88 | totalSuccessCount := 0 89 | totalFailureCount := 0 90 | 91 | for groupOne, header := range groupReport { 92 | fmt.Printf("%s\n", groupOne) 93 | for groupTwo, subheader := range header { 94 | fmt.Printf(" %s\n", groupTwo) 95 | for groupThree, reports := range subheader { 96 | fmt.Printf(" %s\n", groupThree) 97 | stdoutReport := createStdoutReport(reports, 3) 98 | totalSuccessCount += stdoutReport.Summary.Passed 99 | totalFailureCount += stdoutReport.Summary.Failed 100 | fmt.Println(stdoutReport.Text) 101 | if checkGroupsForPassFail(groupOne, groupTwo, groupThree) { 102 | fmt.Printf(" Summary: %d succeeded, %d failed\n\n", stdoutReport.Summary.Passed, stdoutReport.Summary.Failed) 103 | } 104 | } 105 | } 106 | } 107 | 108 | fmt.Printf("Total Summary: %d succeeded, %d failed\n", totalSuccessCount, totalFailureCount) 109 | return nil 110 | } 111 | 112 | // Checks if any of the provided groups are "Passed" or "Failed". 113 | func checkGroupsForPassFail(groups ...string) bool { 114 | for _, group := range groups { 115 | if group == "Passed" || group == "Failed" { 116 | return false 117 | } 118 | } 119 | return true 120 | } 121 | 122 | // Creates the standard text report 123 | func createStdoutReport(reports []Report, indentSize int) reportStdout { 124 | result := reportStdout{} 125 | baseIndent := " " 126 | indent, errIndent := strings.Repeat(baseIndent, indentSize), strings.Repeat(baseIndent, indentSize+1) 127 | 128 | for _, report := range reports { 129 | if !report.IsValid { 130 | fmtRed := color.New(color.FgRed) 131 | paddedString := padErrorString(report.ValidationError.Error()) 132 | result.Text += fmtRed.Sprintf("%s× %s\n", indent, report.FilePath) 133 | result.Text += fmtRed.Sprintf("%serror: %v\n", errIndent, paddedString) 134 | result.Summary.Failed++ 135 | } else { 136 | result.Text += color.New(color.FgGreen).Sprintf("%s✓ %s\n", indent, report.FilePath) 137 | result.Summary.Passed++ 138 | } 139 | } 140 | 141 | return result 142 | } 143 | 144 | // padErrorString adds padding to every newline in the error 145 | // string, except the first line and removes any trailing newlines 146 | // or spaces 147 | func padErrorString(errS string) string { 148 | errS = strings.TrimSpace(errS) 149 | lines := strings.Split(errS, "\n") 150 | for idx := 1; idx < len(lines); idx++ { 151 | lines[idx] = " " + lines[idx] 152 | } 153 | paddedErr := strings.Join(lines, "\n") 154 | return paddedErr 155 | } 156 | -------------------------------------------------------------------------------- /pkg/reporter/writer.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // outputBytesToFile outputs the named file at the destination specified by outputDest. 9 | // if an existing directory is provided to outputDest param, creates a file named with defaultName given at the directory. 10 | // if outputDest specifies a path to the file, creates the file named with outputDest. 11 | // when empty string is given to outputDest param, it returns error. 12 | func outputBytesToFile(outputDest, defaultName, extension string, bytes []byte) error { 13 | var fileName string 14 | info, err := os.Stat(outputDest) 15 | if outputDest == "" { 16 | return fmt.Errorf("outputDest is an empty string: %w", err) 17 | } else if !os.IsNotExist(err) && info.IsDir() { 18 | if extension != "" { 19 | extension = "." + extension 20 | } 21 | fileName = outputDest + "/" + defaultName + extension 22 | } else { 23 | fileName = outputDest 24 | } 25 | 26 | file, err := os.Create(fileName) 27 | if err != nil { 28 | return fmt.Errorf("failed to create a file: %w", err) 29 | } 30 | defer file.Close() 31 | _, err = file.Write(bytes) 32 | if err != nil { 33 | return fmt.Errorf("failed to output bytes to a file: %w", err) 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/reporter/writer_test.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_outputBytesToFile(t *testing.T) { 12 | deleteFiles(t) 13 | 14 | bytes, err := os.ReadFile("../../test/output/example/writer_example.txt") 15 | require.NoError(t, err) 16 | 17 | type args struct { 18 | results []byte 19 | outputDest string 20 | defaultName string 21 | extension string 22 | } 23 | type want struct { 24 | filePath string 25 | data []byte 26 | err assert.ErrorAssertionFunc 27 | } 28 | 29 | tests := map[string]struct { 30 | args args 31 | want want 32 | }{ 33 | "normal/existing dir": { 34 | args: args{ 35 | results: []byte("this is an example file.\nthis is for outputBytesToFile function.\n"), 36 | outputDest: "../../test/output", 37 | defaultName: "default", 38 | extension: "txt", 39 | }, 40 | want: want{ 41 | filePath: "../../test/output/default.txt", 42 | data: bytes, 43 | err: assert.NoError, 44 | }, 45 | }, 46 | "normal/file name is provided to outputDest": { 47 | args: args{ 48 | results: []byte("this is an example file.\nthis is for outputBytesToFile function.\n"), 49 | outputDest: "../../test/output/validator_result.json", 50 | defaultName: "default", 51 | extension: "json", 52 | }, 53 | want: want{ 54 | filePath: "../../test/output/validator_result.json", 55 | data: bytes, 56 | err: assert.NoError, 57 | }, 58 | }, 59 | "normal/existing dir without extension": { 60 | args: args{ 61 | results: []byte("this is an example file.\nthis is for outputBytesToFile function.\n"), 62 | outputDest: "../../test/output", 63 | defaultName: "default", 64 | extension: "", 65 | }, 66 | want: want{ 67 | filePath: "../../test/output/default", 68 | data: bytes, 69 | err: assert.NoError, 70 | }, 71 | }, 72 | "abnormal/empty string outputDest": { 73 | args: args{ 74 | results: []byte("this is an example file.\nthis is for outputBytesToFile function.\n"), 75 | outputDest: "", 76 | defaultName: "default", 77 | extension: ".txt", 78 | }, 79 | want: want{ 80 | data: nil, 81 | err: assertRegexpError("outputDest is an empty string: "), 82 | }, 83 | }, 84 | "abnormal/non-existing dir": { 85 | args: args{ 86 | results: []byte("this is an example file.\nthis is for outputBytesToFile function.\n"), 87 | outputDest: "../../test/wrong/output", 88 | defaultName: "result", 89 | extension: "", 90 | }, 91 | want: want{ 92 | data: nil, 93 | err: assertRegexpError("failed to create a file: "), 94 | }, 95 | }, 96 | } 97 | for name, tt := range tests { 98 | t.Run(name, func(t *testing.T) { 99 | err := outputBytesToFile(tt.args.outputDest, 100 | tt.args.defaultName, tt.args.extension, tt.args.results) 101 | tt.want.err(t, err) 102 | if tt.want.data != nil { 103 | bytes, err := os.ReadFile(tt.want.filePath) 104 | require.NoError(t, err) 105 | assert.Equal(t, tt.want.data, bytes) 106 | err = os.Remove(tt.want.filePath) 107 | require.NoError(t, err) 108 | } 109 | }, 110 | ) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /pkg/validator/csv.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "errors" 7 | "io" 8 | ) 9 | 10 | // CsvValidator is used to validate a byte slice that is intended to represent a 11 | // CSV file. 12 | type CsvValidator struct{} 13 | 14 | // Validate checks if the provided byte slice represents a valid .csv file. 15 | // https://pkg.go.dev/encoding/csv 16 | func (CsvValidator) Validate(b []byte) (bool, error) { 17 | csvReader := csv.NewReader(bytes.NewReader(b)) 18 | csvReader.TrimLeadingSpace = true 19 | 20 | for { 21 | _, err := csvReader.Read() 22 | if errors.Is(err, io.EOF) { 23 | break 24 | } 25 | 26 | if err != nil { 27 | return false, err 28 | } 29 | } 30 | 31 | return true, nil 32 | } 33 | -------------------------------------------------------------------------------- /pkg/validator/editorconfig.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/editorconfig/editorconfig-core-go/v2" 7 | ) 8 | 9 | type EditorConfigValidator struct{} 10 | 11 | // Validate implements the Validator interface by attempting to 12 | // parse a byte array of an editorconfig file using editorconfig-core-go package 13 | func (EditorConfigValidator) Validate(b []byte) (bool, error) { 14 | if _, err := editorconfig.Parse(bytes.NewReader(b)); err != nil { 15 | return false, err 16 | } 17 | return true, nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/validator/env.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/hashicorp/go-envparse" 9 | ) 10 | 11 | type EnvValidator struct{} 12 | 13 | // Validate implements the Validator interface by attempting to 14 | // parse a byte array of a env file using envparse package 15 | func (EnvValidator) Validate(b []byte) (bool, error) { 16 | r := bytes.NewReader(b) 17 | _, err := envparse.Parse(r) 18 | if err != nil { 19 | var customError *envparse.ParseError 20 | if errors.As(err, &customError) { 21 | // we can wrap some useful information with the error 22 | err = fmt.Errorf("Error at line %v: %w", customError.Line, customError.Err) 23 | } 24 | return false, err 25 | } 26 | return true, nil 27 | } 28 | -------------------------------------------------------------------------------- /pkg/validator/hcl.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/hcl/v2/hclparse" 7 | ) 8 | 9 | // HclValidator is used to validate a byte slice that is intended to represent a 10 | // HashiCorp Configuration Language (HCL) file. 11 | type HclValidator struct{} 12 | 13 | // Validate checks if the provided byte slice represents a valid .hcl file. 14 | // 15 | // The hcl parser uses FIFO to determine which error to display to the user. For 16 | // more information, see the documentation at: 17 | // 18 | // https://pkg.go.dev/github.com/hashicorp/hcl/v2#Diagnostics.Error 19 | // 20 | // If the hcl.Diagnostics slice contains more than one error, the wrapped 21 | // error returned by this function will include them as "and {count} other 22 | // diagnostic(s)" in the error message. 23 | func (HclValidator) Validate(b []byte) (bool, error) { 24 | _, diags := hclparse.NewParser().ParseHCL(b, "") 25 | if diags == nil { 26 | return true, nil 27 | } 28 | 29 | subject := diags[0].Subject 30 | 31 | row := subject.Start.Line 32 | col := subject.Start.Column 33 | 34 | return false, fmt.Errorf("error at line %v column %v: %w", row, col, diags) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/validator/hocon.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "github.com/gurkankaymak/hocon" 5 | ) 6 | 7 | // HoconValidator is used to validate a byte slice that is intended to represent a 8 | // HOCON file. 9 | type HoconValidator struct{} 10 | 11 | // Validate checks if the provided byte slice represents a valid .hocon file. 12 | func (HoconValidator) Validate(b []byte) (bool, error) { 13 | _, err := hocon.ParseString(string(b)) 14 | if err != nil { 15 | return false, err 16 | } 17 | 18 | return true, nil 19 | } 20 | -------------------------------------------------------------------------------- /pkg/validator/ini.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "gopkg.in/ini.v1" 5 | ) 6 | 7 | type IniValidator struct{} 8 | 9 | // Validate implements the Validator interface by attempting to 10 | // parse a byte array of ini 11 | func (IniValidator) Validate(b []byte) (bool, error) { 12 | _, err := ini.LoadSources(ini.LoadOptions{}, b) 13 | if err != nil { 14 | return false, err 15 | } 16 | return true, nil 17 | } 18 | -------------------------------------------------------------------------------- /pkg/validator/json.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | type JSONValidator struct{} 11 | 12 | // Returns a custom error message that contains the unmarshal 13 | // error message along with the line and character 14 | // number where the error occurred when parsing the JSON 15 | func getCustomErr(input []byte, err error) error { 16 | var jsonError *json.SyntaxError 17 | if !errors.As(err, &jsonError) { 18 | // not a json.SyntaxError 19 | // nothing interesting we can wrap into the error 20 | return err 21 | } 22 | 23 | offset := int(jsonError.Offset) 24 | line := 1 + strings.Count(string(input)[:offset], "\n") 25 | column := 1 + offset - (strings.LastIndex(string(input)[:offset], "\n") + len("\n")) 26 | return fmt.Errorf("Error at line %v column %v: %w", line, column, jsonError) 27 | } 28 | 29 | // Validate implements the Validator interface by attempting to 30 | // unmarshall a byte array of json 31 | func (JSONValidator) Validate(b []byte) (bool, error) { 32 | var output any 33 | err := json.Unmarshal(b, &output) 34 | if err != nil { 35 | customError := getCustomErr(b, err) 36 | return false, customError 37 | } 38 | return true, nil 39 | } 40 | -------------------------------------------------------------------------------- /pkg/validator/plist.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "bytes" 5 | 6 | "howett.net/plist" 7 | ) 8 | 9 | // PlistValidator is used to validate a byte slice that is intended to represent a 10 | // Apple Property List file (plist). 11 | type PlistValidator struct{} 12 | 13 | // Validate checks if the provided byte slice represents a valid .plist file. 14 | func (PlistValidator) Validate(b []byte) (bool, error) { 15 | var output any 16 | plistDecoder := plist.NewDecoder(bytes.NewReader(b)) 17 | err := plistDecoder.Decode(&output) 18 | if err != nil { 19 | return false, err 20 | } 21 | return true, nil 22 | } 23 | -------------------------------------------------------------------------------- /pkg/validator/properties.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "github.com/magiconair/properties" 5 | ) 6 | 7 | type PropValidator struct{} 8 | 9 | // Validate implements the Validator interface by attempting to 10 | // parse a byte array of properties 11 | func (PropValidator) Validate(b []byte) (bool, error) { 12 | l := &properties.Loader{Encoding: properties.UTF8} 13 | _, err := l.LoadBytes(b) 14 | if err != nil { 15 | return false, err 16 | } 17 | return true, nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/validator/toml.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/pelletier/go-toml/v2" 8 | ) 9 | 10 | type TomlValidator struct{} 11 | 12 | func (TomlValidator) Validate(b []byte) (bool, error) { 13 | var output any 14 | err := toml.Unmarshal(b, &output) 15 | var derr *toml.DecodeError 16 | if errors.As(err, &derr) { 17 | row, col := derr.Position() 18 | return false, fmt.Errorf("Error at line %v column %v: %w", row, col, err) 19 | } 20 | return true, nil 21 | } 22 | -------------------------------------------------------------------------------- /pkg/validator/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | // Validator is the interface that wraps the basic Validate method 4 | 5 | // Validate accepts a byte array of a file or string to be validated 6 | // and returns true or false if the content of the byte array is 7 | // valid or not. If it is not valid, the error return value 8 | // will be populated. 9 | type Validator interface { 10 | Validate(b []byte) (bool, error) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/validator/validator_test.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | _ "embed" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | validPlistBytes = []byte(` 10 | 11 | 12 | 13 | CFBundleShortVersionString 14 | 1.0 15 | CFBundleVersion 16 | 1 17 | NSAppTransportSecurity 18 | 19 | NSAllowsArbitraryLoads 20 | 21 | 22 | 23 | `) 24 | 25 | invalidPlistBytes = []byte(` 26 | 27 | 28 | CFBundleShortVersionString 29 | 1.0 30 | CFBundleVersion 31 | 1 32 | NSAppTransporT-Security 33 | 34 | NSAllowsArbitraryLoads 35 | 36 | 37 | `) 38 | ) 39 | 40 | var testData = []struct { 41 | name string 42 | testInput []byte 43 | expectedResult bool 44 | validator Validator 45 | }{ 46 | {"validJson", []byte(`{"test": "test"}`), true, JSONValidator{}}, 47 | {"invalidJson", []byte(`{test": "test"}`), false, JSONValidator{}}, 48 | {"validYaml", []byte("a: 1\nb: 2"), true, YAMLValidator{}}, 49 | {"invalidYaml", []byte("a: b\nc: d:::::::::::::::"), false, YAMLValidator{}}, 50 | {"validXml", []byte("\n"), true, XMLValidator{}}, 51 | {"invalidXml", []byte(" 2 | 3 | 4 | 5 | CFBundleShortVersionString 6 | 1.0 7 | CFBundleVersion 8 | 1 9 | NSAppTransportSecurity 10 | 11 | NSAllowsArbitraryLoads 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/fixtures/good.properties: -------------------------------------------------------------------------------- 1 | # You are reading a comment in ".properties" file. 2 | ! The exclamation mark can also be used for comments. 3 | # Lines with "properties" contain a key and a value separated by a delimiting character. 4 | # There are 3 delimiting characters: '=' (equal), ':' (colon) and whitespace (space, \t and \f). 5 | website = https://en.wikipedia.org/ 6 | language : English 7 | topic .properties files 8 | # A word on a line will just create a key with no value. 9 | empty 10 | # White space that appears between the key, the value and the delimiter is ignored. 11 | # This means that the following are equivalent (other than for readability). 12 | hello=hello 13 | hello = hello 14 | # Keys with the same name will be overwritten by the key that is the furthest in a file. 15 | # For example the final value for "duplicateKey" will be "second". 16 | duplicateKey = first 17 | duplicateKey = second 18 | # To use the delimiter characters inside a key, you need to escape them with a \. 19 | # However, there is no need to do this in the value. 20 | delimiterCharacters\:\=\ = This is the value for the key "delimiterCharacters\:\=\ " 21 | # Adding a \ at the end of a line means that the value continues to the next line. 22 | multiline = This line \ 23 | continues 24 | # If you want your value to include a \, it should be escaped by another \. 25 | path = c:\\wiki\\templates 26 | # This means that if the number of \ at the end of the line is even, the next line is not included in the value. 27 | # In the following example, the value for "evenKey" is "This is on one line\". 28 | evenKey = This is on one line\\ 29 | # This line is a normal comment and is not included in the value for "evenKey" 30 | # If the number of \ is odd, then the next line is included in the value. 31 | # In the following example, the value for "oddKey" is "This is line one and\#This is line two". 32 | oddKey = This is line one and\\\ 33 | # This is line two 34 | # White space characters are removed before each line. 35 | # Make sure to add your spaces before your \ if you need them on the next line. 36 | # In the following example, the value for "welcome" is "Welcome to Wikipedia!". 37 | welcome = Welcome to \ 38 | Wikipedia! 39 | # If you need to add newlines and carriage returns, they need to be escaped using \n and \r respectively. 40 | # You can also optionally escape tabs with \t for readability purposes. 41 | valueWithEscapes = This is a newline\n and a carriage return\r and a tab\t. 42 | # You can also use Unicode escape characters (maximum of four hexadecimal digits). 43 | # In the following example, the value for "encodedHelloInJapanese" is "こんにちは". 44 | encodedHelloInJapanese = \u3053\u3093\u306b\u3061\u306f 45 | # But with more modern file encodings like UTF-8, you can directly use supported characters. 46 | helloInJapanese = こんにちは -------------------------------------------------------------------------------- /test/fixtures/good.toml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. 2 | 3 | title = "TOML Example" 4 | 5 | [owner] 6 | name = "Tom Preston-Werner" 7 | dob = 1979-05-27T07:32:00-08:00 # First class dates 8 | 9 | [database] 10 | server = "192.168.1.1" 11 | ports = [ 8000, 8001, 8002 ] 12 | connection_max = 5000 13 | enabled = true 14 | 15 | [servers] 16 | 17 | # Indentation (tabs and/or spaces) is allowed but not required 18 | [servers.alpha] 19 | ip = "10.0.0.1" 20 | dc = "eqdc10" 21 | 22 | [servers.beta] 23 | ip = "10.0.0.2" 24 | dc = "eqdc10" 25 | 26 | [clients] 27 | data = [ ["gamma", "delta"], [1, 2] ] 28 | 29 | # Line breaks are OK when inside arrays 30 | hosts = [ 31 | "alpha", 32 | "omega" 33 | ] 34 | -------------------------------------------------------------------------------- /test/fixtures/good.yaml: -------------------------------------------------------------------------------- 1 | test: test -------------------------------------------------------------------------------- /test/fixtures/mixedcase-extension/good.CSv: -------------------------------------------------------------------------------- 1 | first_name,last_name,username 2 | Rob,Pike,rob 3 | Ken,Thompson,ken 4 | Robert,Griesemer,gri -------------------------------------------------------------------------------- /test/fixtures/mixedcase-extension/good.HCl: -------------------------------------------------------------------------------- 1 | io_mode = "async" 2 | 3 | service "http" "web_proxy" { 4 | listen_addr = "127.0.0.1:8080" 5 | 6 | process "main" { 7 | command = ["/usr/local/bin/awesome-app", "server"] 8 | } 9 | 10 | process "mgmt" { 11 | command = ["/usr/local/bin/awesome-app", "mgmt"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/mixedcase-extension/good.INi: -------------------------------------------------------------------------------- 1 | [Version] 2 | First=name 3 | second=value 4 | 5 | [Empty] 6 | 7 | [Last] 8 | 1=2 9 | -------------------------------------------------------------------------------- /test/fixtures/mixedcase-extension/good.JSon: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "name": "test" 4 | } 5 | } -------------------------------------------------------------------------------- /test/fixtures/mixedcase-extension/good.PList: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleShortVersionString 6 | 1.0 7 | CFBundleVersion 8 | 1 9 | NSAppTransportSecurity 10 | 11 | NSAllowsArbitraryLoads 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/fixtures/mixedcase-extension/good.PRoperties: -------------------------------------------------------------------------------- 1 | # You are reading a comment in ".properties" file. 2 | ! The exclamation mark can also be used for comments. 3 | # Lines with "properties" contain a key and a value separated by a delimiting character. 4 | # There are 3 delimiting characters: '=' (equal), ':' (colon) and whitespace (space, \t and \f). 5 | website = https://en.wikipedia.org/ 6 | language : English 7 | topic .properties files 8 | # A word on a line will just create a key with no value. 9 | empty 10 | # White space that appears between the key, the value and the delimiter is ignored. 11 | # This means that the following are equivalent (other than for readability). 12 | hello=hello 13 | hello = hello 14 | # Keys with the same name will be overwritten by the key that is the furthest in a file. 15 | # For example the final value for "duplicateKey" will be "second". 16 | duplicateKey = first 17 | duplicateKey = second 18 | # To use the delimiter characters inside a key, you need to escape them with a \. 19 | # However, there is no need to do this in the value. 20 | delimiterCharacters\:\=\ = This is the value for the key "delimiterCharacters\:\=\ " 21 | # Adding a \ at the end of a line means that the value continues to the next line. 22 | multiline = This line \ 23 | continues 24 | # If you want your value to include a \, it should be escaped by another \. 25 | path = c:\\wiki\\templates 26 | # This means that if the number of \ at the end of the line is even, the next line is not included in the value. 27 | # In the following example, the value for "evenKey" is "This is on one line\". 28 | evenKey = This is on one line\\ 29 | # This line is a normal comment and is not included in the value for "evenKey" 30 | # If the number of \ is odd, then the next line is included in the value. 31 | # In the following example, the value for "oddKey" is "This is line one and\#This is line two". 32 | oddKey = This is line one and\\\ 33 | # This is line two 34 | # White space characters are removed before each line. 35 | # Make sure to add your spaces before your \ if you need them on the next line. 36 | # In the following example, the value for "welcome" is "Welcome to Wikipedia!". 37 | welcome = Welcome to \ 38 | Wikipedia! 39 | # If you need to add newlines and carriage returns, they need to be escaped using \n and \r respectively. 40 | # You can also optionally escape tabs with \t for readability purposes. 41 | valueWithEscapes = This is a newline\n and a carriage return\r and a tab\t. 42 | # You can also use Unicode escape characters (maximum of four hexadecimal digits). 43 | # In the following example, the value for "encodedHelloInJapanese" is "こんにちは". 44 | encodedHelloInJapanese = \u3053\u3093\u306b\u3061\u306f 45 | # But with more modern file encodings like UTF-8, you can directly use supported characters. 46 | helloInJapanese = こんにちは -------------------------------------------------------------------------------- /test/fixtures/mixedcase-extension/good.TOml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. 2 | 3 | title = "TOML Example" 4 | 5 | [owner] 6 | name = "Tom Preston-Werner" 7 | dob = 1979-05-27T07:32:00-08:00 # First class dates 8 | 9 | [database] 10 | server = "192.168.1.1" 11 | ports = [ 8000, 8001, 8002 ] 12 | connection_max = 5000 13 | enabled = true 14 | 15 | [servers] 16 | 17 | # Indentation (tabs and/or spaces) is allowed but not required 18 | [servers.alpha] 19 | ip = "10.0.0.1" 20 | dc = "eqdc10" 21 | 22 | [servers.beta] 23 | ip = "10.0.0.2" 24 | dc = "eqdc10" 25 | 26 | [clients] 27 | data = [ ["gamma", "delta"], [1, 2] ] 28 | 29 | # Line breaks are OK when inside arrays 30 | hosts = [ 31 | "alpha", 32 | "omega" 33 | ] 34 | -------------------------------------------------------------------------------- /test/fixtures/mixedcase-extension/good.YAml: -------------------------------------------------------------------------------- 1 | test: test -------------------------------------------------------------------------------- /test/fixtures/subdir/bad.json: -------------------------------------------------------------------------------- 1 | { 2 | "names": [ 3 | "name": "test1" 4 | ] 5 | } -------------------------------------------------------------------------------- /test/fixtures/subdir/bad.toml: -------------------------------------------------------------------------------- 1 | # This is a bad TOML document. 2 | 3 | title = "TOML Example" 4 | 5 | [owner] 6 | name "Tom Preston-Werner" 7 | dob = 1979-05-27T07:32:00-08:00 # First class dates 8 | 9 | [database] 10 | server = "192.168.1.1" 11 | ports = [ 8000, 8001, 8002 ] 12 | connection_max = 5000 13 | enabled = true -------------------------------------------------------------------------------- /test/fixtures/subdir/bad.yml: -------------------------------------------------------------------------------- 1 | test: 1 2 | test2: 2 3 | tests: 4 | - test1: test2 5 | - -------------------------------------------------------------------------------- /test/fixtures/subdir/good.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "name": "test" 4 | } 5 | } -------------------------------------------------------------------------------- /test/fixtures/subdir2/bad.csv: -------------------------------------------------------------------------------- 1 | This string has a \" in it -------------------------------------------------------------------------------- /test/fixtures/subdir2/bad.editorconfig: -------------------------------------------------------------------------------- 1 | [*.md 2 | working = false 3 | -------------------------------------------------------------------------------- /test/fixtures/subdir2/bad.env: -------------------------------------------------------------------------------- 1 | TEST=True 2 | DEMO="\a" 3 | -------------------------------------------------------------------------------- /test/fixtures/subdir2/bad.hcl: -------------------------------------------------------------------------------- 1 | "key" = "value" 2 | -------------------------------------------------------------------------------- /test/fixtures/subdir2/bad.hocon: -------------------------------------------------------------------------------- 1 | database { 2 | host = "localhost" 3 | port = [3306,,] 4 | user = "admin" 5 | password = "secret" 6 | } 7 | 8 | // substitution cycle 9 | bar : ${foo} 10 | foo : ${bar} 11 | -------------------------------------------------------------------------------- /test/fixtures/subdir2/bad.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | name value 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/subdir2/bad.json: -------------------------------------------------------------------------------- 1 | [}{}] -------------------------------------------------------------------------------- /test/fixtures/subdir2/bad.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleShortVersionString 5 | 1.0 6 | CFBundleVersion 7 | 1 8 | NSAppTransporT-Security 9 | 10 | NSAllowsArbitraryLoads 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/fixtures/subdir2/bad.properties: -------------------------------------------------------------------------------- 1 | # You are reading a comment in ".properties" file. 2 | ! The exclamation mark can also be used for comments. 3 | # Lines with "properties" contain a key and a value separated by a delimiting character. 4 | # There are 3 delimiting characters: '=' (equal), ':' (colon) and whitespace (space, \t and \f). 5 | website = https://en.wikipedia.org/ 6 | language : English 7 | topic .properties files 8 | # A word on a line will just create a key with no value. 9 | empty 10 | # White space that appears between the key, the value and the delimiter is ignored. 11 | # This means that the following are equivalent (other than for readability). 12 | hello=hello 13 | hello = hello 14 | # Keys with the same name will be overwritten by the key that is the furthest in a file. 15 | # For example the final value for "duplicateKey" will be "second". 16 | duplicateKey = first 17 | duplicateKey = second 18 | # To use the delimiter characters inside a key, you need to escape them with a \. 19 | # However, there is no need to do this in the value. 20 | delimiterCharacters\:\=\ = This is the value for the key "delimiterCharacters\:\=\ " 21 | # Adding a \ at the end of a line means that the value continues to the next line. 22 | multiline = This line \ 23 | continues 24 | # If you want your value to include a \, it should be escaped by another \. 25 | path = c:\\wiki\\templates 26 | # This means that if the number of \ at the end of the line is even, the next line is not included in the value. 27 | # In the following example, the value for "evenKey" is "This is on one line\". 28 | evenKey = This is on one line\\ 29 | # This line is a normal comment and is not included in the value for "evenKey" 30 | # If the number of \ is odd, then the next line is included in the value. 31 | # In the following example, the value for "oddKey" is "This is line one and\#This is line two". 32 | oddKey = This is line one and\\\ 33 | # This is line two 34 | # White space characters are removed before each line. 35 | # Make sure to add your spaces before your \ if you need them on the next line. 36 | # In the following example, the value for "welcome" is "Welcome to Wikipedia!". 37 | welcome = Welcome to \ 38 | Wikipedia! 39 | # If you need to add newlines and carriage returns, they need to be escaped using \n and \r respectively. 40 | # You can also optionally escape tabs with \t for readability purposes. 41 | valueWithEscapes = This is a newline\n and a carriage return\r and a tab\t. 42 | # You can also use Unicode escape characters (maximum of four hexadecimal digits). 43 | # In the following example, the value for "encodedHelloInJapanese" is "こんにちは". 44 | encodedHelloInJapanese = \u3053\u3093\u306b\u3061\u306f 45 | # But with more modern file encodings like UTF-8, you can directly use supported characters. 46 | helloInJapanese = こんにちは 47 | # Circular Dependency 48 | key = ${key} 49 | -------------------------------------------------------------------------------- /test/fixtures/subdir2/good.ini: -------------------------------------------------------------------------------- 1 | [Version] 2 | First=name 3 | second=value 4 | 5 | [Empty] 6 | 7 | [Last] 8 | 1=2 9 | -------------------------------------------------------------------------------- /test/fixtures/subdir2/good.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "name": "test" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/uppercase-extension/good.CSV: -------------------------------------------------------------------------------- 1 | first_name,last_name,username 2 | Rob,Pike,rob 3 | Ken,Thompson,ken 4 | Robert,Griesemer,gri -------------------------------------------------------------------------------- /test/fixtures/uppercase-extension/good.HCL: -------------------------------------------------------------------------------- 1 | io_mode = "async" 2 | 3 | service "http" "web_proxy" { 4 | listen_addr = "127.0.0.1:8080" 5 | 6 | process "main" { 7 | command = ["/usr/local/bin/awesome-app", "server"] 8 | } 9 | 10 | process "mgmt" { 11 | command = ["/usr/local/bin/awesome-app", "mgmt"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/uppercase-extension/good.INI: -------------------------------------------------------------------------------- 1 | [Version] 2 | First=name 3 | second=value 4 | 5 | [Empty] 6 | 7 | [Last] 8 | 1=2 9 | -------------------------------------------------------------------------------- /test/fixtures/uppercase-extension/good.JSON: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "name": "test" 4 | } 5 | } -------------------------------------------------------------------------------- /test/fixtures/uppercase-extension/good.PLIST: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleShortVersionString 6 | 1.0 7 | CFBundleVersion 8 | 1 9 | NSAppTransportSecurity 10 | 11 | NSAllowsArbitraryLoads 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/fixtures/uppercase-extension/good.PROPERTIES: -------------------------------------------------------------------------------- 1 | # You are reading a comment in ".properties" file. 2 | ! The exclamation mark can also be used for comments. 3 | # Lines with "properties" contain a key and a value separated by a delimiting character. 4 | # There are 3 delimiting characters: '=' (equal), ':' (colon) and whitespace (space, \t and \f). 5 | website = https://en.wikipedia.org/ 6 | language : English 7 | topic .properties files 8 | # A word on a line will just create a key with no value. 9 | empty 10 | # White space that appears between the key, the value and the delimiter is ignored. 11 | # This means that the following are equivalent (other than for readability). 12 | hello=hello 13 | hello = hello 14 | # Keys with the same name will be overwritten by the key that is the furthest in a file. 15 | # For example the final value for "duplicateKey" will be "second". 16 | duplicateKey = first 17 | duplicateKey = second 18 | # To use the delimiter characters inside a key, you need to escape them with a \. 19 | # However, there is no need to do this in the value. 20 | delimiterCharacters\:\=\ = This is the value for the key "delimiterCharacters\:\=\ " 21 | # Adding a \ at the end of a line means that the value continues to the next line. 22 | multiline = This line \ 23 | continues 24 | # If you want your value to include a \, it should be escaped by another \. 25 | path = c:\\wiki\\templates 26 | # This means that if the number of \ at the end of the line is even, the next line is not included in the value. 27 | # In the following example, the value for "evenKey" is "This is on one line\". 28 | evenKey = This is on one line\\ 29 | # This line is a normal comment and is not included in the value for "evenKey" 30 | # If the number of \ is odd, then the next line is included in the value. 31 | # In the following example, the value for "oddKey" is "This is line one and\#This is line two". 32 | oddKey = This is line one and\\\ 33 | # This is line two 34 | # White space characters are removed before each line. 35 | # Make sure to add your spaces before your \ if you need them on the next line. 36 | # In the following example, the value for "welcome" is "Welcome to Wikipedia!". 37 | welcome = Welcome to \ 38 | Wikipedia! 39 | # If you need to add newlines and carriage returns, they need to be escaped using \n and \r respectively. 40 | # You can also optionally escape tabs with \t for readability purposes. 41 | valueWithEscapes = This is a newline\n and a carriage return\r and a tab\t. 42 | # You can also use Unicode escape characters (maximum of four hexadecimal digits). 43 | # In the following example, the value for "encodedHelloInJapanese" is "こんにちは". 44 | encodedHelloInJapanese = \u3053\u3093\u306b\u3061\u306f 45 | # But with more modern file encodings like UTF-8, you can directly use supported characters. 46 | helloInJapanese = こんにちは -------------------------------------------------------------------------------- /test/fixtures/uppercase-extension/good.TOML: -------------------------------------------------------------------------------- 1 | # This is a TOML document. 2 | 3 | title = "TOML Example" 4 | 5 | [owner] 6 | name = "Tom Preston-Werner" 7 | dob = 1979-05-27T07:32:00-08:00 # First class dates 8 | 9 | [database] 10 | server = "192.168.1.1" 11 | ports = [ 8000, 8001, 8002 ] 12 | connection_max = 5000 13 | enabled = true 14 | 15 | [servers] 16 | 17 | # Indentation (tabs and/or spaces) is allowed but not required 18 | [servers.alpha] 19 | ip = "10.0.0.1" 20 | dc = "eqdc10" 21 | 22 | [servers.beta] 23 | ip = "10.0.0.2" 24 | dc = "eqdc10" 25 | 26 | [clients] 27 | data = [ ["gamma", "delta"], [1, 2] ] 28 | 29 | # Line breaks are OK when inside arrays 30 | hosts = [ 31 | "alpha", 32 | "omega" 33 | ] 34 | -------------------------------------------------------------------------------- /test/fixtures/uppercase-extension/good.YAML: -------------------------------------------------------------------------------- 1 | test: test -------------------------------------------------------------------------------- /test/fixtures/with-depth/additional-depth/sample.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/fixtures/with-depth/sample.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/fixtures/wrong_ext.jason: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Boeing/config-file-validator/fe4044f9d2449183d6e41ef6d65d383b1aad6748/test/fixtures/wrong_ext.jason -------------------------------------------------------------------------------- /test/output/example/good.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "name": "test" 4 | } 5 | } -------------------------------------------------------------------------------- /test/output/example/result.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | { 4 | "path": "test/output/example/good.json", 5 | "status": "passed" 6 | } 7 | ], 8 | "summary": { 9 | "passed": 1, 10 | "failed": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/output/example/result.sarif: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.1.0", 3 | "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json", 4 | "runs": [ 5 | { 6 | "tool": { 7 | "driver": { 8 | "name": "config-file-validator", 9 | "informationUri": "https://github.com/Boeing/config-file-validator", 10 | "version": "1.8.0" 11 | } 12 | }, 13 | "results": [ 14 | { 15 | "kind": "pass", 16 | "level": "none", 17 | "message": { 18 | "text": "No errors detected" 19 | }, 20 | "locations": [ 21 | { 22 | "physicalLocation": { 23 | "artifactLocation": { 24 | "uri": "file:///test/output/example/good.json" 25 | } 26 | } 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /test/output/example/result.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/output/example/writer_example.txt: -------------------------------------------------------------------------------- 1 | this is an example file. 2 | this is for outputBytesToFile function. 3 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package configfilevalidator 2 | 3 | import "fmt" 4 | 5 | // Version information set by link flags during build. We fall back to these sane 6 | // default values when not provided 7 | var ( 8 | version = "unknown" 9 | ) 10 | 11 | // Version contains config-file-validator version information 12 | type Version struct { 13 | Version string 14 | } 15 | 16 | // String outputs the version as a string 17 | func (v Version) String() string { 18 | return fmt.Sprintf("validator version %v", v.Version) 19 | } 20 | 21 | // GetVersion returns the version information 22 | func GetVersion() Version { 23 | return Version{ 24 | Version: version, 25 | } 26 | } 27 | --------------------------------------------------------------------------------