├── .github ├── dependabot.yml └── workflows │ ├── release.yaml │ └── test-and-build.yaml ├── .gitignore ├── .goreleaser.yaml ├── Dockerfile ├── Dockerfile.goreleaser ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── internal └── exporter │ ├── exporter.go │ └── exporter_test.go └── main.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: weekly 12 | open-pull-requests-limit: 10 13 | - package-ecosystem: docker 14 | directory: / 15 | schedule: 16 | interval: weekly 17 | open-pull-requests-limit: 10 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | jobs: 8 | goreleaser: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | packages: write 12 | contents: write 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Fetch all tags 20 | run: git fetch --force --tags 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 24 | 25 | - name: Login to ghcr.io 26 | uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc 27 | with: 28 | registry: ghcr.io 29 | username: ${{ github.actor }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Set up Go 33 | uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 34 | with: 35 | go-version: "1.20.x" 36 | 37 | - name: Run GoReleaser 38 | uses: goreleaser/goreleaser-action@336e29918d653399e599bfca99fadc1d7ffbc9f7 39 | with: 40 | args: release --rm-dist 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/test-and-build.yaml: -------------------------------------------------------------------------------- 1 | name: test-and-build 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - 'README.md' 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: read 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 14 | 15 | - name: Unshallow 16 | run: git fetch --prune --unshallow 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 20 | with: 21 | go-version: "1.20.x" 22 | 23 | - name: Test 24 | run: go test -short -v ./... 25 | 26 | goreleaser: 27 | runs-on: ubuntu-latest 28 | permissions: 29 | contents: read 30 | packages: write 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 34 | 35 | - name: Unshallow 36 | run: git fetch --prune --unshallow 37 | 38 | - name: Set up Go 39 | uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 40 | with: 41 | go-version: "1.20.x" 42 | 43 | - name: Run GoReleaser 44 | uses: goreleaser/goreleaser-action@336e29918d653399e599bfca99fadc1d7ffbc9f7 45 | with: 46 | args: --snapshot --skip-sign --skip-validate --skip-publish --rm-dist 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/go,goland+all,intellij+all 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=go,goland+all,intellij+all 3 | 4 | ### Go ### 5 | # If you prefer the allow list template instead of the deny list, see community template: 6 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 7 | # 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, built with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Dependency directories (remove the comment below to include it) 22 | # vendor/ 23 | 24 | # Go workspace file 25 | go.work 26 | 27 | ### GoLand+all ### 28 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 29 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 30 | 31 | # User-specific stuff 32 | .idea/**/workspace.xml 33 | .idea/**/tasks.xml 34 | .idea/**/usage.statistics.xml 35 | .idea/**/dictionaries 36 | .idea/**/shelf 37 | 38 | # AWS User-specific 39 | .idea/**/aws.xml 40 | 41 | # Generated files 42 | .idea/**/contentModel.xml 43 | 44 | # Sensitive or high-churn files 45 | .idea/**/dataSources/ 46 | .idea/**/dataSources.ids 47 | .idea/**/dataSources.local.xml 48 | .idea/**/sqlDataSources.xml 49 | .idea/**/dynamic.xml 50 | .idea/**/uiDesigner.xml 51 | .idea/**/dbnavigator.xml 52 | 53 | # Gradle 54 | .idea/**/gradle.xml 55 | .idea/**/libraries 56 | 57 | # Gradle and Maven with auto-import 58 | # When using Gradle or Maven with auto-import, you should exclude module files, 59 | # since they will be recreated, and may cause churn. Uncomment if using 60 | # auto-import. 61 | # .idea/artifacts 62 | # .idea/compiler.xml 63 | # .idea/jarRepositories.xml 64 | # .idea/modules.xml 65 | # .idea/*.iml 66 | # .idea/modules 67 | # *.iml 68 | # *.ipr 69 | 70 | # CMake 71 | cmake-build-*/ 72 | 73 | # Mongo Explorer plugin 74 | .idea/**/mongoSettings.xml 75 | 76 | # File-based project format 77 | *.iws 78 | 79 | # IntelliJ 80 | out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # SonarLint plugin 92 | .idea/sonarlint/ 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | # Editor-based Rest Client 101 | .idea/httpRequests 102 | 103 | # Android studio 3.1+ serialized cache file 104 | .idea/caches/build_file_checksums.ser 105 | 106 | ### GoLand+all Patch ### 107 | # Ignore everything but code style settings and run configurations 108 | # that are supposed to be shared within teams. 109 | 110 | .idea/* 111 | 112 | !.idea/codeStyles 113 | !.idea/runConfigurations 114 | 115 | ### Intellij+all ### 116 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 117 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 118 | 119 | # User-specific stuff 120 | 121 | # AWS User-specific 122 | 123 | # Generated files 124 | 125 | # Sensitive or high-churn files 126 | 127 | # Gradle 128 | 129 | # Gradle and Maven with auto-import 130 | # When using Gradle or Maven with auto-import, you should exclude module files, 131 | # since they will be recreated, and may cause churn. Uncomment if using 132 | # auto-import. 133 | # .idea/artifacts 134 | # .idea/compiler.xml 135 | # .idea/jarRepositories.xml 136 | # .idea/modules.xml 137 | # .idea/*.iml 138 | # .idea/modules 139 | # *.iml 140 | # *.ipr 141 | 142 | # CMake 143 | 144 | # Mongo Explorer plugin 145 | 146 | # File-based project format 147 | 148 | # IntelliJ 149 | 150 | # mpeltonen/sbt-idea plugin 151 | 152 | # JIRA plugin 153 | 154 | # Cursive Clojure plugin 155 | 156 | # SonarLint plugin 157 | 158 | # Crashlytics plugin (for Android Studio and IntelliJ) 159 | 160 | # Editor-based Rest Client 161 | 162 | # Android studio 3.1+ serialized cache file 163 | 164 | ### Intellij+all Patch ### 165 | # Ignore everything but code style settings and run configurations 166 | # that are supposed to be shared within teams. 167 | 168 | 169 | 170 | # End of https://www.toptal.com/developers/gitignore/api/go,goland+all,intellij+all 171 | 172 | dependency-track-exporter 173 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - binary: dependency-track-exporter 3 | env: 4 | - CGO_ENABLED=0 5 | goos: 6 | - linux 7 | - darwin 8 | - windows 9 | goarch: 10 | - "386" 11 | - amd64 12 | - arm 13 | - arm64 14 | - mips64le 15 | flags: 16 | - -v 17 | ldflags: | 18 | -X github.com/prometheus/common/version.Version={{.Version}} 19 | -X github.com/prometheus/common/version.Revision={{.Commit}} 20 | -X github.com/prometheus/common/version.Branch={{.Branch}} 21 | -X github.com/prometheus/common/version.BuildDate={{.Date}} 22 | release: 23 | github: 24 | owner: jetstack 25 | name: dependency-track-exporter 26 | dockers: 27 | - image_templates: 28 | - "ghcr.io/jetstack/dependency-track-exporter:{{.Version}}-amd64" 29 | dockerfile: Dockerfile.goreleaser 30 | use: buildx 31 | build_flag_templates: 32 | - "--pull" 33 | - "--label=org.opencontainers.image.created={{.Date}}" 34 | - "--label=org.opencontainers.image.name={{.ProjectName}}" 35 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 36 | - "--label=org.opencontainers.image.version={{.Version}}" 37 | - "--label=org.opencontainers.image.source={{.GitURL}}" 38 | - "--platform=linux/amd64" 39 | - image_templates: 40 | - "ghcr.io/jetstack/dependency-track-exporter:{{.Version}}-arm64" 41 | dockerfile: Dockerfile.goreleaser 42 | use: buildx 43 | build_flag_templates: 44 | - "--pull" 45 | - "--label=org.opencontainers.image.created={{.Date}}" 46 | - "--label=org.opencontainers.image.name={{.ProjectName}}" 47 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 48 | - "--label=org.opencontainers.image.version={{.Version}}" 49 | - "--label=org.opencontainers.image.source={{.GitURL}}" 50 | - "--platform=linux/arm64" 51 | goarch: arm64 52 | docker_manifests: 53 | - name_template: "ghcr.io/jetstack/dependency-track-exporter:{{.Version}}" 54 | image_templates: 55 | - "ghcr.io/jetstack/dependency-track-exporter:{{.Version}}-amd64" 56 | - "ghcr.io/jetstack/dependency-track-exporter:{{.Version}}-arm64" 57 | - name_template: "ghcr.io/jetstack/dependency-track-exporter:latest" 58 | image_templates: 59 | - "ghcr.io/jetstack/dependency-track-exporter:{{.Version}}-amd64" 60 | - "ghcr.io/jetstack/dependency-track-exporter:{{.Version}}-arm64" 61 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-buster AS build 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod ./ 6 | 7 | COPY go.sum ./ 8 | 9 | RUN go mod download 10 | 11 | COPY *.go ./ 12 | 13 | COPY internal ./internal 14 | 15 | RUN CGO_ENABLED=0 go build -o /dependency-track-exporter 16 | 17 | FROM gcr.io/distroless/static:nonroot 18 | 19 | WORKDIR / 20 | 21 | COPY --from=build /dependency-track-exporter /dependency-track-exporter 22 | 23 | EXPOSE 9916 24 | 25 | USER nonroot:nonroot 26 | 27 | ENTRYPOINT ["/dependency-track-exporter"] 28 | -------------------------------------------------------------------------------- /Dockerfile.goreleaser: -------------------------------------------------------------------------------- 1 | FROM gcr.io/distroless/static:nonroot 2 | 3 | COPY dependency-track-exporter / 4 | 5 | USER nonroot 6 | 7 | ENTRYPOINT ["/dependency-track-exporter"] 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dependency-Track Exporter 2 | 3 | Exports Prometheus metrics for [Dependency-Track](https://dependencytrack.org/). 4 | 5 | ## Usage 6 | 7 | ``` 8 | usage: dependency-track-exporter [] 9 | 10 | Flags: 11 | -h, --help Show context-sensitive help (also try --help-long and --help-man). 12 | --web.config.file="" [EXPERIMENTAL] Path to configuration file that can enable TLS or authentication. 13 | --web.listen-address=":9916" 14 | Address to listen on for web interface and telemetry. 15 | --web.metrics-path="/metrics" 16 | Path under which to expose metrics 17 | --dtrack.address=DTRACK.ADDRESS 18 | Dependency-Track server address (default: http://localhost:8080 or $DEPENDENCY_TRACK_ADDR) 19 | --dtrack.api-key=DTRACK.API-KEY 20 | Dependency-Track API key (default: $DEPENDENCY_TRACK_API_KEY) 21 | --log.level=info Only log messages with the given severity or above. One of: [debug, info, warn, error] 22 | --log.format=logfmt Output format of log messages. One of: [logfmt, json] 23 | --version Show application version. 24 | ``` 25 | 26 | The API key the exporter uses needs to have the following permissions: 27 | - `VIEW_POLICY_VIOLATION` 28 | - `VIEW_PORTFOLIO` 29 | 30 | ## Metrics 31 | 32 | | Metric | Meaning | Labels | 33 | | ----------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------ | 34 | | dependency_track_portfolio_inherited_risk_score | The inherited risk score of the whole portfolio. | | 35 | | dependency_track_portfolio_vulnerabilities | Number of vulnerabilities across the whole portfolio, by severity. | severity | 36 | | dependency_track_portfolio_findings | Number of findings across the whole portfolio, audited and unaudited. | audited | 37 | | dependency_track_project_info | Project information. | uuid, name, version, active, tags | 38 | | dependency_track_project_vulnerabilities | Number of vulnerabilities for a project by severity. | uuid, name, version, severity | 39 | | dependency_track_project_policy_violations | Policy violations for a project. | uuid, name, version, state, analysis, suppressed | 40 | | dependency_track_project_last_bom_import | Last BOM import date, represented as a Unix timestamp. | uuid, name, version | 41 | | dependency_track_project_inherited_risk_score | Inherited risk score for a project. | uuid, name, version | 42 | 43 | ## Example queries 44 | 45 | Retrieve the number of `WARN` policy violations that have not been analyzed or 46 | suppressed: 47 | 48 | ``` 49 | dependency_track_project_policy_violations{state="WARN",analysis!="APPROVED",analysis!="REJECTED",suppressed="false"} > 0 50 | ``` 51 | 52 | Exclude inactive projects: 53 | 54 | ``` 55 | dependency_track_project_policy_violations{state="WARN",analysis!="APPROVED",analysis!="REJECTED",suppressed="false"} > 0 56 | and on(uuid) dependency_track_project_info{active="true"} 57 | ``` 58 | 59 | Only include projects tagged with `prod`: 60 | 61 | ``` 62 | dependency_track_project_policy_violations{state="WARN",analysis!="APPROVED",analysis!="REJECTED",suppressed="false"} > 0 63 | and on(uuid) dependency_track_project_info{active="true",tags=~".*,prod,.*"} 64 | ``` 65 | 66 | Or, join the tags label into the returned series. Filtering on active/tag could 67 | then happen in alert routes: 68 | 69 | ``` 70 | (dependency_track_project_policy_violations{state="WARN",analysis!="APPROVED",analysis!="REJECTED",suppressed="false"} > 0) 71 | * on (uuid) group_left(tags,active) dependency_track_project_info 72 | ``` 73 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jetstack/dependency-track-exporter 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/DependencyTrack/client-go v0.11.0 7 | github.com/alecthomas/kingpin/v2 v2.3.2 8 | github.com/go-kit/log v0.2.1 9 | github.com/google/go-cmp v0.5.9 10 | github.com/google/uuid v1.3.0 11 | github.com/prometheus/client_golang v1.16.0 12 | github.com/prometheus/common v0.44.0 13 | github.com/prometheus/exporter-toolkit v0.10.0 14 | ) 15 | 16 | require ( 17 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 18 | github.com/beorn7/perks v1.0.1 // indirect 19 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 20 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 21 | github.com/go-logfmt/logfmt v0.5.1 // indirect 22 | github.com/golang/protobuf v1.5.3 // indirect 23 | github.com/jpillora/backoff v1.0.0 // indirect 24 | github.com/kr/text v0.2.0 // indirect 25 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 26 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 27 | github.com/prometheus/client_model v0.4.0 // indirect 28 | github.com/prometheus/procfs v0.10.1 // indirect 29 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 30 | golang.org/x/crypto v0.8.0 // indirect 31 | golang.org/x/net v0.10.0 // indirect 32 | golang.org/x/oauth2 v0.8.0 // indirect 33 | golang.org/x/sync v0.2.0 // indirect 34 | golang.org/x/sys v0.8.0 // indirect 35 | golang.org/x/text v0.9.0 // indirect 36 | google.golang.org/appengine v1.6.7 // indirect 37 | google.golang.org/protobuf v1.30.0 // indirect 38 | gopkg.in/yaml.v2 v2.4.0 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DependencyTrack/client-go v0.11.0 h1:1g+eHC8nJyIzi68zcs+dr3OHRvS1aC+4Uy3YKA0JJhc= 2 | github.com/DependencyTrack/client-go v0.11.0/go.mod h1:XLZnOksOs56Svq+K4xmBkN8U97gpP7r1BkhCc/xA8Iw= 3 | github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU= 4 | github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= 5 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 6 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 7 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 8 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 9 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 10 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 12 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 13 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= 17 | github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 18 | github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= 19 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 20 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 21 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 22 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 23 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 24 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 25 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 26 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 27 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 28 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 29 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 30 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 31 | github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= 32 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 33 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 34 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 35 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 36 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 37 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 38 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 39 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= 40 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 41 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 42 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 43 | github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= 44 | github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= 45 | github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= 46 | github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= 47 | github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= 48 | github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= 49 | github.com/prometheus/exporter-toolkit v0.10.0 h1:yOAzZTi4M22ZzVxD+fhy1URTuNRj/36uQJJ5S8IPza8= 50 | github.com/prometheus/exporter-toolkit v0.10.0/go.mod h1:+sVFzuvV5JDyw+Ih6p3zFxZNVnKQa3x5qPmDSiPu4ZY= 51 | github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= 52 | github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= 53 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 54 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 55 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 56 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 57 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 58 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= 59 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 60 | golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= 61 | golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= 62 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 63 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= 64 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 65 | golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= 66 | golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= 67 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 68 | golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= 69 | golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 71 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 72 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 74 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 75 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 76 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 77 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 78 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 79 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 80 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 81 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 82 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 83 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 84 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 86 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 87 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 88 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 89 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 90 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 91 | -------------------------------------------------------------------------------- /internal/exporter/exporter.go: -------------------------------------------------------------------------------- 1 | package exporter 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | 9 | dtrack "github.com/DependencyTrack/client-go" 10 | "github.com/go-kit/log" 11 | "github.com/go-kit/log/level" 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/prometheus/client_golang/prometheus/promhttp" 14 | ) 15 | 16 | const ( 17 | // Namespace is the metrics namespace of the exporter 18 | Namespace string = "dependency_track" 19 | ) 20 | 21 | // Exporter exports metrics from a Dependency-Track server 22 | type Exporter struct { 23 | Client *dtrack.Client 24 | Logger log.Logger 25 | } 26 | 27 | // HandlerFunc handles requests to /metrics 28 | func (e *Exporter) HandlerFunc() http.HandlerFunc { 29 | return func(w http.ResponseWriter, r *http.Request) { 30 | registry := prometheus.NewRegistry() 31 | 32 | if err := e.collectPortfolioMetrics(r.Context(), registry); err != nil { 33 | level.Error(e.Logger).Log("err", err) 34 | http.Error(w, fmt.Sprintf("error: %s", err), http.StatusInternalServerError) 35 | return 36 | } 37 | 38 | if err := e.collectProjectMetrics(r.Context(), registry); err != nil { 39 | level.Error(e.Logger).Log("err", err) 40 | http.Error(w, fmt.Sprintf("error: %s", err), http.StatusInternalServerError) 41 | return 42 | } 43 | 44 | // Serve 45 | h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) 46 | h.ServeHTTP(w, r) 47 | } 48 | } 49 | 50 | func (e *Exporter) collectPortfolioMetrics(ctx context.Context, registry *prometheus.Registry) error { 51 | var ( 52 | inheritedRiskScore = prometheus.NewGauge( 53 | prometheus.GaugeOpts{ 54 | Name: prometheus.BuildFQName(Namespace, "portfolio", "inherited_risk_score"), 55 | Help: "The inherited risk score of the whole portfolio.", 56 | }, 57 | ) 58 | vulnerabilities = prometheus.NewGaugeVec( 59 | prometheus.GaugeOpts{ 60 | Name: prometheus.BuildFQName(Namespace, "portfolio", "vulnerabilities"), 61 | Help: "Number of vulnerabilities across the whole portfolio, by severity.", 62 | }, 63 | []string{ 64 | "severity", 65 | }, 66 | ) 67 | findings = prometheus.NewGaugeVec( 68 | prometheus.GaugeOpts{ 69 | Name: prometheus.BuildFQName(Namespace, "portfolio", "findings"), 70 | Help: "Number of findings across the whole portfolio, audited and unaudited.", 71 | }, 72 | []string{ 73 | "audited", 74 | }, 75 | ) 76 | ) 77 | registry.MustRegister( 78 | inheritedRiskScore, 79 | vulnerabilities, 80 | findings, 81 | ) 82 | 83 | portfolioMetrics, err := e.Client.Metrics.LatestPortfolioMetrics(ctx) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | inheritedRiskScore.Set(portfolioMetrics.InheritedRiskScore) 89 | 90 | severities := map[string]int{ 91 | "CRITICAL": portfolioMetrics.Critical, 92 | "HIGH": portfolioMetrics.High, 93 | "MEDIUM": portfolioMetrics.Medium, 94 | "LOW": portfolioMetrics.Low, 95 | "UNASSIGNED": portfolioMetrics.Unassigned, 96 | } 97 | for severity, v := range severities { 98 | vulnerabilities.With(prometheus.Labels{ 99 | "severity": severity, 100 | }).Set(float64(v)) 101 | } 102 | 103 | findingsAudited := map[string]int{ 104 | "true": portfolioMetrics.FindingsAudited, 105 | "false": portfolioMetrics.FindingsUnaudited, 106 | } 107 | for audited, v := range findingsAudited { 108 | findings.With(prometheus.Labels{ 109 | "audited": audited, 110 | }).Set(float64(v)) 111 | } 112 | 113 | return nil 114 | } 115 | 116 | func (e *Exporter) collectProjectMetrics(ctx context.Context, registry *prometheus.Registry) error { 117 | var ( 118 | info = prometheus.NewGaugeVec( 119 | prometheus.GaugeOpts{ 120 | Name: prometheus.BuildFQName(Namespace, "project", "info"), 121 | Help: "Project information.", 122 | }, 123 | []string{ 124 | "uuid", 125 | "name", 126 | "version", 127 | "classifier", 128 | "active", 129 | "tags", 130 | }, 131 | ) 132 | vulnerabilities = prometheus.NewGaugeVec( 133 | prometheus.GaugeOpts{ 134 | Name: prometheus.BuildFQName(Namespace, "project", "vulnerabilities"), 135 | Help: "Number of vulnerabilities for a project by severity.", 136 | }, 137 | []string{ 138 | "uuid", 139 | "name", 140 | "version", 141 | "severity", 142 | }, 143 | ) 144 | policyViolations = prometheus.NewGaugeVec( 145 | prometheus.GaugeOpts{ 146 | Name: prometheus.BuildFQName(Namespace, "project", "policy_violations"), 147 | Help: "Policy violations for a project.", 148 | }, 149 | []string{ 150 | "uuid", 151 | "name", 152 | "version", 153 | "type", 154 | "state", 155 | "analysis", 156 | "suppressed", 157 | }, 158 | ) 159 | lastBOMImport = prometheus.NewGaugeVec( 160 | prometheus.GaugeOpts{ 161 | Name: prometheus.BuildFQName(Namespace, "project", "last_bom_import"), 162 | Help: "Last BOM import date, represented as a Unix timestamp.", 163 | }, 164 | []string{ 165 | "uuid", 166 | "name", 167 | "version", 168 | }, 169 | ) 170 | inheritedRiskScore = prometheus.NewGaugeVec( 171 | prometheus.GaugeOpts{ 172 | Name: prometheus.BuildFQName(Namespace, "project", "inherited_risk_score"), 173 | Help: "Inherited risk score for a project.", 174 | }, 175 | []string{ 176 | "uuid", 177 | "name", 178 | "version", 179 | }, 180 | ) 181 | ) 182 | registry.MustRegister( 183 | info, 184 | vulnerabilities, 185 | policyViolations, 186 | lastBOMImport, 187 | inheritedRiskScore, 188 | ) 189 | 190 | projects, err := e.fetchProjects(ctx) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | for _, project := range projects { 196 | projTags := "," 197 | for _, t := range project.Tags { 198 | projTags = projTags + t.Name + "," 199 | } 200 | info.With(prometheus.Labels{ 201 | "uuid": project.UUID.String(), 202 | "name": project.Name, 203 | "version": project.Version, 204 | "classifier": project.Classifier, 205 | "active": strconv.FormatBool(project.Active), 206 | "tags": projTags, 207 | }).Set(1) 208 | 209 | severities := map[string]int{ 210 | "CRITICAL": project.Metrics.Critical, 211 | "HIGH": project.Metrics.High, 212 | "MEDIUM": project.Metrics.Medium, 213 | "LOW": project.Metrics.Low, 214 | "UNASSIGNED": project.Metrics.Unassigned, 215 | } 216 | for severity, v := range severities { 217 | vulnerabilities.With(prometheus.Labels{ 218 | "uuid": project.UUID.String(), 219 | "name": project.Name, 220 | "version": project.Version, 221 | "severity": severity, 222 | }).Set(float64(v)) 223 | } 224 | lastBOMImport.With(prometheus.Labels{ 225 | "uuid": project.UUID.String(), 226 | "name": project.Name, 227 | "version": project.Version, 228 | }).Set(float64(project.LastBOMImport)) 229 | 230 | inheritedRiskScore.With(prometheus.Labels{ 231 | "uuid": project.UUID.String(), 232 | "name": project.Name, 233 | "version": project.Version, 234 | }).Set(project.Metrics.InheritedRiskScore) 235 | 236 | // Initialize all the possible violation series with a 0 value so that it 237 | // properly records increments from 0 -> 1 238 | for _, possibleType := range []string{"LICENSE", "OPERATIONAL", "SECURITY"} { 239 | for _, possibleState := range []string{"INFO", "WARN", "FAIL"} { 240 | for _, possibleAnalysis := range []dtrack.ViolationAnalysisState{ 241 | dtrack.ViolationAnalysisStateApproved, 242 | dtrack.ViolationAnalysisStateRejected, 243 | dtrack.ViolationAnalysisStateNotSet, 244 | // If there isn't any analysis for a policy 245 | // violation then the value in the UI is 246 | // actually empty. So let's represent that in 247 | // these metrics as a possible analysis state. 248 | "", 249 | } { 250 | for _, possibleSuppressed := range []string{"true", "false"} { 251 | policyViolations.With(prometheus.Labels{ 252 | "uuid": project.UUID.String(), 253 | "name": project.Name, 254 | "version": project.Version, 255 | "type": possibleType, 256 | "state": possibleState, 257 | "analysis": string(possibleAnalysis), 258 | "suppressed": possibleSuppressed, 259 | }) 260 | } 261 | } 262 | } 263 | } 264 | } 265 | 266 | violations, err := e.fetchPolicyViolations(ctx) 267 | if err != nil { 268 | return err 269 | } 270 | 271 | for _, violation := range violations { 272 | var ( 273 | analysisState string 274 | suppressed string = "false" 275 | ) 276 | if analysis := violation.Analysis; analysis != nil { 277 | analysisState = string(analysis.State) 278 | suppressed = strconv.FormatBool(analysis.Suppressed) 279 | } 280 | policyViolations.With(prometheus.Labels{ 281 | "uuid": violation.Project.UUID.String(), 282 | "name": violation.Project.Name, 283 | "version": violation.Project.Version, 284 | "type": violation.Type, 285 | "state": violation.PolicyCondition.Policy.ViolationState, 286 | "analysis": analysisState, 287 | "suppressed": suppressed, 288 | }).Inc() 289 | } 290 | 291 | return nil 292 | } 293 | 294 | func (e *Exporter) fetchProjects(ctx context.Context) ([]dtrack.Project, error) { 295 | return dtrack.FetchAll(func(po dtrack.PageOptions) (dtrack.Page[dtrack.Project], error) { 296 | return e.Client.Project.GetAll(ctx, po) 297 | }) 298 | } 299 | 300 | func (e *Exporter) fetchPolicyViolations(ctx context.Context) ([]dtrack.PolicyViolation, error) { 301 | return dtrack.FetchAll(func(po dtrack.PageOptions) (dtrack.Page[dtrack.PolicyViolation], error) { 302 | return e.Client.PolicyViolation.GetAll(ctx, true, po) 303 | }) 304 | } 305 | -------------------------------------------------------------------------------- /internal/exporter/exporter_test.go: -------------------------------------------------------------------------------- 1 | package exporter 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "strconv" 9 | "testing" 10 | 11 | dtrack "github.com/DependencyTrack/client-go" 12 | "github.com/google/go-cmp/cmp" 13 | "github.com/google/uuid" 14 | ) 15 | 16 | func TestFetchProjects_Pagination(t *testing.T) { 17 | mux := http.NewServeMux() 18 | server := httptest.NewServer(mux) 19 | 20 | var wantProjects []dtrack.Project 21 | for i := 0; i < 468; i++ { 22 | wantProjects = append(wantProjects, dtrack.Project{ 23 | UUID: uuid.New(), 24 | }) 25 | } 26 | 27 | mux.HandleFunc("/api/v1/project", func(w http.ResponseWriter, r *http.Request) { 28 | pageSize, err := strconv.Atoi(r.URL.Query().Get("pageSize")) 29 | if err != nil { 30 | t.Fatalf("unexpected error converting pageSize to int: %s", err) 31 | } 32 | pageNumber, err := strconv.Atoi(r.URL.Query().Get("pageNumber")) 33 | if err != nil { 34 | t.Fatalf("unexpected error converting pageNumber to int: %s", err) 35 | } 36 | w.Header().Set("X-Total-Count", strconv.Itoa(len(wantProjects))) 37 | w.Header().Set("Content-type", "application/json") 38 | var projects []dtrack.Project 39 | for i := 0; i < pageSize; i++ { 40 | idx := (pageSize * (pageNumber - 1)) + i 41 | if idx >= len(wantProjects) { 42 | break 43 | } 44 | projects = append(projects, wantProjects[idx]) 45 | } 46 | json.NewEncoder(w).Encode(projects) 47 | }) 48 | 49 | client, err := dtrack.NewClient(server.URL) 50 | if err != nil { 51 | t.Fatalf("unexpected error setting up client: %s", err) 52 | } 53 | 54 | e := &Exporter{ 55 | Client: client, 56 | } 57 | 58 | gotProjects, err := e.fetchProjects(context.Background()) 59 | if err != nil { 60 | t.Fatalf("unexpected error fetching projects: %s", err) 61 | } 62 | 63 | if diff := cmp.Diff(wantProjects, gotProjects); diff != "" { 64 | t.Errorf("unexpected projects:\n%s", diff) 65 | } 66 | } 67 | 68 | func TestFetchPolicyViolations_Pagination(t *testing.T) { 69 | mux := http.NewServeMux() 70 | server := httptest.NewServer(mux) 71 | 72 | var wantPolicyViolations []dtrack.PolicyViolation 73 | for i := 0; i < 468; i++ { 74 | wantPolicyViolations = append(wantPolicyViolations, dtrack.PolicyViolation{ 75 | UUID: uuid.New(), 76 | }) 77 | } 78 | 79 | mux.HandleFunc("/api/v1/violation", func(w http.ResponseWriter, r *http.Request) { 80 | pageSize, err := strconv.Atoi(r.URL.Query().Get("pageSize")) 81 | if err != nil { 82 | t.Fatalf("unexpected error converting pageSize to int: %s", err) 83 | } 84 | pageNumber, err := strconv.Atoi(r.URL.Query().Get("pageNumber")) 85 | if err != nil { 86 | t.Fatalf("unexpected error converting pageNumber to int: %s", err) 87 | } 88 | w.Header().Set("X-Total-Count", strconv.Itoa(len(wantPolicyViolations))) 89 | w.Header().Set("Content-type", "application/json") 90 | var policyViolations []dtrack.PolicyViolation 91 | for i := 0; i < pageSize; i++ { 92 | idx := (pageSize * (pageNumber - 1)) + i 93 | if idx >= len(wantPolicyViolations) { 94 | break 95 | } 96 | policyViolations = append(policyViolations, wantPolicyViolations[idx]) 97 | } 98 | json.NewEncoder(w).Encode(policyViolations) 99 | }) 100 | 101 | client, err := dtrack.NewClient(server.URL) 102 | if err != nil { 103 | t.Fatalf("unexpected error setting up client: %s", err) 104 | } 105 | 106 | e := &Exporter{ 107 | Client: client, 108 | } 109 | 110 | gotPolicyViolations, err := e.fetchPolicyViolations(context.Background()) 111 | if err != nil { 112 | t.Fatalf("unexpected error fetching projects: %s", err) 113 | } 114 | 115 | if diff := cmp.Diff(wantPolicyViolations, gotPolicyViolations); diff != "" { 116 | t.Errorf("unexpected policy violations:\n%s", diff) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | dtrack "github.com/DependencyTrack/client-go" 11 | "github.com/alecthomas/kingpin/v2" 12 | "github.com/go-kit/log/level" 13 | "github.com/jetstack/dependency-track-exporter/internal/exporter" 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/prometheus/common/promlog" 16 | "github.com/prometheus/common/promlog/flag" 17 | "github.com/prometheus/common/version" 18 | "github.com/prometheus/exporter-toolkit/web" 19 | webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag" 20 | ) 21 | 22 | const ( 23 | envAddress string = "DEPENDENCY_TRACK_ADDR" 24 | envAPIKey string = "DEPENDENCY_TRACK_API_KEY" 25 | ) 26 | 27 | func init() { 28 | prometheus.MustRegister(version.NewCollector(exporter.Namespace + "_exporter")) 29 | } 30 | 31 | func main() { 32 | var ( 33 | webConfig = webflag.AddFlags(kingpin.CommandLine, ":9916") 34 | metricsPath = kingpin.Flag("web.metrics-path", "Path under which to expose metrics").Default("/metrics").String() 35 | dtAddress = kingpin.Flag("dtrack.address", fmt.Sprintf("Dependency-Track server address (can also be set with $%s)", envAddress)).Default("http://localhost:8080").Envar(envAddress).String() 36 | dtAPIKey = kingpin.Flag("dtrack.api-key", fmt.Sprintf("Dependency-Track API key (can also be set with $%s)", envAPIKey)).Envar(envAPIKey).Required().String() 37 | promlogConfig = promlog.Config{} 38 | ) 39 | 40 | flag.AddFlags(kingpin.CommandLine, &promlogConfig) 41 | kingpin.Version(version.Print(exporter.Namespace + "_exporter")) 42 | kingpin.HelpFlag.Short('h') 43 | kingpin.Parse() 44 | 45 | logger := promlog.New(&promlogConfig) 46 | 47 | level.Info(logger).Log("msg", fmt.Sprintf("Starting %s_exporter %s", exporter.Namespace, version.Info())) 48 | level.Info(logger).Log("msg", fmt.Sprintf("Build context %s", version.BuildContext())) 49 | 50 | c, err := dtrack.NewClient(*dtAddress, dtrack.WithAPIKey(*dtAPIKey)) 51 | if err != nil { 52 | level.Error(logger).Log("msg", "Error creating client", "err", err) 53 | os.Exit(1) 54 | } 55 | 56 | e := exporter.Exporter{ 57 | Client: c, 58 | Logger: logger, 59 | } 60 | 61 | http.HandleFunc(*metricsPath, e.HandlerFunc()) 62 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 63 | _, _ = w.Write([]byte(` 64 | Dependency-Track Exporter 65 | 66 |

Dependency-Track Exporter

67 |

Metrics

68 | 69 | `)) 70 | }) 71 | 72 | srvc := make(chan struct{}) 73 | term := make(chan os.Signal, 1) 74 | signal.Notify(term, os.Interrupt, syscall.SIGTERM) 75 | 76 | go func() { 77 | srv := &http.Server{} 78 | if err := web.ListenAndServe(srv, webConfig, logger); err != http.ErrServerClosed { 79 | level.Error(logger).Log("msg", "Error starting HTTP server", "err", err) 80 | close(srvc) 81 | } 82 | }() 83 | 84 | for { 85 | select { 86 | case <-term: 87 | level.Info(logger).Log("msg", "Received SIGTERM, exiting gracefully...") 88 | os.Exit(0) 89 | case <-srvc: 90 | os.Exit(1) 91 | } 92 | } 93 | } 94 | --------------------------------------------------------------------------------