├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── homebrew.yml │ ├── lint-pr.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── backend ├── cmd │ ├── dash.go │ ├── root.go │ └── scan.go ├── go.mod ├── go.sum ├── main.go ├── pkg │ └── k8s │ │ ├── cilium-scanner.go │ │ ├── cilium-scanner_test.go │ │ ├── handlers.go │ │ ├── scanner.go │ │ ├── scanner_test.go │ │ ├── target-scanner.go │ │ ├── visualizer.go │ │ └── visualizer_test.go └── statik │ └── statik.go ├── charts ├── Dockerfile └── netfetch │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── clusterwide-networkpolicy.yaml │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── tests │ │ └── test-connection.yaml │ └── values.yaml ├── formula └── netfetch.rb ├── frontend └── dash │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── jsconfig.json │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── logo.png │ ├── src │ ├── App.vue │ ├── Viz.vue │ ├── assets │ │ ├── logo_blue.png │ │ ├── new-clustermap.png │ │ ├── new-dash.png │ │ ├── new-ns.png │ │ ├── new-suggestpolicy.png │ │ ├── small-demo.mov │ │ └── theme.css │ ├── components │ │ └── HelloWorld.vue │ └── main.js │ └── vue.config.js ├── go.work ├── go.work.sum ├── index.yaml ├── netfetch-0.5.3.tgz └── tests ├── cnp.yml ├── cnp2.yml └── cnp3.yml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @deggja -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | rebase-strategy: disabled 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | groups: 9 | actions: 10 | patterns: 11 | - '*' 12 | - package-ecosystem: "gomod" 13 | rebase-strategy: "disabled" 14 | directory: "/backend" 15 | schedule: 16 | interval: "daily" 17 | groups: 18 | go: 19 | patterns: 20 | - '*' 21 | -------------------------------------------------------------------------------- /.github/workflows/homebrew.yml: -------------------------------------------------------------------------------- 1 | name: Update Homebrew Formula 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | inputs: 8 | tag: 9 | description: 'Release tag to update Homebrew formula (e.g., v1.2.3)' 10 | required: true 11 | default: '' 12 | 13 | jobs: 14 | update-homebrew: 15 | runs-on: ubuntu-latest 16 | 17 | permissions: 18 | contents: write 19 | pull-requests: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Extract release version 28 | id: extract_version 29 | run: | 30 | if [ "${{ github.event_name }}" == "release" ]; then 31 | TAG="${{ github.event.release.tag_name }}" 32 | VERSION="${TAG#v}" 33 | echo "Using release tag: $TAG" 34 | elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then 35 | TAG="${{ github.event.inputs.tag }}" 36 | VERSION="${TAG#v}" 37 | echo "Using workflow_dispatch tag: $TAG" 38 | else 39 | echo "Unsupported event: $GITHUB_EVENT_NAME" 40 | exit 1 41 | fi 42 | echo "VERSION=$VERSION" >> $GITHUB_ENV 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | - name: Create temporary directory 47 | run: mkdir temp 48 | 49 | - name: Download macOS tarball 50 | run: | 51 | wget https://github.com/deggja/netfetch/releases/download/v${{ env.VERSION }}/netfetch_${{ env.VERSION }}_darwin_amd64.tar.gz -O temp/netfetch_darwin_amd64.tar.gz 52 | 53 | - name: Calculate macOS SHA256 54 | run: | 55 | SHA=$(sha256sum temp/netfetch_darwin_amd64.tar.gz | awk '{print $1}') 56 | echo "MACOS_SHA256=$SHA" >> $GITHUB_ENV 57 | 58 | - name: Download Linux tarball 59 | run: | 60 | wget https://github.com/deggja/netfetch/releases/download/v${{ env.VERSION }}/netfetch_${{ env.VERSION }}_linux_amd64.tar.gz -O temp/netfetch_linux_amd64.tar.gz 61 | 62 | - name: Calculate Linux SHA256 63 | run: | 64 | SHA=$(sha256sum temp/netfetch_linux_amd64.tar.gz | awk '{print $1}') 65 | echo "LINUX_SHA256=$SHA" >> $GITHUB_ENV 66 | 67 | - name: Update Homebrew Formula 68 | run: | 69 | FORMULA_FILE="formula/netfetch.rb" 70 | sed -i "s|url \".*darwin_amd64\.tar\.gz\"|url \"https://github.com/deggja/netfetch/releases/download/v${{ env.VERSION }}/netfetch_${{ env.VERSION }}_darwin_amd64.tar.gz\"|" $FORMULA_FILE 71 | sed -i -E "/if OS\.mac\?/,/elsif OS\.linux\?/ s|sha256 \".*\"|sha256 \"${{ env.MACOS_SHA256 }}\"|" $FORMULA_FILE 72 | sed -i "s|url \".*linux_amd64\.tar\.gz\"|url \"https://github.com/deggja/netfetch/releases/download/v${{ env.VERSION }}/netfetch_${{ env.VERSION }}_linux_amd64.tar.gz\"|" $FORMULA_FILE 73 | sed -i -E "/elsif OS\.linux\?/,/end/ s|sha256 \".*\"|sha256 \"${{ env.LINUX_SHA256 }}\"|" $FORMULA_FILE 74 | 75 | - name: Fetch Latest Changes from Main 76 | run: | 77 | git fetch origin main 78 | git checkout main 79 | git pull origin main 80 | 81 | - name: Remove temp directory 82 | run: rm -rf temp 83 | 84 | - name: Commit and Push changes 85 | uses: stefanzweifel/git-auto-commit-action@v5 86 | with: 87 | commit_message: "chore: update homebrew formula for v${{ env.VERSION }}" 88 | branch: chore/homebrew-${{ env.VERSION }} 89 | create_branch: true 90 | file_pattern: "formula/netfetch.rb" 91 | commit_user_name: "Netfetch Bot" 92 | commit_author: "Netfetch Bot " 93 | commit_user_email: "bot@netfetch.com" 94 | 95 | - name: Create Pull Request Using GitHub CLI 96 | env: 97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | run: | 99 | gh pr create \ 100 | --title "chore: update homebrew formula for v${{ env.VERSION }}" \ 101 | --body "This is an automated pull request. This pull request updates the Homebrew formula to version v${{ env.VERSION }} with the latest binary URLs and checksums." \ 102 | --base main \ 103 | --head chore/homebrew-${{ env.VERSION }} \ 104 | --label homebrew,automated \ 105 | --reviewer deggja \ 106 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Lint PR" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: lint_pr_title 16 | id: lint_pr_title 17 | uses: amannn/action-semantic-pull-request@v5.5.3 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | - uses: marocchino/sticky-pull-request-comment@v2 21 | if: always() && (steps.lint_pr_title.outputs.error_message != null) 22 | with: 23 | header: pr-title-lint-error 24 | message: | 25 | Hey there and thank you for opening this pull request! 👋🏼 26 | 27 | We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. 28 | 29 | Details: 30 | 31 | ``` 32 | ${{ steps.lint_pr_title.outputs.error_message }} 33 | ``` 34 | # Delete a previous comment when the issue has been resolved 35 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 36 | uses: marocchino/sticky-pull-request-comment@v2 37 | with: 38 | header: pr-title-lint-error 39 | delete: true -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | build-and-release: 10 | name: Build and Release 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: '1.21' 22 | 23 | - name: Run Go Mod Tidy 24 | run: | 25 | cd backend 26 | go mod tidy 27 | 28 | - name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@v6 30 | with: 31 | version: '~> v2' 32 | args: release --clean 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /backend/netfetch -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | # .goreleaser.yml 3 | project_name: netfetch 4 | 5 | # Changelog Configuration 6 | changelog: 7 | sort: desc 8 | filters: 9 | exclude: 10 | - '^Merge pull request' 11 | groups: 12 | - title: Features 13 | regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$' 14 | order: 0 15 | - title: "Bug fixes" 16 | regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$' 17 | order: 1 18 | - title: "Documentation Updates" 19 | regexp: '^.*?docs(\([[:word:]]+\))??!?:.+$' 20 | order: 2 21 | - title: "Other Changes" 22 | regexp: "^(ci|build|misc|perf|deps):" 23 | order: 3 24 | - title: "Miscellaneous" 25 | regexp: ".*" 26 | order: 4 27 | 28 | # Build Configuration 29 | builds: 30 | - id: "netfetch" 31 | main: ./backend/main.go 32 | binary: netfetch 33 | goos: 34 | - linux 35 | - darwin 36 | - windows 37 | goarch: 38 | - amd64 39 | - arm64 40 | ldflags: 41 | - -s -w -X 'github.com/deggja/netfetch/backend/cmd.Version={{.Version}}' 42 | # Additional build flags can be added here 43 | 44 | # Archive Configuration 45 | archives: 46 | - id: "archive" 47 | builds: 48 | - netfetch 49 | format: tar.gz 50 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 51 | wrap_in_directory: true 52 | 53 | # Release Configuration 54 | release: 55 | github: 56 | owner: deggja 57 | name: netfetch 58 | draft: false 59 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Netfetch 2 | 3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## We Develop with Github 12 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 13 | 14 | ## We Use [Github Flow](https://docs.github.com/en/get-started/quickstart/github-flow), So All Code Changes Happen Through Pull Requests 15 | Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: 16 | 17 | 1. Fork the repo and create your branch from `main`. 18 | 2. If you've added code that should be tested, add tests. 19 | 3. If you've changed APIs, update the documentation. 20 | 4. Ensure the test suite passes. 21 | 5. Make sure your code lints. 22 | 6. Issue that pull request! 23 | 24 | ## Any contributions you make will be under the [license](LICENSE.md) we use 25 | In short, when you submit code changes, your submissions are understood to be under the same [LICENSE](LICENSE.md) that covers the project. Feel free to contact the maintainers if that's a concern. 26 | 27 | ## Report bugs using Github's [issues](https://github.com/your-repo/issues) 28 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/deggja/netfetch/issues/new); it's that easy! 29 | 30 | ## Write bug reports with detail, background, and sample code 31 | 32 | **Great Bug Reports** tend to have: 33 | 34 | - A quick summary and/or background 35 | - Steps to reproduce 36 | - Be specific! 37 | - Give sample code if you can. 38 | - What you expected would happen 39 | - What actually happens 40 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 41 | 42 | People *love* thorough bug reports. 43 | 44 | ## Commit Messages and Semantic Versioning 45 | 46 | For versioning, we follow [Semantic Versioning](https://semver.org/). Please include one of the following in your commit messages to indicate the type of release: 47 | - `#major` for major changes 48 | - `#minor` for minor features and enhancements 49 | - `#patch` for bug fixes and patches 50 | 51 | This is required for the automatic versioning script and omitting this will result in a failed merge. 52 | 53 | ## License 54 | By contributing, you agree that your contributions will be licensed under its [LICENSE](LICENSE.md). 55 | 56 | ## References 57 | This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/main/CONTRIBUTING.md) 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Daniel Dagfinrud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | go version 4 | 5 | 6 | d3 version 7 | 8 | 9 | node version 10 | 11 | 12 | vue version 13 | 14 |
15 | 16 | 17 |
18 | 19 |

Netfetch

20 |

Scan your Kubernetes clusters to identifiy unprotected workloads and map your existing Network policies

21 | 22 | Netfetch 23 | 24 |
25 | 26 | ## Contents 27 | - [**What is this project?**](#-what-is-this-project-) 28 | - [Support](#networkpolicy-type-support-in-netfetch) 29 | - **[Installation](#installation)** 30 | - [Install with brew](#installation-via-homebrew-for-mac-) 31 | - [Install in Kubernetes](#installation-via-helm-) 32 | - [**Usage**](#usage) 33 | - [Get started](#get-started) 34 | - [Dashboard](#using-the-dashboard-) 35 | - [Score](#netfetch-score-) 36 | - [Uninstalling](#uninstalling-netfetch) 37 | - [**Contribute**](#contribute-) 38 | 39 | ## ⭐ What is this project ⭐ 40 | 41 | This project aims to demystify network policies in Kubernetes. It's a work in progress! 42 | 43 | The `netfetch` tool will scan your Kubernetes cluster and let you know if you have any pods running without being targeted by network policies. 44 | 45 | | Feature | CLI | Dashboard | 46 | |------------------------------------------------------------------------|------|-----------| 47 | | Scan cluster identify pods without network policies | ✓ | ✓ | 48 | | Save scan output to a text file | ✓ | | 49 | | Visualize network policies and pods in a interactive network map | | ✓ | 50 | | Create default deny network policies where this is missing | ✓ | ✓ | 51 | | Get suggestions for network policies based on existing workloads | | ✓ | 52 | | Calculate a security score based on scan findings | ✓ | ✓ | 53 | | Scan a specific policy by name to see what pods it targets | ✓ | | 54 | 55 | ### NetworkPolicy type support in Netfetch 56 | 57 | | Type | CLI | Dashboard | 58 | |-----------|------|-----------| 59 | | Kubernetes| ✓ | ✓ | 60 | | Cilium | ✓ | | 61 | 62 | Support for additional types of network policies is in the works. No support for the type you need? Check out [issues](https://github.com/deggja/netfetch/issues) for an existing request or create a new one if there is none. 63 | 64 | ## Installation 65 | ### Installation via Homebrew for Mac 💻 66 | 67 | You can install `netfetch` using our Homebrew tap: 68 | 69 | ```sh 70 | brew tap deggja/netfetch https://github.com/deggja/netfetch 71 | brew install netfetch 72 | ``` 73 | 74 | For specific Linux distros, Windows and other install binaries, check the latest release. 75 | 76 | ### Installation via Helm 🎩 77 | 78 | You can deploy the `netfetch` dashboard in your Kubernetes clusters using Helm. 79 | 80 | ```sh 81 | helm repo add deggja https://deggja.github.io/netfetch/ 82 | helm repo update 83 | helm install netfetch deggja/netfetch --namespace netfetch --create-namespace 84 | ``` 85 | 86 | Follow the instructions after deployment to access the dashboard. 87 | 88 | #### Prerequisites 🌌 89 | 90 | - Installed `netfetch` via homebrew or a release binary. 91 | - Access to a Kubernetes cluster with `kubectl` configured. 92 | - Permissions to read and create network policies. 93 | 94 | ## Usage 95 | 96 | ### Get started 97 | 98 | The primary command provided by `netfetch` is `scan`. This command scans all non-system Kubernetes namespaces for network policies. 99 | 100 | You can also scan specific namespaces by specifying the name of that namespace. 101 | 102 | You may add the --dryrun or -d flag to run a dryrun of the scan. The application will not prompt you about adding network policies, but still give you the output of the scan. 103 | 104 | Run `netfetch` in dryrun against a cluster. 105 | 106 | ```sh 107 | netfetch scan --dryrun 108 | ``` 109 | 110 | You can also specify the desired kubeconfig file by using the `--kubeconfig /path/to/config` flag. 111 | 112 | ```sh 113 | netfetch scan --kubeconfig /Users/xxx/.kube/config 114 | ``` 115 | 116 | Run `netfetch` in dryrun against a namespace 117 | 118 | ```sh 119 | netfetch scan crossplane-system --dryrun 120 | ``` 121 | 122 | ![netfetch-demo](https://github.com/deggja/netfetch/assets/15778492/015e9d9f-a678-4a14-a8bd-607f02c13d9f) 123 | 124 | Scan entire cluster. 125 | 126 | ```sh 127 | netfetch scan 128 | ``` 129 | 130 | Scan a namespace called crossplane-system. 131 | 132 | ```sh 133 | netfetch scan crossplane-system 134 | ``` 135 | 136 | Scan entire cluster for Cilium Network Policies and or Cluster Wide Cilium Network Policies. 137 | 138 | ```sh 139 | netfetch scan --cilium 140 | ``` 141 | 142 | Scan a namespace called production for regular Cilium Network Policies. 143 | 144 | ```sh 145 | netfetch scan production --cilium 146 | ``` 147 | 148 | Scan a specific network policy. 149 | 150 | ```sh 151 | netfetch scan --target my-policy-name 152 | ``` 153 | 154 | Scan a specific Cilium Network Policy. 155 | 156 | ```sh 157 | netfetch scan --cilium --target default-cilium-default-deny-all 158 | ``` 159 | 160 | [![asciicast](https://asciinema.org/a/661200.svg)](https://asciinema.org/a/661200) 161 | 162 | ### Using the dashboard 📟 163 | 164 | Launch the dashboard: 165 | 166 | ```sh 167 | netfetch dash 168 | ``` 169 | 170 | You may also specify a port for the dashboard to run on (default is 8080). 171 | 172 | ```sh 173 | netfetch dash --port 8081 174 | ``` 175 | 176 | ### Dashboard functionality overview 177 | 178 | The Netfetch Dashboard offers an intuitive interface for interacting with your Kubernetes cluster's network policies. Below is a detailed overview of the functionalities available through the dashboard: 179 | 180 | | Action | Description | Screenshot Link | 181 | |----------------------|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------| 182 | | Scan Cluster | Initiates a cluster-wide scan to identify pods without network policies, similar to `netfetch scan`. | ![Netfetch Dashboard](https://github.com/deggja/netfetch/blob/main/frontend/dash/src/assets/new-dash.png) | 183 | | Scan Namespace | Scans a selected namespace for pods not covered by network policies, equivalent to `netfetch scan namespace`. | ![Cluster map](https://github.com/deggja/netfetch/blob/main/frontend/dash/src/assets/new-clustermap.png) | 184 | | Create Cluster Map | Generates a D3-rendered network map of all pods and policies across accessible namespaces. | ![Network map](https://github.com/deggja/netfetch/blob/main/frontend/dash/src/assets/new-ns.png) | 185 | | Suggest Policy | Provides network policy suggestions based on existing workloads within a selected namespace. | ![Suggested policies](https://github.com/deggja/netfetch/blob/main/frontend/dash/src/assets/new-suggestpolicy.png) | 186 | 187 | ### Interactive Features 188 | 189 | - **Table View**: Shows pods not targeted by network policies. It updates based on the cluster or namespace scans. 190 | - **Network Map Visualization**: Rendered using D3 to show how pods and policies interact within the cluster. 191 | - **Policy Preview**: Double-click network policy nodes within the network map to view policy YAML. 192 | - **Policy Editing**: Edit suggested policies directly within the dashboard or copy the YAML for external use. 193 | 194 | 195 | ### Netfetch score 🥇 196 | 197 | The `netfetch` tool provides a basic score at the end of each scan. The score ranges from 1 to 100, with 1 being the lowest and 100 being the highest possible score. 198 | 199 | Your score will decrease based on the amount of workloads in your cluster that are running without being targeted by a network policy. 200 | 201 | The score reflects the security posture of your Kubernetes namespaces based on network policies and general policy coverage. If changes are made based on recommendations from the initial scan, rerunning `netfetch` will likely result in a higher score. 202 | 203 | ### Uninstalling netfetch 204 | 205 | If you want to uninstall the application - you can do so by running the following commands. 206 | 207 | ``` 208 | brew uninstall netfetch 209 | brew cleanup -s netfetch 210 | brew untap deggja/netfetch https://github.com/deggja/netfetch 211 | ``` 212 | 213 | ## Running Tests 214 | 215 | To run tests for netfetch, follow these steps: 216 | 217 | 1. Navigate to the root directory of the project in your terminal. 218 | 219 | 2. Navigate to the backend directory within the project: 220 | 221 | ``` 222 | cd backend 223 | ``` 224 | 225 | 3. Run the following command to execute all tests in the project: 226 | 227 | ``` 228 | go test ./... 229 | ``` 230 | 231 | This command will recursively search for tests in all subdirectories (./...) and run them. 232 | 233 | 4. After executing the command, you will see the test results in the terminal output. 234 | 235 | ## Contribute 🔨 236 | Thank you to the following awesome people: 237 | 238 | - [roopeshsn](https://github.com/roopeshsn) 239 | - [s-rd](https://github.com/s-rd) 240 | - [JJGadgets](https://github.com/JJGadgets) 241 | - [Home Operations Discord](https://github.com/onedr0p/home-ops) 242 | - [pehlicd](https://github.com/pehlicd) 243 | 244 | 245 | You are welcome to contribute! 246 | 247 | See [CONTRIBUTING](CONTRIBUTING.md) for instructions on how to proceed. 248 | 249 | ## Tools 🧰 250 | Netfetch uses other tools for a plethora of different things. It would not be possible without the following: 251 | 252 | - [statik](https://github.com/rakyll/statik) 253 | - [D3](https://d3-graph-gallery.com/network.html) 254 | - [Helm](https://helm.sh/docs/) 255 | - [Brew](https://brew.sh/) 256 | - [lipgloss](https://github.com/charmbracelet/lipgloss) 257 | 258 | ## License 259 | 260 | Netfetch is distributed under the MIT License. See the [LICENSE](LICENSE) for more information. 261 | -------------------------------------------------------------------------------- /backend/cmd/dash.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | 11 | "github.com/charmbracelet/lipgloss" 12 | _ "github.com/deggja/netfetch/backend/statik" 13 | "github.com/rakyll/statik/fs" 14 | 15 | "github.com/deggja/netfetch/backend/pkg/k8s" 16 | "github.com/rs/cors" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | var dashCmd = &cobra.Command{ 21 | Use: "dash", 22 | Short: "Launch the Netfetch interactive dashboard", 23 | Run: func(cmd *cobra.Command, args []string) { 24 | port, _ := cmd.Flags().GetString("port") 25 | startDashboardServer(port, kubeconfigPath) 26 | }, 27 | } 28 | 29 | func setNoCacheHeaders(w http.ResponseWriter) { 30 | w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") 31 | w.Header().Set("Pragma", "no-cache") 32 | w.Header().Set("Expires", "0") 33 | } 34 | 35 | func startDashboardServer(port string, kubeconfigPath string) { 36 | // Verify connection to cluster or throw error 37 | clientset, err := k8s.GetClientset(kubeconfigPath) 38 | if err != nil { 39 | log.Fatalf("You are not connected to a Kubernetes cluster. Please connect to a cluster and re-run the command: %v", err) 40 | return 41 | } 42 | 43 | _, err = clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{}) 44 | if err != nil { 45 | log.Fatalf("You are not connected to a Kubernetes cluster. Please connect to a cluster and re-run the command: %v", err) 46 | return 47 | } 48 | 49 | c := cors.New(cors.Options{ 50 | AllowOriginRequestFunc: func(r *http.Request, origin string) bool { 51 | // Implement your dynamic origin check here 52 | host := r.Host // Extract the host from the request 53 | allowedOrigins := []string{"http://localhost:" + port, "https://" + host} 54 | for _, allowedOrigin := range allowedOrigins { 55 | if origin == allowedOrigin { 56 | return true 57 | } 58 | } 59 | return false 60 | }, 61 | AllowedMethods: []string{"GET", "POST", "OPTIONS"}, 62 | AllowedHeaders: []string{"Accept", "Content-Type", "X-CSRF-Token"}, 63 | AllowCredentials: true, 64 | }) 65 | 66 | // Set up handlers 67 | http.HandleFunc("/", dashboardHandler) 68 | http.HandleFunc("/scan", k8s.HandleScanRequest(kubeconfigPath)) 69 | http.HandleFunc("/namespaces", k8s.HandleNamespaceListRequest(kubeconfigPath)) 70 | http.HandleFunc("/add-policy", k8s.HandleAddPolicyRequest(kubeconfigPath)) 71 | http.HandleFunc("/create-policy", k8s.HandleCreatePolicyRequest(kubeconfigPath)) 72 | http.HandleFunc("/namespaces-with-policies", k8s.HandleNamespacesWithPoliciesRequest(kubeconfigPath)) 73 | http.HandleFunc("/namespace-policies", k8s.HandleNamespacePoliciesRequest(kubeconfigPath)) 74 | http.HandleFunc("/visualization", k8s.HandleVisualizationRequest(kubeconfigPath)) 75 | http.HandleFunc("/visualization/cluster", k8s.HandleClusterVisualizationRequest(kubeconfigPath)) 76 | http.HandleFunc("/policy-yaml", k8s.HandlePolicyYAMLRequest(kubeconfigPath)) 77 | http.HandleFunc("/pod-info", k8s.HandlePodInfoRequest(kubeconfigPath)) 78 | 79 | // Wrap the default serve mux with the CORS middleware 80 | handler := c.Handler(http.DefaultServeMux) 81 | 82 | // Start the server 83 | serverURL := fmt.Sprintf("http://localhost:%s", port) 84 | startupMessage := HeaderStyle.Render(fmt.Sprintf("Starting dashboard server on %s", serverURL)) 85 | fmt.Println(startupMessage) 86 | if err := http.ListenAndServe(":"+port, handler); err != nil { 87 | log.Fatalf("Failed to start server: %v\n", err) 88 | } 89 | } 90 | 91 | // func dashboardHandler(w http.ResponseWriter, r *http.Request) { 92 | // // Check if we are in development mode 93 | // isDevelopment := true 94 | // if isDevelopment { 95 | // // Redirect to the Vue dev server 96 | // vueDevServer := "http://localhost:8081" 97 | // http.Redirect(w, r, vueDevServer+r.RequestURI, http.StatusTemporaryRedirect) 98 | // } else { 99 | // // Serve the embedded frontend using statik 100 | // statikFS, err := fs.New() 101 | // if err != nil { 102 | // log.Fatal(err) 103 | // } 104 | // http.FileServer(statikFS).ServeHTTP(w, r) 105 | // } 106 | // } 107 | 108 | func dashboardHandler(w http.ResponseWriter, r *http.Request) { 109 | // Set cache control headers 110 | setNoCacheHeaders(w) 111 | 112 | statikFS, err := fs.New() 113 | if err != nil { 114 | log.Fatal(err) 115 | } 116 | 117 | // Serve the embedded frontend 118 | http.FileServer(statikFS).ServeHTTP(w, r) 119 | } 120 | 121 | var HeaderStyle = lipgloss.NewStyle(). 122 | Bold(true). 123 | Foreground(lipgloss.Color("6")). 124 | Align(lipgloss.Center). 125 | PaddingTop(1). 126 | PaddingBottom(1). 127 | PaddingLeft(4). 128 | PaddingRight(4). 129 | BorderStyle(lipgloss.NormalBorder()). 130 | BorderForeground(lipgloss.Color("99")) 131 | 132 | func init() { 133 | dashCmd.Flags().StringVar(&kubeconfigPath, "kubeconfig", "", "Path to the kubeconfig file (optional)") 134 | dashCmd.Flags().StringP("port", "p", "8080", "Port for the interactive dashboard") 135 | rootCmd.AddCommand(dashCmd) 136 | } -------------------------------------------------------------------------------- /backend/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var Version string 11 | 12 | var rootCmd = &cobra.Command{ 13 | Use: "netfetch", 14 | Short: "Netfetch is a CLI tool for scanning Kubernetes clusters for network policies", 15 | Long: `Netfetch is a CLI tool for scanning clusters for network policies and identifying unprotected workloads.`, 16 | } 17 | 18 | var versionCmd = &cobra.Command{ 19 | Use: "version", 20 | Short: "Print the version number of Netfetch", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | fmt.Printf(Version + "\n") 23 | }, 24 | } 25 | 26 | func Execute() { 27 | if err := rootCmd.Execute(); err != nil { 28 | fmt.Fprintln(os.Stderr, err) 29 | os.Exit(1) 30 | } 31 | } 32 | 33 | func init() { 34 | rootCmd.AddCommand(versionCmd) 35 | } 36 | -------------------------------------------------------------------------------- /backend/cmd/scan.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/lipgloss" 7 | "github.com/charmbracelet/lipgloss/table" 8 | "github.com/deggja/netfetch/backend/pkg/k8s" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | dryRun bool 14 | native bool 15 | cilium bool 16 | verbose bool 17 | targetPolicy string 18 | kubeconfigPath string 19 | ) 20 | 21 | var scanCmd = &cobra.Command{ 22 | Use: "scan [namespace]", 23 | Short: "Scan Kubernetes namespaces for network policies", 24 | Long: `Scan Kubernetes namespaces for network policies. 25 | By default, it scans for native Kubernetes network policies. 26 | Use --cilium to scan for Cilium network policies. 27 | You may also target a specific network policy using the --target flag. 28 | This can be used in combination with --native and --cilium for select policy types.`, 29 | Args: cobra.MaximumNArgs(1), 30 | Run: func(cmd *cobra.Command, args []string) { 31 | var namespace string 32 | if len(args) > 0 { 33 | namespace = args[0] 34 | } 35 | 36 | // Initialize the Kubernetes clients 37 | clientset, err := k8s.GetClientset(kubeconfigPath) 38 | if err != nil { 39 | fmt.Println("Error creating Kubernetes client:", err) 40 | return 41 | } 42 | dynamicClient, err := k8s.GetCiliumDynamicClient(kubeconfigPath) 43 | if err != nil { 44 | fmt.Println("Error creating Kubernetes dynamic client:", err) 45 | return 46 | } 47 | 48 | // Handle target policy for native Kubernetes network policies 49 | if targetPolicy != "" { 50 | if !cilium || native { 51 | fmt.Println("Policy type: Kubernetes") 52 | fmt.Printf("Searching for Kubernetes native network policy '%s' across all non-system namespaces...\n", targetPolicy) 53 | policy, foundNamespace, err := k8s.FindNativeNetworkPolicyByName(dynamicClient, clientset, targetPolicy) 54 | if err != nil { 55 | fmt.Println("Error during Kubernetes native network policy search:", err) 56 | } else { 57 | fmt.Printf("Found Kubernetes native network policy '%s' in namespace '%s'.\n", policy.GetName(), foundNamespace) 58 | 59 | // List the pods targeted by this policy 60 | pods, err := k8s.ListPodsTargetedByNetworkPolicy(dynamicClient, policy, foundNamespace) 61 | if err != nil { 62 | fmt.Printf("Error listing pods targeted by policy %s: %v\n", policy.GetName(), err) 63 | } else if len(pods) == 0 { 64 | fmt.Printf("No pods targeted by policy '%s' in namespace '%s'.\n", policy.GetName(), foundNamespace) 65 | } else { 66 | fmt.Printf("Pods targeted by policy '%s' in namespace '%s':\n", policy.GetName(), foundNamespace) 67 | fmt.Println(createTargetPodsTable(pods)) 68 | } 69 | } 70 | return 71 | } 72 | } 73 | 74 | // Handle target policy for Cilium network policies and cluster wide policies 75 | if targetPolicy != "" && cilium { 76 | fmt.Println("Policy type: Cilium") 77 | fmt.Printf("Searching for Cilium network policy '%s' across all non-system namespaces...\n", targetPolicy) 78 | policy, foundNamespace, err := k8s.FindCiliumNetworkPolicyByName(dynamicClient, targetPolicy) 79 | if err != nil { 80 | // If not found in namespaces, search for cluster wide policy 81 | fmt.Println("Cilium network policy not found in namespaces, searching for cluster-wide policy...") 82 | policy, err = k8s.FindCiliumClusterWideNetworkPolicyByName(dynamicClient, targetPolicy) 83 | if err != nil { 84 | fmt.Println("Error during Cilium cluster wide network policy search:", err) 85 | } else { 86 | fmt.Printf("Found Cilium clusterwide network policy '%s'.\n", policy.GetName()) 87 | 88 | // List the pods targeted by this cluster wide policy 89 | pods, err := k8s.ListPodsTargetedByCiliumClusterWideNetworkPolicy(clientset, dynamicClient, policy) 90 | if err != nil { 91 | fmt.Printf("Error listing pods targeted by cluster wide policy %s: %v\n", policy.GetName(), err) 92 | } else if len(pods) == 0 { 93 | fmt.Printf("No pods targeted by cluster wide policy '%s'.\n", policy.GetName()) 94 | } else { 95 | fmt.Printf("Pods targeted by cluster wide policy '%s':\n", policy.GetName()) 96 | fmt.Println(createTargetPodsTable(pods)) 97 | } 98 | } 99 | } else { 100 | fmt.Printf("Found Cilium network policy '%s' in namespace '%s'.\n", policy.GetName(), foundNamespace) 101 | 102 | // List the pods targeted by this policy 103 | pods, err := k8s.ListPodsTargetedByCiliumNetworkPolicy(dynamicClient, policy, foundNamespace) 104 | if err != nil { 105 | fmt.Printf("Error listing pods targeted by policy %s: %v\n", policy.GetName(), err) 106 | } else if len(pods) == 0 { 107 | fmt.Printf("No pods targeted by policy '%s' in namespace '%s'.\n", policy.GetName(), foundNamespace) 108 | } else { 109 | fmt.Printf("Pods targeted by policy '%s' in namespace '%s':\n", policy.GetName(), foundNamespace) 110 | fmt.Println(createTargetPodsTable(pods)) 111 | } 112 | } 113 | return 114 | } 115 | 116 | // Default to native scan if no specific type is mentioned or if --native is used 117 | if !cilium || native { 118 | fmt.Println("Running native network policies scan...") 119 | nativeScanResult, err := k8s.ScanNetworkPolicies(namespace, dryRun, false, true, true, true, kubeconfigPath) 120 | if err != nil { 121 | fmt.Println("Error during Kubernetes native network policies scan:", err) 122 | } else { 123 | fmt.Println("Kubernetes native network policies scan completed successfully.") 124 | handleScanResult(nativeScanResult) 125 | } 126 | } 127 | 128 | // Perform Cilium network policy scan if --cilium is used 129 | if cilium { 130 | // Perform cluster wide Cilium scan first if no namespace is specified 131 | if namespace == "" { 132 | fmt.Println("Running cluster wide Cilium network policies scan...") 133 | dynamicClient, err := k8s.GetCiliumDynamicClient(kubeconfigPath) 134 | if err != nil { 135 | fmt.Println("Error obtaining dynamic client:", err) 136 | return 137 | } 138 | 139 | clusterwideScanResult, err := k8s.ScanCiliumClusterwideNetworkPolicies(dynamicClient, false, dryRun, true, kubeconfigPath) 140 | if err != nil { 141 | fmt.Println("Error during cluster wide Cilium network policies scan:", err) 142 | } else { 143 | // Handle the cluster wide scan result; skip further scanning if all pods are protected 144 | if clusterwideScanResult.AllPodsProtected { 145 | fmt.Println("All pods are protected by cluster wide cilium policies.\nYour Netfetch security score is: 100/100") 146 | return 147 | } 148 | handleScanResult(clusterwideScanResult) 149 | } 150 | } 151 | 152 | // Proceed with normal Cilium network policy scan 153 | fmt.Println("Running cilium network policies scan...") 154 | ciliumScanResult, err := k8s.ScanCiliumNetworkPolicies(namespace, dryRun, false, true, true, true, kubeconfigPath) 155 | if err != nil { 156 | fmt.Println("Error during Cilium network policies scan:", err) 157 | } else { 158 | fmt.Println("Cilium network policies scan completed successfully.") 159 | handleScanResult(ciliumScanResult) 160 | } 161 | } 162 | }, 163 | } 164 | 165 | func handleScanResult(scanResult *k8s.ScanResult) { 166 | // Implement your logic to handle scan results 167 | } 168 | 169 | var ( 170 | headerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6")).Align(lipgloss.Center) 171 | evenRowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")) 172 | oddRowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")) 173 | tableBorderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("99")) 174 | ) 175 | 176 | // Function to create a table for pods 177 | func createTargetPodsTable(pods [][]string) string { 178 | t := table.New(). 179 | Border(lipgloss.NormalBorder()). 180 | BorderStyle(tableBorderStyle). 181 | StyleFunc(func(row, col int) lipgloss.Style { 182 | if row == 0 { 183 | return headerStyle 184 | } 185 | if row%2 == 0 { 186 | return evenRowStyle 187 | } 188 | return oddRowStyle 189 | }). 190 | Headers("Namespace", "Pod Name", "IP Address") 191 | 192 | for _, podDetails := range pods { 193 | t.Row(podDetails...) 194 | } 195 | 196 | return t.String() 197 | } 198 | 199 | func init() { 200 | scanCmd.Flags().StringVar(&kubeconfigPath, "kubeconfig", "", "Path to the kubeconfig file (optional)") 201 | scanCmd.Flags().BoolVarP(&dryRun, "dryrun", "d", false, "Perform a dry run without applying any changes") 202 | scanCmd.Flags().BoolVar(&native, "native", false, "Scan only native network policies") 203 | scanCmd.Flags().BoolVar(&cilium, "cilium", false, "Scan only Cilium network policies (includes cluster wide policies if no namespace is specified)") 204 | scanCmd.Flags().StringVarP(&targetPolicy, "target", "t", "", "Scan a specific network policy by name") 205 | scanCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") 206 | rootCmd.AddCommand(scanCmd) 207 | } 208 | -------------------------------------------------------------------------------- /backend/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/deggja/netfetch/backend 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/AlecAivazis/survey/v2 v2.3.7 9 | github.com/spf13/cobra v1.9.1 10 | k8s.io/api v0.33.1 11 | k8s.io/apimachinery v0.33.1 12 | k8s.io/client-go v0.33.1 13 | ) 14 | 15 | require ( 16 | github.com/charmbracelet/lipgloss v1.1.0 17 | github.com/rakyll/statik v0.1.7 18 | github.com/stretchr/testify v1.10.0 19 | ) 20 | 21 | require ( 22 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 23 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 24 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 25 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect 26 | github.com/charmbracelet/x/term v0.2.1 // indirect 27 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 28 | github.com/google/go-cmp v0.7.0 // indirect 29 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 30 | github.com/mattn/go-runewidth v0.0.16 // indirect 31 | github.com/muesli/termenv v0.16.0 // indirect 32 | github.com/pkg/errors v0.9.1 // indirect 33 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 34 | github.com/rivo/uniseg v0.4.7 // indirect 35 | github.com/x448/float16 v0.8.4 // indirect 36 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 37 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 38 | sigs.k8s.io/randfill v1.0.0 // indirect 39 | ) 40 | 41 | require ( 42 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 43 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 44 | github.com/go-logr/logr v1.4.2 // indirect 45 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 46 | github.com/go-openapi/jsonreference v0.20.2 // indirect 47 | github.com/go-openapi/swag v0.23.0 // indirect 48 | github.com/gogo/protobuf v1.3.2 // indirect 49 | github.com/google/gnostic-models v0.6.9 // indirect 50 | github.com/google/uuid v1.6.0 // indirect 51 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 52 | github.com/josharian/intern v1.0.0 // indirect 53 | github.com/json-iterator/go v1.1.12 // indirect 54 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 55 | github.com/mailru/easyjson v0.7.7 // indirect 56 | github.com/mattn/go-colorable v0.1.2 // indirect 57 | github.com/mattn/go-isatty v0.0.20 // indirect 58 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 59 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 60 | github.com/modern-go/reflect2 v1.0.2 // indirect 61 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 62 | github.com/rs/cors v1.11.1 63 | github.com/spf13/pflag v1.0.6 // indirect 64 | golang.org/x/net v0.38.0 // indirect 65 | golang.org/x/oauth2 v0.27.0 // indirect 66 | golang.org/x/sys v0.31.0 // indirect 67 | golang.org/x/term v0.30.0 // indirect 68 | golang.org/x/text v0.23.0 // indirect 69 | golang.org/x/time v0.9.0 // indirect 70 | google.golang.org/protobuf v1.36.5 // indirect 71 | gopkg.in/inf.v0 v0.9.1 // indirect 72 | gopkg.in/yaml.v2 v2.4.0 73 | gopkg.in/yaml.v3 v3.0.1 // indirect 74 | k8s.io/klog/v2 v2.130.1 // indirect 75 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 76 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 77 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 78 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 79 | sigs.k8s.io/yaml v1.4.0 // indirect 80 | ) 81 | -------------------------------------------------------------------------------- /backend/go.sum: -------------------------------------------------------------------------------- 1 | github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= 2 | github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= 3 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= 4 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= 5 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 6 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 7 | github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 8 | github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 9 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= 10 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= 11 | github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 12 | github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 13 | github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 14 | github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 15 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= 16 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 17 | github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= 18 | github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 19 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 20 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 21 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 22 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 23 | github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= 24 | github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 28 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 30 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 31 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 32 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 33 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 34 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 35 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 36 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 37 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 38 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 39 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 40 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 41 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 42 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 43 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 44 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 45 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 46 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 47 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 48 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 49 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 50 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 51 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 52 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 53 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 54 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 55 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 56 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 57 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= 58 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= 59 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 60 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 61 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 62 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 63 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 64 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 65 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 66 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 67 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 68 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 69 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 70 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 71 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 72 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 73 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 74 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 75 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 76 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 77 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 78 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 79 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 80 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 81 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 82 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 83 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 84 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 85 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 86 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 87 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 88 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 89 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 90 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 91 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 92 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 93 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 94 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 95 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 96 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 97 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 98 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 99 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 100 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 101 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 102 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 103 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 104 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 105 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 106 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 107 | github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= 108 | github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= 109 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 110 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 111 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 112 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 113 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 114 | github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= 115 | github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 116 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 117 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 118 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 119 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 120 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 121 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 122 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 123 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 124 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 125 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 126 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 127 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 128 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 129 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 130 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 131 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 132 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 133 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 134 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 135 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 136 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 137 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 138 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 139 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 140 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 141 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 142 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 143 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 144 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 145 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 146 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 147 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 148 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 149 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 150 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 151 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 152 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 153 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 154 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 155 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 156 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 157 | golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= 158 | golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 159 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 160 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 161 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 162 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 163 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 164 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 165 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 166 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 169 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 170 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 171 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 172 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 173 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 174 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 175 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 176 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 177 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 178 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 179 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 180 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 181 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 182 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 183 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 184 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 185 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 186 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 187 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 188 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 189 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 190 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 191 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 192 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 193 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 194 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 195 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 196 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 197 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 198 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 199 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 200 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 201 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 202 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 203 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 204 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 205 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 206 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 207 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 208 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 209 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 210 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 211 | k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= 212 | k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= 213 | k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= 214 | k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 215 | k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= 216 | k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= 217 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 218 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 219 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 220 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 221 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 222 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 223 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 224 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 225 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 226 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 227 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 228 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= 229 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 230 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 231 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 232 | -------------------------------------------------------------------------------- /backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/deggja/netfetch/backend/cmd" 5 | ) 6 | 7 | var ( 8 | version = "0.0.36" 9 | ) 10 | 11 | func main() { 12 | cmd.Execute() 13 | } 14 | -------------------------------------------------------------------------------- /backend/pkg/k8s/cilium-scanner_test.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | ) 9 | 10 | func TestMatchesLabels(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | podLabels map[string]string 14 | policyLabels map[string]interface{} 15 | expectedMatch bool 16 | }{ 17 | { 18 | name: "Matching labels", 19 | podLabels: map[string]string{ 20 | "app": "test", 21 | }, 22 | // Equivalent to MatchLabels in a NetworkPolicy object 23 | policyLabels: map[string]interface{}{ 24 | "app": "test", 25 | }, 26 | expectedMatch: true, 27 | }, 28 | { 29 | name: "Matching labels", 30 | podLabels: map[string]string{ 31 | "app": "test", 32 | }, 33 | // Equivalent to MatchLabels in a NetworkPolicy object 34 | policyLabels: map[string]interface{}{ 35 | "app": "test-policy", 36 | }, 37 | expectedMatch: false, 38 | }, 39 | } 40 | 41 | for _, test := range tests { 42 | t.Run(test.name, func(t *testing.T) { 43 | match := MatchesLabels(test.podLabels, test.policyLabels) 44 | // if match != test.expectedMatch { 45 | // t.Errorf("Expected match: %v, got: %v", test.expectedMatch, match) 46 | // } 47 | assert.Equal(t, test.expectedMatch, match, "they should be equal") 48 | }) 49 | } 50 | } 51 | 52 | func TestConvertEndpointToSelector(t *testing.T) { 53 | tests := []struct { 54 | name string 55 | endpointSelector map[string]interface{} 56 | expectedSelector string 57 | expectedError error 58 | }{ 59 | { 60 | name: "Non-Empty Selector", 61 | endpointSelector: map[string]interface{}{ 62 | "matchLabels": map[string]interface{}{"app": "test"}, 63 | }, 64 | expectedSelector: "app=test", 65 | expectedError: nil, 66 | }, 67 | { 68 | name: "Empty Selector", 69 | endpointSelector: map[string]interface{}{ 70 | "matchLabels": map[string]string{}, 71 | }, 72 | expectedSelector: "", 73 | expectedError: nil, 74 | }, 75 | } 76 | 77 | for _, test := range tests { 78 | t.Run(test.name, func(t *testing.T) { 79 | selector, err := ConvertEndpointToSelector(test.endpointSelector) 80 | if err != nil { 81 | t.Fatalf("unexpected error: %v", err) 82 | } 83 | assert.Equal(t, test.expectedSelector, selector) 84 | }) 85 | } 86 | } 87 | 88 | func TestIsDefaultDenyAllCiliumClusterwidePolicy(t *testing.T) { 89 | tests := []struct { 90 | name string 91 | policyUnstructured unstructured.Unstructured 92 | expectedDenyAll bool 93 | expectedCluster bool 94 | }{ 95 | { 96 | name: "Default Deny All Policy", 97 | policyUnstructured: unstructured.Unstructured{ 98 | Object: map[string]interface{}{ 99 | "spec": map[string]interface{}{ 100 | "ingress": []interface{}{}, 101 | "egress": []interface{}{}, 102 | "endpointSelector": map[string]interface{}{ 103 | "matchLabels": map[string]interface{}{}, 104 | }, 105 | }, 106 | }, 107 | }, 108 | expectedDenyAll: true, 109 | expectedCluster: true, 110 | }, 111 | { 112 | name: "Non-Default Policy", 113 | policyUnstructured: unstructured.Unstructured{ 114 | Object: map[string]interface{}{ 115 | "spec": map[string]interface{}{ 116 | "ingress": []interface{}{ 117 | map[string]interface{}{ 118 | "port": 80, 119 | }}, 120 | "egress": []interface{}{}, 121 | "endpointSelector": map[string]interface{}{ 122 | "matchLabels": map[string]interface{}{"app": "test"}, 123 | }, 124 | }, 125 | }, 126 | }, 127 | expectedDenyAll: false, 128 | expectedCluster: false, 129 | }, 130 | } 131 | 132 | for _, test := range tests { 133 | t.Run(test.name, func(t *testing.T) { 134 | denyAll, cluster := IsDefaultDenyAllCiliumClusterwidePolicy(test.policyUnstructured) 135 | 136 | if denyAll != test.expectedDenyAll { 137 | t.Errorf("Expected Default Deny All: %v, got: %v", test.expectedDenyAll, denyAll) 138 | } 139 | 140 | if cluster != test.expectedCluster { 141 | t.Errorf("Expected Clusterwide: %v, got: %v", test.expectedCluster, cluster) 142 | } 143 | }) 144 | } 145 | } 146 | 147 | func TestIsEmptyOrOnlyContainsEmptyObjects(t *testing.T) { 148 | tests := []struct { 149 | name string 150 | slice []interface{} 151 | expectedEmpty bool 152 | }{ 153 | { 154 | name: "Empty Slice", 155 | slice: []interface{}{}, 156 | expectedEmpty: true, 157 | }, 158 | { 159 | name: "Non-Empty Slice", 160 | slice: []interface{}{map[string]interface{}{"key": "value"}}, 161 | expectedEmpty: false, 162 | }, 163 | } 164 | 165 | for _, test := range tests { 166 | t.Run(test.name, func(t *testing.T) { 167 | isEmpty := IsEmptyOrOnlyContainsEmptyObjects(test.slice) 168 | 169 | if isEmpty != test.expectedEmpty { 170 | t.Errorf("Expected empty: %v, got: %v", test.expectedEmpty, isEmpty) 171 | } 172 | }) 173 | } 174 | } 175 | 176 | func TestIsEndpointSelectorEmpty(t *testing.T) { 177 | tests := []struct { 178 | name string 179 | selector map[string]interface{} 180 | expectedIsEmpty bool 181 | }{ 182 | { 183 | name: "Empty Selector", 184 | selector: map[string]interface{}{}, 185 | expectedIsEmpty: true, 186 | }, 187 | { 188 | name: "Non-Empty Selector", 189 | selector: map[string]interface{}{ 190 | "matchLabels": map[string]interface{}{"app": "test"}, 191 | }, 192 | expectedIsEmpty: false, 193 | }, 194 | } 195 | 196 | for _, test := range tests { 197 | t.Run(test.name, func(t *testing.T) { 198 | isEmpty := isEndpointSelectorEmpty(test.selector) 199 | 200 | if isEmpty != test.expectedIsEmpty { 201 | t.Errorf("Expected is empty: %v, got: %v", test.expectedIsEmpty, isEmpty) 202 | } 203 | }) 204 | } 205 | } 206 | 207 | func TestIsDefaultDenyAllCiliumPolicy(t *testing.T) { 208 | tests := []struct { 209 | name string 210 | policyUnstructured unstructured.Unstructured 211 | expectedDenyAll bool 212 | }{ 213 | { 214 | name: "Default Deny All Policy", 215 | policyUnstructured: unstructured.Unstructured{ 216 | Object: map[string]interface{}{ 217 | "spec": map[string]interface{}{ 218 | "ingress": []interface{}{}, 219 | "egress": []interface{}{}, 220 | }, 221 | }, 222 | }, 223 | expectedDenyAll: true, 224 | }, 225 | { 226 | name: "Non-Default Policy", 227 | policyUnstructured: unstructured.Unstructured{ 228 | Object: map[string]interface{}{ 229 | "spec": map[string]interface{}{ 230 | "ingress": []interface{}{map[string]interface{}{"port": 80}}, 231 | "egress": []interface{}{}, 232 | }, 233 | }, 234 | }, 235 | expectedDenyAll: false, 236 | }, 237 | } 238 | 239 | for _, test := range tests { 240 | t.Run(test.name, func(t *testing.T) { 241 | denyAll := IsDefaultDenyAllCiliumPolicy(test.policyUnstructured) 242 | 243 | if denyAll != test.expectedDenyAll { 244 | t.Errorf("Expected Default Deny All: %v, got: %v", test.expectedDenyAll, denyAll) 245 | } 246 | }) 247 | } 248 | } -------------------------------------------------------------------------------- /backend/pkg/k8s/handlers.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | // INTERACTIVE DASHBOARD LOGIC 14 | 15 | func setNoCacheHeaders(w http.ResponseWriter) { 16 | w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") 17 | w.Header().Set("Pragma", "no-cache") 18 | w.Header().Set("Expires", "0") 19 | } 20 | 21 | // HandleScanRequest handles the HTTP request for scanning network policies 22 | func HandleScanRequest(kubeconfigPath string) http.HandlerFunc { 23 | return func(w http.ResponseWriter, r *http.Request) { 24 | namespace := r.URL.Query().Get("namespace") 25 | 26 | // Perform the scan 27 | result, err := ScanNetworkPolicies(namespace, false, true, false, false, false, kubeconfigPath) 28 | if err != nil { 29 | http.Error(w, err.Error(), http.StatusInternalServerError) 30 | return 31 | } 32 | 33 | // Respond with JSON 34 | w.Header().Set("Content-Type", "application/json") 35 | json.NewEncoder(w).Encode(result) 36 | } 37 | } 38 | 39 | // HandleNamespaceListRequest lists all non-system Kubernetes namespaces 40 | func HandleNamespaceListRequest(kubeconfigPath string) http.HandlerFunc { 41 | return func(w http.ResponseWriter, r *http.Request) { 42 | clientset, err := GetClientset(kubeconfigPath) 43 | if err != nil { 44 | http.Error(w, "Failed to create Kubernetes client: "+err.Error(), http.StatusInternalServerError) 45 | return 46 | } 47 | 48 | namespaces, err := clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{}) 49 | if err != nil { 50 | // Handle forbidden access error specifically 51 | if statusErr, isStatus := err.(*k8serrors.StatusError); isStatus { 52 | if statusErr.Status().Code == http.StatusForbidden { 53 | http.Error(w, "Access forbidden: "+err.Error(), http.StatusForbidden) 54 | return 55 | } 56 | } 57 | http.Error(w, "Failed to list namespaces: "+err.Error(), http.StatusInternalServerError) 58 | return 59 | } 60 | 61 | var namespaceList []string 62 | for _, ns := range namespaces.Items { 63 | if !IsSystemNamespace(ns.Name) { 64 | namespaceList = append(namespaceList, ns.Name) 65 | } 66 | } 67 | 68 | w.Header().Set("Content-Type", "application/json") 69 | json.NewEncoder(w).Encode(map[string][]string{"namespaces": namespaceList}) 70 | } 71 | } 72 | 73 | func HandleAddPolicyRequest(kubeconfigPath string) http.HandlerFunc { 74 | return func(w http.ResponseWriter, r *http.Request) { 75 | // Define a struct to parse the incoming request 76 | type request struct { 77 | Namespace string `json:"namespace"` 78 | } 79 | 80 | // Parse the incoming JSON request 81 | var req request 82 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 83 | http.Error(w, err.Error(), http.StatusBadRequest) 84 | return 85 | } 86 | 87 | // Apply the default deny policy 88 | err := createAndApplyDefaultDenyPolicy(req.Namespace, kubeconfigPath) 89 | if err != nil { 90 | http.Error(w, "Failed to apply default deny policy: "+err.Error(), http.StatusInternalServerError) 91 | return 92 | } 93 | 94 | // Respond with success message 95 | w.Header().Set("Content-Type", "application/json") 96 | json.NewEncoder(w).Encode(map[string]string{ 97 | "message": "Implicit default deny all network policy successfully added to namespace " + req.Namespace, 98 | }) 99 | 100 | // Re-scan the namespace 101 | scanResult, err := ScanNetworkPolicies(req.Namespace, false, true, false, false, false, kubeconfigPath) 102 | if err != nil { 103 | http.Error(w, "Error re-scanning after applying policy: "+err.Error(), http.StatusInternalServerError) 104 | return 105 | } 106 | 107 | // Respond with updated scan results 108 | w.Header().Set("Content-Type", "application/json") 109 | json.NewEncoder(w).Encode(scanResult) 110 | } 111 | } 112 | 113 | // HandleNamespacesWithPoliciesRequest handles the HTTP request for serving a list of namespaces with network policies. 114 | func HandleNamespacesWithPoliciesRequest(kubeconfigPath string) http.HandlerFunc { 115 | return func(w http.ResponseWriter, r *http.Request) { 116 | if r.Method != http.MethodGet { 117 | http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) 118 | return 119 | } 120 | 121 | clientset, err := GetClientset(kubeconfigPath) 122 | if err != nil { 123 | http.Error(w, "You are not connected to a Kubernetes cluster. Please connect to a cluster and re-run the command: "+err.Error(), http.StatusInternalServerError) 124 | return 125 | } 126 | 127 | namespaces, err := GatherNamespacesWithPolicies(clientset) 128 | if err != nil { 129 | http.Error(w, err.Error(), http.StatusInternalServerError) 130 | return 131 | } 132 | 133 | setNoCacheHeaders(w) 134 | w.Header().Set("Content-Type", "application/json") 135 | if err := json.NewEncoder(w).Encode(struct { 136 | Namespaces []string `json:"namespaces"` 137 | }{Namespaces: namespaces}); err != nil { 138 | http.Error(w, "Failed to encode namespaces data", http.StatusInternalServerError) 139 | } 140 | } 141 | } 142 | 143 | // HandleNamespacePoliciesRequest handles the HTTP request for serving a list of network policies in a namespace. 144 | func HandleNamespacePoliciesRequest(kubeconfigPath string) http.HandlerFunc { 145 | return func(w http.ResponseWriter, r *http.Request) { 146 | if r.Method != http.MethodGet { 147 | http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) 148 | return 149 | } 150 | 151 | // Extract the namespace parameter from the query string 152 | namespace := r.URL.Query().Get("namespace") 153 | if namespace == "" { 154 | http.Error(w, "Namespace parameter is required", http.StatusBadRequest) 155 | return 156 | } 157 | 158 | // Obtain the Kubernetes clientset 159 | clientset, err := GetClientset(kubeconfigPath) 160 | if err != nil { 161 | http.Error(w, fmt.Sprintf("Failed to create Kubernetes client: %v", err), http.StatusInternalServerError) 162 | return 163 | } 164 | 165 | // Fetch network policies from the specified namespace 166 | policies, err := clientset.NetworkingV1().NetworkPolicies(namespace).List(context.Background(), metav1.ListOptions{}) 167 | if err != nil { 168 | http.Error(w, fmt.Sprintf("Failed to get network policies: %v", err), http.StatusInternalServerError) 169 | return 170 | } 171 | 172 | setNoCacheHeaders(w) 173 | w.Header().Set("Content-Type", "application/json") 174 | json.NewEncoder(w).Encode(policies) 175 | } 176 | } 177 | 178 | // HandleClusterVisualizationRequest handles the HTTP request for serving cluster-wide visualization data. 179 | func HandleClusterVisualizationRequest(kubeconfigPath string) http.HandlerFunc { 180 | return func(w http.ResponseWriter, r *http.Request) { 181 | if r.Method != http.MethodGet { 182 | http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) 183 | return 184 | } 185 | 186 | clientset, err := GetClientset(kubeconfigPath) 187 | if err != nil { 188 | http.Error(w, err.Error(), http.StatusInternalServerError) 189 | return 190 | } 191 | 192 | // Call the function to gather cluster-wide visualization data 193 | clusterVizData, err := GatherClusterVisualizationData(clientset) 194 | if err != nil { 195 | http.Error(w, err.Error(), http.StatusInternalServerError) 196 | return 197 | } 198 | 199 | setNoCacheHeaders(w) 200 | w.Header().Set("Content-Type", "application/json") 201 | if err := json.NewEncoder(w).Encode(clusterVizData); err != nil { 202 | http.Error(w, "Failed to encode cluster visualization data", http.StatusInternalServerError) 203 | } 204 | } 205 | } 206 | 207 | // HandlePodInfoRequest handles the HTTP request for serving pod information. 208 | func HandlePodInfoRequest(kubeconfigPath string) http.HandlerFunc { 209 | return func(w http.ResponseWriter, r *http.Request) { 210 | if r.Method != http.MethodGet { 211 | http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) 212 | return 213 | } 214 | 215 | // Extract the namespace parameter from the query string 216 | namespace := r.URL.Query().Get("namespace") 217 | if namespace == "" { 218 | http.Error(w, "Namespace parameter is required", http.StatusBadRequest) 219 | return 220 | } 221 | 222 | // Obtain the Kubernetes clientset 223 | clientset, err := GetClientset(kubeconfigPath) 224 | if err != nil { 225 | http.Error(w, fmt.Sprintf("Failed to create Kubernetes client: %v", err), http.StatusInternalServerError) 226 | return 227 | } 228 | 229 | // Fetch pod information from the specified namespace 230 | podInfo, err := GetPodInfo(clientset, namespace) 231 | if err != nil { 232 | http.Error(w, fmt.Sprintf("Failed to get pod information: %v", err), http.StatusInternalServerError) 233 | return 234 | } 235 | 236 | setNoCacheHeaders(w) 237 | w.Header().Set("Content-Type", "application/json") 238 | json.NewEncoder(w).Encode(podInfo) 239 | } 240 | } 241 | 242 | // HandleCreatePolicyRequest handles the HTTP request to create a network policy from YAML. 243 | func HandleCreatePolicyRequest(kubeconfigPath string) http.HandlerFunc { 244 | return func(w http.ResponseWriter, r *http.Request) { 245 | if r.Method != http.MethodPost { 246 | http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) 247 | return 248 | } 249 | 250 | var policyRequest struct { 251 | YAML string `json:"yaml"` 252 | Namespace string `json:"namespace"` 253 | } 254 | if err := json.NewDecoder(r.Body).Decode(&policyRequest); err != nil { 255 | http.Error(w, fmt.Sprintf("Failed to decode request body: %v", err), http.StatusBadRequest) 256 | return 257 | } 258 | defer r.Body.Close() 259 | 260 | clientset, err := GetClientset(kubeconfigPath) 261 | if err != nil { 262 | http.Error(w, fmt.Sprintf("Failed to create Kubernetes client: %v", err), http.StatusInternalServerError) 263 | return 264 | } 265 | 266 | networkPolicy, err := YAMLToNetworkPolicy(policyRequest.YAML) 267 | if err != nil { 268 | http.Error(w, fmt.Sprintf("Failed to parse network policy YAML: %v", err), http.StatusBadRequest) 269 | return 270 | } 271 | 272 | createdPolicy, err := clientset.NetworkingV1().NetworkPolicies(policyRequest.Namespace).Create(context.Background(), networkPolicy, metav1.CreateOptions{}) 273 | if err != nil { 274 | http.Error(w, fmt.Sprintf("Failed to create network policy: %v", err), http.StatusInternalServerError) 275 | return 276 | } 277 | 278 | w.Header().Set("Content-Type", "application/json") 279 | json.NewEncoder(w).Encode(createdPolicy) 280 | } 281 | } 282 | 283 | func HandleVisualizationRequest(kubeconfigPath string) http.HandlerFunc { 284 | return func(w http.ResponseWriter, r *http.Request) { 285 | if r.Method != http.MethodGet { 286 | http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) 287 | return 288 | } 289 | 290 | namespace := r.URL.Query().Get("namespace") 291 | 292 | clientset, err := GetClientset(kubeconfigPath) 293 | if err != nil { 294 | http.Error(w, "Failed to create Kubernetes client: "+err.Error(), http.StatusInternalServerError) 295 | return 296 | } 297 | 298 | vizData, err := gatherVisualizationData(clientset, namespace) 299 | if err != nil { 300 | http.Error(w, "Failed to gather visualization data: "+err.Error(), http.StatusInternalServerError) 301 | return 302 | } 303 | 304 | w.Header().Set("Content-Type", "application/json") 305 | if err := json.NewEncoder(w).Encode(vizData); err != nil { 306 | http.Error(w, "Failed to encode visualization data: "+err.Error(), http.StatusInternalServerError) 307 | } 308 | } 309 | } 310 | 311 | // HandlePolicyYAMLRequest handles the HTTP request for serving the YAML of a network policy. 312 | func HandlePolicyYAMLRequest(kubeconfigPath string) http.HandlerFunc { 313 | return func(w http.ResponseWriter, r *http.Request) { 314 | if r.Method != http.MethodGet { 315 | http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) 316 | return 317 | } 318 | 319 | // Extract the policy name and namespace from query parameters 320 | policyName := r.URL.Query().Get("name") 321 | namespace := r.URL.Query().Get("namespace") 322 | if policyName == "" || namespace == "" { 323 | http.Error(w, "Policy name or namespace not provided", http.StatusBadRequest) 324 | return 325 | } 326 | 327 | // Retrieve the network policy YAML 328 | clientset, err := GetClientset(kubeconfigPath) 329 | if err != nil { 330 | http.Error(w, err.Error(), http.StatusInternalServerError) 331 | return 332 | } 333 | 334 | yamlData, err := getNetworkPolicyYAML(clientset, namespace, policyName) 335 | if err != nil { 336 | http.Error(w, err.Error(), http.StatusInternalServerError) 337 | return 338 | } 339 | 340 | w.Header().Set("Content-Type", "application/x-yaml") 341 | w.Write([]byte(yamlData)) 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /backend/pkg/k8s/scanner.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "errors" 8 | "fmt" 9 | "net" 10 | "net/url" 11 | "os" 12 | "path/filepath" 13 | "regexp" 14 | "strings" 15 | 16 | "github.com/AlecAivazis/survey/v2" 17 | v1 "k8s.io/api/core/v1" 18 | networkingv1 "k8s.io/api/networking/v1" 19 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | "k8s.io/apimachinery/pkg/runtime/serializer" 22 | "k8s.io/client-go/kubernetes" 23 | "k8s.io/client-go/kubernetes/scheme" 24 | _ "k8s.io/client-go/plugin/pkg/client/auth" 25 | "k8s.io/client-go/rest" 26 | "k8s.io/client-go/tools/clientcmd" 27 | "k8s.io/client-go/util/homedir" 28 | ) 29 | 30 | // Helper function to write to both buffer and standard output 31 | func printToBoth(writer *bufio.Writer, s string) { 32 | // Print to standard output with ANSI codes 33 | fmt.Print(s) 34 | 35 | // Write to buffer without ANSI codes 36 | cleanString := StripANSICodes(s) 37 | fmt.Fprint(writer, cleanString) 38 | } 39 | 40 | // StripANSICodes removes ANSI escape codes from a string 41 | func StripANSICodes(str string) string { 42 | ansi := regexp.MustCompile(`\x1B\[[0-9;]*[a-zA-Z]`) 43 | return ansi.ReplaceAllString(str, "") 44 | } 45 | 46 | // Struct to represent scan results in dashboard 47 | type ScanResult struct { 48 | NamespacesScanned []string 49 | DeniedNamespaces []string 50 | UnprotectedPods []string 51 | PolicyChangesMade bool 52 | UserDeniedPolicies bool 53 | HasDenyAll []string 54 | Score int 55 | AllPodsProtected bool 56 | } 57 | 58 | // Check if error scanning is related to network issues 59 | func isNetworkError(err error) bool { 60 | var urlError *url.Error 61 | var netOpError *net.OpError 62 | var dnsError *net.DNSError 63 | 64 | if errors.As(err, &urlError) { 65 | if errors.As(urlError.Err, &netOpError) { 66 | if errors.As(netOpError.Err, &dnsError) { 67 | if dnsError.IsNotFound { 68 | return true 69 | } 70 | } 71 | } 72 | } 73 | return false 74 | } 75 | 76 | // Initialize client 77 | func InitializeClient(kubeconfigPath string) (*kubernetes.Clientset, error) { 78 | clientset, err := GetClientset(kubeconfigPath) 79 | if err != nil { 80 | fmt.Printf("Error creating Kubernetes client: %s\n", err) 81 | return nil, err 82 | } 83 | return clientset, nil 84 | } 85 | 86 | // Select which namespace to scan 87 | func SelectNamespaces(clientset *kubernetes.Clientset, specificNamespace string) ([]string, error) { 88 | var namespaces []string 89 | if specificNamespace != "" { 90 | _, err := clientset.CoreV1().Namespaces().Get(context.TODO(), specificNamespace, metav1.GetOptions{}) 91 | if err != nil { 92 | if k8serrors.IsNotFound(err) { 93 | return nil, fmt.Errorf("namespace %s does not exist", specificNamespace) 94 | } 95 | return nil, fmt.Errorf("error checking namespace %s: %w", specificNamespace, err) 96 | } 97 | namespaces = append(namespaces, specificNamespace) 98 | } else { 99 | nsList, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) 100 | if err != nil { 101 | if isNetworkError(err) { 102 | return nil, fmt.Errorf("network error while listing namespaces, please check your connection to the Kubernetes cluster: %w", err) 103 | } 104 | return nil, fmt.Errorf("error listing namespaces: %w", err) 105 | } 106 | for _, ns := range nsList.Items { 107 | if !IsSystemNamespace(ns.Name) { 108 | namespaces = append(namespaces, ns.Name) 109 | } 110 | } 111 | } 112 | return namespaces, nil 113 | } 114 | 115 | // promptForPolicyApplication asks the user whether to apply a default deny policy 116 | func promptForPolicyApplication(namespace string, writer *bufio.Writer) bool { 117 | var confirm bool 118 | prompt := &survey.Confirm{ 119 | Message: fmt.Sprintf("Do you want to add a default deny all network policy to the namespace %s?", namespace), 120 | } 121 | err := survey.AskOne(prompt, &confirm) 122 | if err != nil { 123 | fmt.Fprintf(writer, "Error prompting for policy application: %s\n", err) 124 | return false 125 | } 126 | return confirm 127 | } 128 | 129 | // Fetches all network policies for a namespace and returns a map of covered pods 130 | func fetchCoveredPods(clientset *kubernetes.Clientset, nsName string, writer *bufio.Writer) (map[string]bool, error) { 131 | coveredPods := make(map[string]bool) 132 | policies, err := clientset.NetworkingV1().NetworkPolicies(nsName).List(context.TODO(), metav1.ListOptions{}) 133 | if err != nil { 134 | printToBoth(writer, fmt.Sprintf("\nError listing network policies in namespace %s: %s\n", nsName, err)) 135 | return nil, fmt.Errorf("error listing network policies: %w", err) 136 | } 137 | 138 | for _, policy := range policies.Items { 139 | selector, err := metav1.LabelSelectorAsSelector(&policy.Spec.PodSelector) 140 | if err != nil { 141 | printToBoth(writer, fmt.Sprintf("Error parsing selector for policy %s: %s\n", policy.Name, err)) 142 | continue 143 | } 144 | pods, err := clientset.CoreV1().Pods(nsName).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()}) 145 | if err != nil { 146 | printToBoth(writer, fmt.Sprintf("Error listing pods for policy %s: %s\n", policy.Name, err)) 147 | continue 148 | } 149 | for _, pod := range pods.Items { 150 | coveredPods[pod.Name] = true 151 | } 152 | } 153 | return coveredPods, nil 154 | } 155 | 156 | // Fetches all pods in a namespace and determines which are unprotected 157 | func determineUnprotectedPods(clientset *kubernetes.Clientset, nsName string, coveredPods map[string]bool, writer *bufio.Writer, scanResult *ScanResult) ([]string, error) { 158 | unprotectedPods := []string{} 159 | allPods, err := clientset.CoreV1().Pods(nsName).List(context.TODO(), metav1.ListOptions{}) 160 | if err != nil { 161 | printToBoth(writer, fmt.Sprintf("Error listing all pods in namespace %s: %s\n", nsName, err)) 162 | return nil, fmt.Errorf("error listing all pods: %w", err) 163 | } 164 | 165 | for _, pod := range allPods.Items { 166 | // Skip pods that are not in running state 167 | if pod.Status.Phase != v1.PodRunning { 168 | continue 169 | } 170 | if !coveredPods[pod.Name] { 171 | podDetail := fmt.Sprintf("%s %s %s", nsName, pod.Name, pod.Status.PodIP) 172 | if !containsPodDetail(scanResult.UnprotectedPods, podDetail) { 173 | unprotectedPods = append(unprotectedPods, podDetail) 174 | } 175 | } 176 | } 177 | return unprotectedPods, nil 178 | } 179 | 180 | // This function just displays unprotected pods without prompting for any actions 181 | func displayUnprotectedPods(nsName string, unprotectedPods []string, writer *bufio.Writer) { 182 | if len(unprotectedPods) > 0 { 183 | headerText := fmt.Sprintf("Unprotected pods found in namespace %s:", nsName) 184 | styledHeaderText := HeaderStyle.Render(headerText) 185 | printToBoth(writer, styledHeaderText+"\n") 186 | 187 | podsInfo := make([][]string, len(unprotectedPods)) 188 | for i, podDetail := range unprotectedPods { 189 | podsInfo[i] = strings.Fields(podDetail) 190 | } 191 | 192 | tableOutput := createPodsTable(podsInfo) 193 | printToBoth(writer, tableOutput+"\n") 194 | } 195 | } 196 | 197 | func handleCLIInteractions(nsName string, unprotectedPods []string, writer *bufio.Writer, scanResult *ScanResult, kubeconfigPath string) { 198 | if len(unprotectedPods) > 0 { 199 | // Header 200 | headerText := fmt.Sprintf("Unprotected pods found in namespace %s:", nsName) 201 | styledHeaderText := HeaderStyle.Render(headerText) 202 | printToBoth(writer, styledHeaderText+"\n") 203 | 204 | // Prepare data for table 205 | podsInfo := make([][]string, len(unprotectedPods)) 206 | for i, podDetail := range unprotectedPods { 207 | podsInfo[i] = strings.Fields(podDetail) 208 | } 209 | 210 | // Create and print table 211 | tableOutput := createPodsTable(podsInfo) 212 | printToBoth(writer, tableOutput+"\n") 213 | 214 | // Prompt for applying policies 215 | if promptForPolicyApplication(nsName, writer) { 216 | err := createAndApplyDefaultDenyPolicy(nsName, kubeconfigPath) 217 | if err != nil { 218 | fmt.Fprintf(writer, "Failed to apply default deny policy in namespace %s: %s\n", nsName, err) 219 | } else { 220 | fmt.Fprintf(writer, "Applied default deny policy in namespace %s\n", nsName) 221 | scanResult.PolicyChangesMade = true 222 | } 223 | } 224 | } 225 | } 226 | 227 | func processNamespacePolicies(clientset *kubernetes.Clientset, nsName string, writer *bufio.Writer, isCLI bool, dryRun bool, scanResult *ScanResult, kubeconfigPath string) error { 228 | // Fetch covered pods 229 | coveredPods, err := fetchCoveredPods(clientset, nsName, writer) 230 | if err != nil { 231 | return fmt.Errorf("fetching covered pods failed for namespace %s: %w", nsName, err) 232 | } 233 | 234 | // Determine unprotected pods 235 | unprotectedPods, err := determineUnprotectedPods(clientset, nsName, coveredPods, writer, scanResult) 236 | if err != nil { 237 | return fmt.Errorf("determining unprotected pods failed for namespace %s: %w", nsName, err) 238 | } 239 | 240 | // Always add pods to result for visibility 241 | scanResult.UnprotectedPods = append(scanResult.UnprotectedPods, unprotectedPods...) 242 | scanResult.DeniedNamespaces = append(scanResult.DeniedNamespaces, nsName) 243 | 244 | // Only handle CLI interactions if it's CLI mode and not a dry run 245 | if isCLI && !dryRun { 246 | handleCLIInteractions(nsName, unprotectedPods, writer, scanResult, kubeconfigPath) 247 | } else if dryRun { 248 | // If it's a dry run, we just display the data without prompting for any actions 249 | displayUnprotectedPods(nsName, unprotectedPods, writer) 250 | } 251 | 252 | return nil 253 | } 254 | 255 | var hasStartedNativeScan bool = false 256 | 257 | // ScanNetworkPolicies scans namespaces for network policies 258 | func ScanNetworkPolicies(specificNamespace string, dryRun bool, returnResult bool, isCLI bool, printScore bool, printMessages bool, kubeconfigPath string) (*ScanResult, error) { 259 | var output bytes.Buffer 260 | var namespacesToScan []string 261 | 262 | unprotectedPodsCount := 0 263 | scanResult := new(ScanResult) 264 | 265 | writer := bufio.NewWriter(&output) 266 | 267 | clientset, err := InitializeClient(kubeconfigPath) 268 | if err != nil { 269 | return nil, err 270 | } 271 | 272 | namespacesToScan, err = SelectNamespaces(clientset, specificNamespace) 273 | if err != nil { 274 | return nil, err 275 | } 276 | 277 | missingPoliciesOrUncoveredPods := false 278 | userDeniedPolicyApplication := false 279 | deniedNamespaces := []string{} 280 | 281 | if isCLI && !hasStartedNativeScan { 282 | fmt.Println("Policy type: Kubernetes") 283 | hasStartedNativeScan = true 284 | } 285 | 286 | for _, nsName := range namespacesToScan { 287 | err := processNamespacePolicies(clientset, nsName, writer, isCLI, dryRun, scanResult, kubeconfigPath) 288 | if err != nil { 289 | fmt.Printf("Error processing namespace %s: %v\n", nsName, err) 290 | continue 291 | } 292 | unprotectedPodsCount += len(scanResult.UnprotectedPods) 293 | 294 | // Check if namespace is already marked as denied 295 | if !contains(deniedNamespaces, nsName) { 296 | deniedNamespaces = append(deniedNamespaces, nsName) 297 | } 298 | } 299 | 300 | writer.Flush() 301 | if output.Len() > 0 { 302 | handleOutputAndPrompts(writer, &output) 303 | } 304 | 305 | score := CalculateScore(!missingPoliciesOrUncoveredPods, !userDeniedPolicyApplication, unprotectedPodsCount) 306 | scanResult.Score = score 307 | 308 | if printMessages { 309 | printToBoth(writer, "\nNetfetch scan completed!\n") 310 | } 311 | 312 | if printScore { 313 | // Print the final score 314 | fmt.Printf("\nYour Netfetch security score is: %d/100\n", score) 315 | } 316 | 317 | hasStartedNativeScan = false 318 | return scanResult, nil 319 | } 320 | 321 | // handleOutputAndPrompts manages saving scan results to a file and outputting 322 | func handleOutputAndPrompts(writer *bufio.Writer, output *bytes.Buffer) { 323 | saveToFile := false 324 | prompt := &survey.Confirm{ 325 | Message: "Do you want to save the output to netfetch.txt?", 326 | } 327 | survey.AskOne(prompt, &saveToFile, nil) 328 | 329 | if saveToFile { 330 | err := os.WriteFile("netfetch.txt", output.Bytes(), 0644) 331 | if err != nil { 332 | errorFileMsg := fmt.Sprintf("Error writing to file: %s\n", err) 333 | printToBoth(writer, errorFileMsg) 334 | } else { 335 | printToBoth(writer, "Output file created: netfetch.txt\n") 336 | } 337 | } else { 338 | printToBoth(writer, "Output file not created.\n") 339 | } 340 | } 341 | 342 | // Function to create the implicit default deny if missing 343 | func createAndApplyDefaultDenyPolicy(namespace string, kubeconfigPath string) error { 344 | // Initialize Kubernetes client 345 | clientset, err := GetClientset(kubeconfigPath) 346 | if err != nil { 347 | return fmt.Errorf("failed to create Kubernetes client: %v", err) 348 | } 349 | 350 | // Define the network policy 351 | policyName := namespace + "-default-deny-all" 352 | policy := &networkingv1.NetworkPolicy{ 353 | ObjectMeta: metav1.ObjectMeta{ 354 | Name: policyName, 355 | Namespace: namespace, 356 | }, 357 | Spec: networkingv1.NetworkPolicySpec{ 358 | PodSelector: metav1.LabelSelector{}, 359 | PolicyTypes: []networkingv1.PolicyType{ 360 | networkingv1.PolicyTypeIngress, 361 | networkingv1.PolicyTypeEgress, 362 | }, 363 | }, 364 | } 365 | 366 | // Create the policy 367 | _, err = clientset.NetworkingV1().NetworkPolicies(namespace).Create(context.TODO(), policy, metav1.CreateOptions{}) 368 | return err 369 | } 370 | 371 | // hasDefaultDenyAllPolicy checks if the list of policies includes a default deny all policy 372 | func hasDefaultDenyAllPolicy(policies []networkingv1.NetworkPolicy) bool { 373 | for _, policy := range policies { 374 | if isDefaultDenyAllPolicy(policy) { 375 | return true 376 | } 377 | } 378 | return false 379 | } 380 | 381 | // isDefaultDenyAllPolicy checks if a single network policy is a default deny all policy 382 | func isDefaultDenyAllPolicy(policy networkingv1.NetworkPolicy) bool { 383 | return len(policy.Spec.Ingress) == 0 && len(policy.Spec.Egress) == 0 384 | } 385 | 386 | // isSystemNamespace checks if the given namespace is a system namespace 387 | func IsSystemNamespace(namespace string) bool { 388 | switch namespace { 389 | case "kube-system", "tigera-operator", "kube-public", "kube-node-lease", "gatekeeper-system", "calico-system": 390 | return true 391 | default: 392 | return false 393 | } 394 | } 395 | 396 | // Scoring logic 397 | func CalculateScore(hasPolicies bool, hasDenyAll bool, unprotectedPodsCount int) int { 398 | score := 50 // Start with a base score of 50 399 | 400 | // if hasDenyAll { 401 | // score += 20 // Add 20 points for having deny-all policies 402 | // } else if !hasPolicies { 403 | // score -= 20 // Subtract 20 points if there are no policies at all 404 | // } 405 | 406 | // Deduct score based on the number of unprotected pods 407 | score -= unprotectedPodsCount 408 | 409 | if score > 100 { 410 | score = 100 411 | } else if score < 1 { 412 | score = 1 413 | } 414 | 415 | return score 416 | } 417 | 418 | var ( 419 | isClientInitialized = false 420 | clientset *kubernetes.Clientset 421 | ) 422 | 423 | // GetClientset creates a new Kubernetes clientset 424 | func GetClientset(kubeconfigPath string) (*kubernetes.Clientset, error) { 425 | if isClientInitialized { 426 | return clientset, nil 427 | } 428 | 429 | var config *rest.Config 430 | var err error 431 | 432 | if kubeconfigPath != "" { 433 | // Use the provided kubeconfig 434 | fmt.Println("Mode: CLI") 435 | fmt.Println("Using provided kubeconfig path: ", kubeconfigPath) 436 | fmt.Printf("\n") 437 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath) 438 | if err != nil { 439 | return nil, fmt.Errorf("failed to build config from kubeconfig path %s: %v", kubeconfigPath, err) 440 | } 441 | } else { 442 | // First try to use the in-cluster configuration 443 | config, err = rest.InClusterConfig() 444 | if err == nil { 445 | fmt.Println("Using in-cluster Kubernetes configuration") 446 | } else { 447 | fmt.Println("Mode: CLI") 448 | 449 | // Fallback to kubeconfig 450 | var kubeconfig string 451 | if kc := os.Getenv("KUBECONFIG"); kc != "" { 452 | kubeconfig = kc 453 | fmt.Println("Using KUBECONFIG from environment:", kubeconfig) 454 | } else { 455 | kubeconfig = filepath.Join(homedir.HomeDir(), ".kube", "config") 456 | fmt.Println("Using default kubeconfig path:", kubeconfig) 457 | fmt.Printf("\n") 458 | } 459 | 460 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) 461 | if err != nil { 462 | return nil, fmt.Errorf("failed to build config from kubeconfig path %s: %v", kubeconfig, err) 463 | } 464 | } 465 | } 466 | 467 | // Create and store the clientset 468 | clientset, err = kubernetes.NewForConfig(config) 469 | if err != nil { 470 | return nil, fmt.Errorf("failed to create clientset: %v", err) 471 | } 472 | 473 | isClientInitialized = true 474 | return clientset, nil 475 | } 476 | 477 | // contains checks if a string is present in a slice 478 | func contains(slice []string, str string) bool { 479 | for _, v := range slice { 480 | if v == str { 481 | return true 482 | } 483 | } 484 | return false 485 | } 486 | 487 | // containsPodDetail checks if a pod detail string is present in a slice 488 | func containsPodDetail(slice []string, detail string) bool { 489 | for _, v := range slice { 490 | if v == detail { 491 | return true 492 | } 493 | } 494 | return false 495 | } 496 | 497 | // PodInfo holds the desired information from a Pods YAML. 498 | type PodInfo struct { 499 | Name string 500 | Namespace string 501 | Labels map[string]string 502 | Ports []v1.ContainerPort 503 | } 504 | 505 | // Hold the desired info from a Pods ports 506 | type ContainerPortInfo struct { 507 | Name string 508 | ContainerPort int32 509 | Protocol v1.Protocol 510 | } 511 | 512 | func GetPodInfo(clientset kubernetes.Interface, namespace string) ([]PodInfo, error) { 513 | pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) 514 | if err != nil { 515 | return nil, err 516 | } 517 | 518 | var podInfos []PodInfo 519 | for _, pod := range pods.Items { 520 | var containerPorts []v1.ContainerPort 521 | for _, container := range pod.Spec.Containers { 522 | containerPorts = append(containerPorts, container.Ports...) 523 | } 524 | 525 | podInfo := PodInfo{ 526 | Name: pod.Name, 527 | Namespace: pod.Namespace, 528 | Labels: pod.Labels, 529 | Ports: containerPorts, 530 | } 531 | podInfos = append(podInfos, podInfo) 532 | } 533 | 534 | return podInfos, nil 535 | } 536 | 537 | // YAMLToNetworkPolicy converts a YAML string to a NetworkPolicy object. 538 | func YAMLToNetworkPolicy(yamlContent string) (*networkingv1.NetworkPolicy, error) { 539 | decoder := serializer.NewCodecFactory(scheme.Scheme).UniversalDeserializer() 540 | obj, _, err := decoder.Decode([]byte(yamlContent), nil, nil) 541 | if err != nil { 542 | return nil, err 543 | } 544 | 545 | networkPolicy, ok := obj.(*networkingv1.NetworkPolicy) 546 | if !ok { 547 | return nil, fmt.Errorf("decoded object is not a NetworkPolicy") 548 | } 549 | 550 | return networkPolicy, nil 551 | } 552 | -------------------------------------------------------------------------------- /backend/pkg/k8s/scanner_test.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | corev1 "k8s.io/api/core/v1" 9 | netv1 "k8s.io/api/networking/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/client-go/kubernetes" 12 | "k8s.io/client-go/kubernetes/fake" 13 | ) 14 | 15 | func TestHasDefaultDenyAllPolicy(t *testing.T) { 16 | // Test case 1: Default deny all policy exists 17 | policyWithDefaultDeny := netv1.NetworkPolicy{ 18 | Spec: netv1.NetworkPolicySpec{}, 19 | } 20 | if !hasDefaultDenyAllPolicy([]netv1.NetworkPolicy{policyWithDefaultDeny}) { 21 | t.Errorf("Expected to identify default deny all policy, but it was not detected") 22 | } 23 | 24 | // Test case 2: No default deny all policy 25 | policyWithoutDefaultDeny := netv1.NetworkPolicy{ 26 | Spec: netv1.NetworkPolicySpec{ 27 | Ingress: []netv1.NetworkPolicyIngressRule{ 28 | {}, 29 | }, 30 | Egress: []netv1.NetworkPolicyEgressRule{ 31 | {}, 32 | }, 33 | }, 34 | } 35 | if hasDefaultDenyAllPolicy([]netv1.NetworkPolicy{policyWithoutDefaultDeny}) { 36 | t.Errorf("Expected not to identify default deny all policy, but it was detected") 37 | } 38 | } 39 | 40 | func TestIsDefaultDenyAllPolicy(t *testing.T) { 41 | // Test case 1: Default deny all policy 42 | defaultDenyPolicy := netv1.NetworkPolicy{ 43 | Spec: netv1.NetworkPolicySpec{}, 44 | } 45 | if !isDefaultDenyAllPolicy(defaultDenyPolicy) { 46 | t.Fatalf("Expected policy to be default deny all, but it was not") 47 | } 48 | 49 | // Test case 2: Non-default deny all policy 50 | nonDefaultDenyPolicy := netv1.NetworkPolicy{ 51 | Spec: netv1.NetworkPolicySpec{ 52 | Ingress: []netv1.NetworkPolicyIngressRule{ 53 | {}, 54 | }, 55 | Egress: []netv1.NetworkPolicyEgressRule{ 56 | {}, 57 | }, 58 | }, 59 | } 60 | if isDefaultDenyAllPolicy(nonDefaultDenyPolicy) { 61 | t.Fatalf("Expected policy not to be default deny all, but it was") 62 | } 63 | } 64 | 65 | func TestIsSystemNamespace(t *testing.T) { 66 | // Test case 1: System namespace 67 | systemNamespaces := []string{"kube-system", "tigera-operator", "kube-public", "kube-node-lease", "gatekeeper-system", "calico-system"} 68 | for _, ns := range systemNamespaces { 69 | if !IsSystemNamespace(ns) { 70 | t.Fatalf("Expected namespace %s to be a system namespace, but it was not", ns) 71 | } 72 | } 73 | 74 | // Test case 2: Non-system namespace 75 | nonSystemNamespace := "test-namespace" 76 | if IsSystemNamespace(nonSystemNamespace) { 77 | t.Fatalf("Expected namespace %s not to be a system namespace, but it was", nonSystemNamespace) 78 | } 79 | } 80 | 81 | func TestCalculateScore(t *testing.T) { 82 | // Test case 1: Policies exist, deny all exists, no unprotected pods 83 | score1 := CalculateScore(true, true, 0) 84 | if score1 != 70 { // 50 base + 20 for deny-all 85 | t.Fatalf("Expected score to be 70, got %d", score1) 86 | } 87 | 88 | // Test case 2: No policies, no deny all, 5 unprotected pods 89 | score2 := CalculateScore(false, false, 5) 90 | if score2 != 25 { // 50 base - 20 for no policies - 5 for unprotected pods 91 | t.Fatalf("Expected score to be 25, got %d", score2) 92 | } 93 | 94 | // Test case 3: No policies, no deny all, no unprotected pods 95 | score3 := CalculateScore(false, false, 0) 96 | if score3 != 30 { // 50 base - 20 for no policies 97 | t.Fatalf("Expected score to be 30, got %d", score3) 98 | } 99 | 100 | // Test case 4: Policies exist, deny all exists, no unprotected pods (repeat of case 1 for consistency) 101 | score4 := CalculateScore(true, true, 0) 102 | if score4 != 70 { 103 | t.Fatalf("Expected score to be 70, got %d", score4) 104 | } 105 | } 106 | 107 | func TestGetPodInfo(t *testing.T) { 108 | var clientset kubernetes.Interface = fake.NewSimpleClientset() 109 | 110 | podInfo := PodInfo{ 111 | Name: "test-pod", 112 | Namespace: "test-namespace", 113 | Labels: map[string]string{ 114 | "app": "test", // Label on pod to match netpol selector 115 | }, 116 | Ports: []corev1.ContainerPort{ 117 | { 118 | ContainerPort: 80, 119 | }, 120 | }, 121 | } 122 | 123 | pod := &corev1.Pod{ 124 | ObjectMeta: metav1.ObjectMeta{ 125 | Name: podInfo.Name, 126 | Namespace: podInfo.Namespace, 127 | Labels: podInfo.Labels, 128 | }, 129 | Spec: corev1.PodSpec{ 130 | Containers: []corev1.Container{ 131 | { 132 | Name: "nginx", 133 | Image: "nginx", 134 | Ports: podInfo.Ports, 135 | }, 136 | }, 137 | }, 138 | } 139 | _, err := clientset.CoreV1().Pods("test-namespace").Create(context.TODO(), pod, metav1.CreateOptions{}) 140 | if err != nil { 141 | t.Fatalf("Failed to create pod: %v", err) 142 | } 143 | 144 | var expectedPodInfo []PodInfo 145 | expectedPodInfo = append(expectedPodInfo, podInfo) 146 | 147 | actualPodInfo, err := GetPodInfo(clientset, podInfo.Namespace) 148 | if err != nil { 149 | t.Fatalf("Failed to get actual podInfo: %v", err) 150 | } 151 | 152 | assert.Equal(t, expectedPodInfo, actualPodInfo, "they should be equal") 153 | } 154 | 155 | func TestYAMLToNetworkPolicy(t *testing.T) { 156 | YAMLString := ` 157 | apiVersion: networking.k8s.io/v1 158 | kind: NetworkPolicy 159 | metadata: 160 | name: test-policy 161 | namespace: test-namespace 162 | spec: 163 | podSelector: 164 | matchLabels: 165 | app: test 166 | ` 167 | expectedNetworkPolicy := &netv1.NetworkPolicy{ 168 | TypeMeta: metav1.TypeMeta{ 169 | APIVersion: "networking.k8s.io/v1", 170 | Kind: "NetworkPolicy", 171 | }, 172 | ObjectMeta: metav1.ObjectMeta{ 173 | Name: "test-policy", 174 | Namespace: "test-namespace", 175 | }, 176 | Spec: netv1.NetworkPolicySpec{ 177 | PodSelector: metav1.LabelSelector{ 178 | MatchLabels: map[string]string{"app": "test"}, 179 | }, 180 | }, 181 | } 182 | 183 | actualNetworkPolicy, err := YAMLToNetworkPolicy(YAMLString) 184 | if err != nil { 185 | t.Fatalf("Failed to convert YAML string to a NetworkPolicy object: %v", err) 186 | } 187 | 188 | assert.Equal(t, expectedNetworkPolicy, actualNetworkPolicy, "they should be equal") 189 | } -------------------------------------------------------------------------------- /backend/pkg/k8s/target-scanner.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | 8 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | "k8s.io/apimachinery/pkg/labels" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | "k8s.io/client-go/dynamic" 13 | "k8s.io/client-go/kubernetes" 14 | ) 15 | 16 | // FindNativeNetworkPolicyByName searches for a specific native network policy by name across all non-system namespaces. 17 | func FindNativeNetworkPolicyByName(dynamicClient dynamic.Interface, clientset *kubernetes.Clientset, policyName string) (*unstructured.Unstructured, string, error) { 18 | gvr := schema.GroupVersionResource{ 19 | Group: "networking.k8s.io", 20 | Version: "v1", 21 | Resource: "networkpolicies", 22 | } 23 | 24 | namespaces, err := GetAllNonSystemNamespaces(dynamicClient) 25 | if err != nil { 26 | return nil, "", fmt.Errorf("error getting namespaces: %v", err) 27 | } 28 | 29 | for _, namespace := range namespaces { 30 | policy, err := dynamicClient.Resource(gvr).Namespace(namespace).Get(context.TODO(), policyName, v1.GetOptions{}) 31 | if err == nil { 32 | return policy, namespace, nil 33 | } 34 | } 35 | return nil, "", fmt.Errorf("network policy %s not found in any non-system namespace", policyName) 36 | } 37 | 38 | // FindCiliumNetworkPolicyByName searches for a specific Cilium network policy by name across all non-system namespaces. 39 | func FindCiliumNetworkPolicyByName(dynamicClient dynamic.Interface, policyName string) (*unstructured.Unstructured, string, error) { 40 | gvr := schema.GroupVersionResource{ 41 | Group: "cilium.io", 42 | Version: "v2", 43 | Resource: "ciliumnetworkpolicies", 44 | } 45 | 46 | namespaces, err := GetAllNonSystemNamespaces(dynamicClient) 47 | if err != nil { 48 | return nil, "", fmt.Errorf("error getting namespaces: %v", err) 49 | } 50 | 51 | for _, namespace := range namespaces { 52 | policy, err := dynamicClient.Resource(gvr).Namespace(namespace).Get(context.TODO(), policyName, v1.GetOptions{}) 53 | if err == nil { 54 | return policy, namespace, nil 55 | } 56 | } 57 | return nil, "", fmt.Errorf("cilium network policy %s not found in any non-system namespace", policyName) 58 | } 59 | 60 | // FindCiliumClusterWideNetworkPolicyByName searches for a specific cluster wide Cilium network policy by name. 61 | func FindCiliumClusterWideNetworkPolicyByName(dynamicClient dynamic.Interface, policyName string) (*unstructured.Unstructured, error) { 62 | gvr := schema.GroupVersionResource{ 63 | Group: "cilium.io", 64 | Version: "v2", 65 | Resource: "ciliumclusterwidenetworkpolicies", 66 | } 67 | 68 | policy, err := dynamicClient.Resource(gvr).Get(context.TODO(), policyName, v1.GetOptions{}) 69 | if err != nil { 70 | return nil, fmt.Errorf("cilium cluster wide network policy %s not found", policyName) 71 | } 72 | return policy, nil 73 | } 74 | 75 | 76 | // GetAllNonSystemNamespaces returns a list of all non-system namespaces using a dynamic client. 77 | func GetAllNonSystemNamespaces(dynamicClient dynamic.Interface) ([]string, error) { 78 | gvr := schema.GroupVersionResource{ 79 | Group: "", 80 | Version: "v1", 81 | Resource: "namespaces", 82 | } 83 | 84 | namespacesList, err := dynamicClient.Resource(gvr).List(context.TODO(), v1.ListOptions{}) 85 | if err != nil { 86 | return nil, fmt.Errorf("error listing namespaces: %v", err) 87 | } 88 | 89 | var namespaces []string 90 | for _, ns := range namespacesList.Items { 91 | if !IsSystemNamespace(ns.GetName()) { 92 | namespaces = append(namespaces, ns.GetName()) 93 | } 94 | } 95 | return namespaces, nil 96 | } 97 | 98 | // ListPodsTargetedByNetworkPolicy lists all pods targeted by the given network policy in the specified namespace. 99 | func ListPodsTargetedByNetworkPolicy(dynamicClient dynamic.Interface, policy *unstructured.Unstructured, namespace string) ([][]string, error) { 100 | // Retrieve the PodSelector (matchLabels) 101 | podSelector, found, err := unstructured.NestedMap(policy.Object, "spec", "podSelector", "matchLabels") 102 | if err != nil { 103 | return nil, fmt.Errorf("failed to retrieve pod selector from network policy %s: %v", policy.GetName(), err) 104 | } 105 | 106 | // Check if the selector is empty 107 | selector := make(labels.Set) 108 | if found && len(podSelector) > 0 { 109 | for key, value := range podSelector { 110 | if strValue, ok := value.(string); ok { 111 | selector[key] = strValue 112 | } else { 113 | return nil, fmt.Errorf("invalid type for selector value %v in policy %s", value, policy.GetName()) 114 | } 115 | } 116 | } 117 | 118 | // Fetch pods based on the selector 119 | pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), v1.ListOptions{LabelSelector: selector.AsSelectorPreValidated().String()}) 120 | if err != nil { 121 | return nil, fmt.Errorf("error listing pods in namespace %s: %v", namespace, err) 122 | } 123 | 124 | var targetedPods [][]string 125 | for _, pod := range pods.Items { 126 | podDetails := []string{namespace, pod.Name, pod.Status.PodIP} 127 | if pod.Status.PodIP == "" { 128 | podDetails[2] = "N/A" 129 | } 130 | targetedPods = append(targetedPods, podDetails) 131 | } 132 | 133 | return targetedPods, nil 134 | } 135 | 136 | // ListPodsTargetedByCiliumNetworkPolicy lists all pods targeted by the given Cilium network policy in the specified namespace. 137 | func ListPodsTargetedByCiliumNetworkPolicy(dynamicClient dynamic.Interface, policy *unstructured.Unstructured, namespace string) ([][]string, error) { 138 | // Retrieve the PodSelector (matchLabels) 139 | podSelector, found, err := unstructured.NestedMap(policy.Object, "spec", "endpointSelector", "matchLabels") 140 | if err != nil { 141 | return nil, fmt.Errorf("failed to retrieve pod selector from Cilium network policy %s: %v", policy.GetName(), err) 142 | } 143 | 144 | // Check if the selector is empty 145 | selector := make(labels.Set) 146 | if found && len(podSelector) > 0 { 147 | for key, value := range podSelector { 148 | if strValue, ok := value.(string); ok { 149 | selector[key] = strValue 150 | } else { 151 | return nil, fmt.Errorf("invalid type for selector value %v in policy %s", value, policy.GetName()) 152 | } 153 | } 154 | } 155 | 156 | // Fetch pods based on the selector 157 | pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), v1.ListOptions{LabelSelector: selector.AsSelectorPreValidated().String()}) 158 | if err != nil { 159 | return nil, fmt.Errorf("error listing pods in namespace %s: %v", namespace, err) 160 | } 161 | 162 | var targetedPods [][]string 163 | for _, pod := range pods.Items { 164 | targetedPods = append(targetedPods, []string{namespace, pod.Name, pod.Status.PodIP}) 165 | } 166 | 167 | return targetedPods, nil 168 | } 169 | 170 | // ListPodsTargetedByCiliumClusterWideNetworkPolicy lists all pods targeted by the given Cilium cluster wide network policy. 171 | func ListPodsTargetedByCiliumClusterWideNetworkPolicy(clientset *kubernetes.Clientset, dynamicClient dynamic.Interface, policy *unstructured.Unstructured) ([][]string, error) { 172 | // Retrieve the PodSelector (matchLabels) 173 | podSelector, found, err := unstructured.NestedMap(policy.Object, "spec", "endpointSelector", "matchLabels") 174 | if err != nil { 175 | return nil, fmt.Errorf("failed to retrieve pod selector from Cilium cluster wide network policy %s: %v", policy.GetName(), err) 176 | } 177 | 178 | // Regex for valid Kubernetes label keys 179 | validLabelKey := regexp.MustCompile(`^[A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9]$`) 180 | 181 | // Check if the selector is empty 182 | selector := labels.Set{} 183 | if found && len(podSelector) > 0 { 184 | for key, value := range podSelector { 185 | // Skip reserved labels 186 | if !validLabelKey.MatchString(key) { 187 | fmt.Printf("Skipping reserved label key %s in policy %s\n", key, policy.GetName()) 188 | continue 189 | } 190 | if strValue, ok := value.(string); ok { 191 | selector[key] = strValue 192 | } else { 193 | return nil, fmt.Errorf("invalid type for selector value %v in policy %s", value, policy.GetName()) 194 | } 195 | } 196 | } 197 | 198 | // Fetch pods based on the selector across all namespaces 199 | pods, err := clientset.CoreV1().Pods("").List(context.TODO(), v1.ListOptions{ 200 | LabelSelector: selector.AsSelector().String(), 201 | }) 202 | if err != nil { 203 | return nil, fmt.Errorf("error listing pods for cluster wide policy: %v", err) 204 | } 205 | 206 | var targetedPods [][]string 207 | for _, pod := range pods.Items { 208 | podDetails := []string{pod.Namespace, pod.Name, pod.Status.PodIP} 209 | targetedPods = append(targetedPods, podDetails) 210 | } 211 | 212 | return targetedPods, nil 213 | } -------------------------------------------------------------------------------- /backend/pkg/k8s/visualizer.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "gopkg.in/yaml.v2" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/client-go/kubernetes" 12 | ) 13 | 14 | // VisualizationData represents the structure of network policy and pod data for visualization. 15 | type VisualizationData struct { 16 | Policies []PolicyVisualization `json:"policies"` 17 | } 18 | 19 | // PolicyVisualization represents a network policy and the pods it affects for visualization purposes. 20 | type PolicyVisualization struct { 21 | Name string `json:"name"` 22 | Namespace string `json:"namespace"` 23 | TargetPods []string `json:"targetPods"` 24 | } 25 | 26 | // gatherVisualizationData retrieves network policies and associated pods for visualization. 27 | func gatherVisualizationData(clientset kubernetes.Interface, namespace string) (*VisualizationData, error) { 28 | // Retrieve all network policies in the specified namespace 29 | policies, err := clientset.NetworkingV1().NetworkPolicies(namespace).List(context.TODO(), metav1.ListOptions{}) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | vizData := &VisualizationData{ 35 | Policies: make([]PolicyVisualization, 0), // Initialize as empty slice 36 | } 37 | 38 | // Iterate over the retrieved policies to build the visualization data 39 | for _, policy := range policies.Items { 40 | // For each policy, find the pods that match its pod selector 41 | selector, err := metav1.LabelSelectorAsSelector(&policy.Spec.PodSelector) 42 | if err != nil { 43 | log.Printf("Error parsing selector for policy %s: %v\n", policy.Name, err) 44 | continue 45 | } 46 | 47 | pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{ 48 | LabelSelector: selector.String(), 49 | }) 50 | if err != nil { 51 | log.Printf("Error listing pods for policy %s: %v\n", policy.Name, err) 52 | continue 53 | } 54 | 55 | podNames := make([]string, 0, len(pods.Items)) 56 | for _, pod := range pods.Items { 57 | podNames = append(podNames, pod.Name) 58 | } 59 | 60 | vizData.Policies = append(vizData.Policies, PolicyVisualization{ 61 | Name: policy.Name, 62 | Namespace: policy.Namespace, 63 | TargetPods: podNames, 64 | }) 65 | } 66 | 67 | return vizData, nil 68 | } 69 | 70 | // gatherNamespacesWithPolicies returns a list of all namespaces that contain network policies. 71 | func GatherNamespacesWithPolicies(clientset kubernetes.Interface) ([]string, error) { 72 | // Retrieve all namespaces 73 | namespaces, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | var namespacesWithPolicies []string 79 | 80 | // Check each namespace for network policies 81 | for _, ns := range namespaces.Items { 82 | policies, err := clientset.NetworkingV1().NetworkPolicies(ns.Name).List(context.TODO(), metav1.ListOptions{}) 83 | if err != nil { 84 | log.Printf("Error listing policies in namespace %s: %v\n", ns.Name, err) 85 | continue 86 | } 87 | 88 | if len(policies.Items) > 0 { 89 | namespacesWithPolicies = append(namespacesWithPolicies, ns.Name) 90 | } 91 | } 92 | 93 | return namespacesWithPolicies, nil 94 | } 95 | 96 | // gatherClusterVisualizationData retrieves visualization data for all namespaces with network policies. 97 | func GatherClusterVisualizationData(clientset kubernetes.Interface) ([]VisualizationData, error) { 98 | namespacesWithPolicies, err := GatherNamespacesWithPolicies(clientset) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | // Slice to hold the visualization data for the entire cluster 104 | var clusterVizData []VisualizationData 105 | 106 | for _, namespace := range namespacesWithPolicies { 107 | vizData, err := gatherVisualizationData(clientset, namespace) 108 | if err != nil { 109 | log.Printf("Error gathering visualization data for namespace %s: %v\n", namespace, err) 110 | continue 111 | } 112 | clusterVizData = append(clusterVizData, *vizData) 113 | } 114 | 115 | return clusterVizData, nil 116 | } 117 | 118 | // getNetworkPolicyYAML retrieves the YAML representation of a network policy, excluding annotations. 119 | func getNetworkPolicyYAML(clientset kubernetes.Interface, namespace string, policyName string) (string, error) { 120 | // Get the specified network policy 121 | networkPolicy, err := clientset.NetworkingV1().NetworkPolicies(namespace).Get(context.TODO(), policyName, metav1.GetOptions{}) 122 | if err != nil { 123 | return "", err 124 | } 125 | 126 | // Convert the network policy to a map[string]interface{} for manipulation 127 | networkPolicyMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(networkPolicy) 128 | if err != nil { 129 | return "", err 130 | } 131 | 132 | // Remove the fields that are not needed 133 | unstructured.RemoveNestedField(networkPolicyMap, "metadata", "annotations") 134 | unstructured.RemoveNestedField(networkPolicyMap, "metadata", "generateName") 135 | unstructured.RemoveNestedField(networkPolicyMap, "metadata", "selfLink") 136 | unstructured.RemoveNestedField(networkPolicyMap, "metadata", "uid") 137 | unstructured.RemoveNestedField(networkPolicyMap, "metadata", "resourceVersion") 138 | unstructured.RemoveNestedField(networkPolicyMap, "metadata", "generation") 139 | unstructured.RemoveNestedField(networkPolicyMap, "metadata", "creationTimestamp") 140 | unstructured.RemoveNestedField(networkPolicyMap, "metadata", "deletionTimestamp") 141 | unstructured.RemoveNestedField(networkPolicyMap, "metadata", "deletionGracePeriodSeconds") 142 | unstructured.RemoveNestedField(networkPolicyMap, "metadata", "ownerReferences") 143 | unstructured.RemoveNestedField(networkPolicyMap, "metadata", "managedFields") 144 | 145 | // Convert the cleaned map back to YAML 146 | yamlBytes, err := yaml.Marshal(networkPolicyMap) 147 | if err != nil { 148 | return "", err 149 | } 150 | 151 | return string(yamlBytes), nil 152 | } 153 | -------------------------------------------------------------------------------- /backend/pkg/k8s/visualizer_test.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "gopkg.in/yaml.v2" 9 | corev1 "k8s.io/api/core/v1" 10 | netv1 "k8s.io/api/networking/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | "k8s.io/client-go/kubernetes" 14 | "k8s.io/client-go/kubernetes/fake" 15 | ) 16 | 17 | type Metadata struct { 18 | Name string `yaml:"name"` 19 | Namespace string `yaml:"namespace"` 20 | } 21 | 22 | type PodSelector struct { 23 | MatchLabels map[string]string `yaml:"matchLabels"` 24 | } 25 | 26 | type Policy struct { 27 | Metadata Metadata `yaml:"metadata"` 28 | Spec struct { 29 | PodSelector PodSelector `yaml:"podSelector"` 30 | } `yaml:"spec"` 31 | } 32 | 33 | func TestGatherVisualizationData(t *testing.T) { 34 | var clientset kubernetes.Interface = fake.NewSimpleClientset() 35 | 36 | // Creating a pod object and a networkPolicy object 37 | pod := &corev1.Pod{ 38 | ObjectMeta: metav1.ObjectMeta{ 39 | Name: "test-pod", 40 | Namespace: "test-namespace", 41 | Labels: map[string]string{ 42 | "app": "test", // Label on pod to match netpol selector 43 | }, 44 | }, 45 | } 46 | networkPolicy := &netv1.NetworkPolicy{ 47 | ObjectMeta: metav1.ObjectMeta{ 48 | Name: "test-policy", 49 | Namespace: "test-namespace", 50 | }, 51 | Spec: netv1.NetworkPolicySpec{ 52 | PodSelector: metav1.LabelSelector{ 53 | MatchLabels: map[string]string{"app": "test"}, 54 | }, 55 | }, 56 | } 57 | _, err := clientset.CoreV1().Pods("test-namespace").Create(context.TODO(), pod, metav1.CreateOptions{}) 58 | if err != nil { 59 | t.Fatalf("Failed to create pod: %v", err) 60 | } 61 | _, err = clientset.NetworkingV1().NetworkPolicies("test-namespace").Create(context.TODO(), networkPolicy, metav1.CreateOptions{}) 62 | if err != nil { 63 | t.Fatalf("Failed to create network policy: %v", err) 64 | } 65 | 66 | visualizationData, err := gatherVisualizationData(clientset, "test-namespace") 67 | if err != nil { 68 | t.Fatalf("Error occurred while gathering visualization data: %v", err) 69 | } 70 | 71 | expected := &VisualizationData{ 72 | Policies: []PolicyVisualization{ 73 | { 74 | Name: "test-policy", 75 | Namespace: "test-namespace", 76 | TargetPods: []string{"test-pod"}, 77 | }, 78 | }, 79 | } 80 | 81 | assert.Equal(t, expected, visualizationData, "they should be equal") 82 | } 83 | 84 | func TestGetNetworkPolicyYAML(t *testing.T) { 85 | var clientset kubernetes.Interface = fake.NewSimpleClientset() 86 | 87 | var ns string = "test-namespace" 88 | 89 | // Creating a pod object and a networkPolicy object 90 | pod := &corev1.Pod{ 91 | ObjectMeta: metav1.ObjectMeta{ 92 | Name: "test-pod", 93 | Namespace: ns, 94 | Labels: map[string]string{ 95 | "app": "test", // Label on pod to match netpol selector 96 | }, 97 | }, 98 | } 99 | _, err := clientset.CoreV1().Pods(ns).Create(context.TODO(), pod, metav1.CreateOptions{}) 100 | if err != nil { 101 | t.Fatalf("Failed to create pod: %v", err) 102 | } 103 | 104 | testPolicy := &netv1.NetworkPolicy{ 105 | ObjectMeta: metav1.ObjectMeta{ 106 | Name: "test-policy", 107 | Namespace: "test-namespace", 108 | }, 109 | Spec: netv1.NetworkPolicySpec{ 110 | PodSelector: metav1.LabelSelector{ 111 | MatchLabels: map[string]string{"app": "test"}, 112 | }, 113 | }, 114 | } 115 | _, err1 := clientset.NetworkingV1().NetworkPolicies(ns).Create(context.TODO(), testPolicy, metav1.CreateOptions{}) 116 | if err1 != nil { 117 | t.Fatalf("Failed to create test network policy: %v", err1) 118 | } 119 | 120 | // Call the getNetworkPolicyYAML function with the fake clientset 121 | YAML, err := getNetworkPolicyYAML(clientset, "test-namespace", "test-policy") 122 | if err != nil { 123 | t.Fatalf("Failed to get network policy YAML: %v", err) 124 | } 125 | 126 | // Policy YAML string 127 | // expectedYAML := ` 128 | // metadata: 129 | // name: test-policy 130 | // namespace: test-namespace 131 | // spec: 132 | // podSelector: 133 | // matchLabels: 134 | // app: test 135 | // ` 136 | 137 | // Below the testPolicy which is expected is converted into Policy struct (map to bytes array to struct) and 138 | // compared with actualPolicy which is also converted to Policy struct from string. 139 | expectedNetworkPolicyMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(testPolicy) 140 | if err != nil { 141 | t.Fatalf("error: %v", err) 142 | } 143 | 144 | expectedPolicyBytes, err := yaml.Marshal(expectedNetworkPolicyMap) 145 | if err != nil { 146 | t.Fatalf("error: %v", err) 147 | } 148 | 149 | var expectedPolicy Policy 150 | err2 := yaml.Unmarshal(expectedPolicyBytes, &expectedPolicy) 151 | if err2 != nil { 152 | t.Fatalf("error: %v", err2) 153 | } 154 | 155 | var actualPolicy Policy 156 | err3 := yaml.Unmarshal([]byte(YAML), &actualPolicy) 157 | if err3 != nil { 158 | t.Fatalf("error: %v", err3) 159 | } 160 | 161 | assert.Equal(t, expectedPolicy, actualPolicy, "they should be equal") 162 | } 163 | 164 | func TestGatherNamespacesWithPolicies(t *testing.T) { 165 | // Define namespaces both with and without network policies 166 | namespaces := []string{"test-namespace1", "test-namespace2", "test-namespace3", "test-namespace4"} 167 | namespacesWithPolicies := []string{"test-namespace3", "test-namespace4"} 168 | 169 | var clientset kubernetes.Interface = fake.NewSimpleClientset() 170 | 171 | for _, ns := range namespaces { 172 | // Create a namespace 173 | namespace := &corev1.Namespace{ 174 | ObjectMeta: metav1.ObjectMeta{ 175 | Name: ns, 176 | }, 177 | } 178 | 179 | _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), namespace, metav1.CreateOptions{}) 180 | if err != nil { 181 | t.Fatalf("Failed to create namespace %s: %v", ns, err) 182 | } 183 | } 184 | 185 | for _, ns := range namespacesWithPolicies { 186 | // Create a pod in the namespace 187 | pod := &corev1.Pod{ 188 | ObjectMeta: metav1.ObjectMeta{ 189 | Name: "test-pod", 190 | Namespace: ns, 191 | Labels: map[string]string{ 192 | "app": "test", 193 | }, 194 | }, 195 | } 196 | _, err := clientset.CoreV1().Pods(ns).Create(context.TODO(), pod, metav1.CreateOptions{}) 197 | if err != nil { 198 | t.Fatalf("Failed to create pod: %v", err) 199 | } 200 | 201 | // Create a networkPolicy in the namespace 202 | networkPolicy := &netv1.NetworkPolicy{ 203 | ObjectMeta: metav1.ObjectMeta{ 204 | Name: "test-policy", 205 | Namespace: ns, 206 | }, 207 | Spec: netv1.NetworkPolicySpec{ 208 | PodSelector: metav1.LabelSelector{ 209 | MatchLabels: map[string]string{"app": "test"}, 210 | }, 211 | }, 212 | } 213 | _, err1 := clientset.NetworkingV1().NetworkPolicies(ns).Create(context.TODO(), networkPolicy, metav1.CreateOptions{}) 214 | if err1 != nil { 215 | t.Fatalf("Failed to create network policy in namespace %s: %v", ns, err1) 216 | } 217 | } 218 | 219 | gatheredNamespaces, err := GatherNamespacesWithPolicies(clientset) 220 | if err != nil { 221 | t.Fatalf("Error calling GatherNamespacesWithPolicies: %v", err) 222 | } 223 | 224 | assert.Equal(t, namespacesWithPolicies, gatheredNamespaces, "they should be equal") 225 | } 226 | 227 | func TestGatherClusterVisualizationData(t *testing.T) { 228 | namespacesWithPolicies := []string{"namespace1", "namespace2"} 229 | expectedVisualizationData := make([]VisualizationData, len(namespacesWithPolicies)) 230 | 231 | var clientset kubernetes.Interface = fake.NewSimpleClientset() 232 | 233 | for i, ns := range namespacesWithPolicies { 234 | // Create a namespace 235 | namespace := &corev1.Namespace{ 236 | ObjectMeta: metav1.ObjectMeta{ 237 | Name: ns, 238 | }, 239 | } 240 | _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), namespace, metav1.CreateOptions{}) 241 | if err != nil { 242 | t.Fatalf("Failed to create namespace %s: %v", ns, err) 243 | } 244 | 245 | // Create a pod in the namespace 246 | pod := &corev1.Pod{ 247 | ObjectMeta: metav1.ObjectMeta{ 248 | Name: "test-pod", 249 | Namespace: ns, 250 | Labels: map[string]string{ 251 | "app": "test", 252 | }, 253 | }, 254 | } 255 | _, err1 := clientset.CoreV1().Pods(ns).Create(context.TODO(), pod, metav1.CreateOptions{}) 256 | if err1 != nil { 257 | t.Fatalf("Failed to create pod: %v", err1) 258 | } 259 | 260 | // Create a network policy in the namespace 261 | networkPolicy := &netv1.NetworkPolicy{ 262 | ObjectMeta: metav1.ObjectMeta{ 263 | Name: "test-policy", 264 | Namespace: ns, 265 | }, 266 | Spec: netv1.NetworkPolicySpec{ 267 | PodSelector: metav1.LabelSelector{ 268 | MatchLabels: map[string]string{"app": "test"}, 269 | }, 270 | }, 271 | } 272 | _, err2 := clientset.NetworkingV1().NetworkPolicies(ns).Create(context.TODO(), networkPolicy, metav1.CreateOptions{}) 273 | if err2 != nil { 274 | t.Fatalf("Failed to create network policy in namespace %s: %v", ns, err2) 275 | } 276 | 277 | vizData := &VisualizationData{ 278 | Policies: []PolicyVisualization{ 279 | { 280 | Name: networkPolicy.Name, 281 | Namespace: ns, 282 | TargetPods: []string{"test-pod"}, 283 | }, 284 | }, 285 | } 286 | expectedVisualizationData[i] = *vizData 287 | } 288 | 289 | gatheredVisualizationData, err := GatherClusterVisualizationData(clientset) 290 | if err != nil { 291 | t.Fatalf("Error calling GatherClusterVisualizationData: %v", err) 292 | } 293 | 294 | assert.Equal(t, expectedVisualizationData, gatheredVisualizationData) 295 | } -------------------------------------------------------------------------------- /charts/Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build the Vue.js frontend 2 | FROM node:21 as nodebuilder 3 | 4 | WORKDIR /app/frontend 5 | 6 | # Copy the frontend code - make sure this path matches the location of your Vue.js files in the repo 7 | COPY frontend/dash /app/frontend 8 | 9 | # Install dependencies and build the frontend 10 | RUN npm install 11 | RUN npm run build 12 | 13 | # Stage 2: Build the Go backend and bundle the frontend 14 | FROM golang:1.21 as gobuilder 15 | 16 | WORKDIR /app/backend 17 | 18 | # Copy the backend code 19 | COPY backend /app/backend 20 | 21 | # Install statik 22 | RUN go install github.com/rakyll/statik@latest 23 | 24 | # Bundle the frontend with the backend 25 | COPY --from=nodebuilder /app/frontend/dist /app/frontend/dist 26 | RUN statik -src=/app/frontend/dist 27 | RUN go build -o netfetch 28 | 29 | # Stage 3: Final image 30 | FROM debian:latest 31 | 32 | WORKDIR /usr/local/bin/ 33 | 34 | # Copy the compiled Go binary 35 | COPY --from=gobuilder /app/backend/netfetch /usr/local/bin/ 36 | 37 | # Expose the port the app runs on 38 | EXPOSE 8080 39 | 40 | # Run the binary 41 | CMD ["netfetch", "dash"] 42 | -------------------------------------------------------------------------------- /charts/netfetch/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/netfetch/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: netfetch 3 | description: A Helm chart enabling netfetch to run the netfetch dashboard in a Kubernetes deployment. 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.5.3 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "0.5.3" 25 | -------------------------------------------------------------------------------- /charts/netfetch/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "netfetch.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "netfetch.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "netfetch.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "netfetch.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://localhost:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /charts/netfetch/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "netfetch.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "netfetch.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "netfetch.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "netfetch.labels" -}} 37 | helm.sh/chart: {{ include "netfetch.chart" . }} 38 | {{ include "netfetch.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "netfetch.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "netfetch.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "netfetch.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "netfetch.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /charts/netfetch/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create .Values.rbac.clusterWideAccess }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "netfetch.fullname" . }}-clusterrole 6 | rules: 7 | # Rules for core resources 8 | - apiGroups: [""] 9 | resources: ["pods", "namespaces"] 10 | verbs: ["get", "list", "watch", "create"] 11 | 12 | # Rules for NetworkPolicies in the networking.k8s.io API group 13 | - apiGroups: ["networking.k8s.io"] 14 | resources: ["networkpolicies"] 15 | verbs: ["get", "list", "watch", "create"] 16 | {{- end }} 17 | -------------------------------------------------------------------------------- /charts/netfetch/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create .Values.rbac.clusterWideAccess }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: {{ include "netfetch.fullname" . }}-clusterrolebinding 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: ClusterRole 9 | name: {{ include "netfetch.fullname" . }}-clusterrole 10 | subjects: 11 | - kind: ServiceAccount 12 | name: {{ include "netfetch.serviceAccountName" . }} 13 | namespace: {{ .Release.Namespace }} 14 | {{- end }} 15 | -------------------------------------------------------------------------------- /charts/netfetch/templates/clusterwide-networkpolicy.yaml: -------------------------------------------------------------------------------- 1 | # templates/clusterwide-networkpolicy.yaml 2 | {{- if and .Values.rbac.create .Values.rbac.clusterWideAccess }} 3 | apiVersion: networking.k8s.io/v1 4 | kind: NetworkPolicy 5 | metadata: 6 | name: {{ include "netfetch.fullname" . }}-clusterwide-netpol 7 | namespace: {{ .Release.Namespace }} 8 | spec: 9 | podSelector: 10 | matchLabels: 11 | {{- include "netfetch.selectorLabels" . | nindent 6 }} 12 | policyTypes: 13 | - Ingress 14 | - Egress 15 | ingress: 16 | - {} # Allow all ingress traffic 17 | egress: 18 | - {} # Allow all egress traffic 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /charts/netfetch/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "netfetch.fullname" . }} 5 | labels: 6 | {{- include "netfetch.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "netfetch.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "netfetch.selectorLabels" . | nindent 8 }} 22 | spec: 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | serviceAccountName: {{ include "netfetch.serviceAccountName" . }} 28 | securityContext: 29 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 30 | containers: 31 | - name: {{ .Chart.Name }} 32 | securityContext: 33 | {{- toYaml .Values.securityContext | nindent 12 }} 34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 35 | imagePullPolicy: {{ .Values.image.pullPolicy }} 36 | command: ["netfetch"] 37 | args: ["dash"] 38 | ports: 39 | - name: http 40 | containerPort: 8080 41 | protocol: TCP 42 | # livenessProbe: 43 | # httpGet: 44 | # path: / 45 | # port: 8080 46 | # readinessProbe: 47 | # httpGet: 48 | # path: / 49 | # port: 8080 50 | resources: 51 | {{- toYaml .Values.resources | nindent 12 }} 52 | {{- with .Values.nodeSelector }} 53 | nodeSelector: 54 | {{- toYaml . | nindent 8 }} 55 | {{- end }} 56 | {{- with .Values.affinity }} 57 | affinity: 58 | {{- toYaml . | nindent 8 }} 59 | {{- end }} 60 | {{- with .Values.tolerations }} 61 | tolerations: 62 | {{- toYaml . | nindent 8 }} 63 | {{- end }} 64 | -------------------------------------------------------------------------------- /charts/netfetch/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "netfetch.fullname" . }} 6 | labels: 7 | {{- include "netfetch.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "netfetch.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /charts/netfetch/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "netfetch.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "netfetch.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /charts/netfetch/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "netfetch.fullname" . }} 5 | labels: 6 | {{- include "netfetch.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "netfetch.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /charts/netfetch/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "netfetch.serviceAccountName" . }} 6 | labels: 7 | {{- include "netfetch.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /charts/netfetch/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "netfetch.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "netfetch.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "netfetch.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /charts/netfetch/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for netfetch. 2 | replicaCount: 1 3 | 4 | image: 5 | repository: deggja/netfetch 6 | pullPolicy: Always 7 | tag: "latest" 8 | 9 | imagePullSecrets: [] 10 | nameOverride: "" 11 | fullnameOverride: "" 12 | 13 | serviceAccount: 14 | create: true 15 | annotations: {} 16 | name: "" 17 | 18 | podAnnotations: {} 19 | 20 | podSecurityContext: {} 21 | # fsGroup: 2000 22 | 23 | securityContext: {} 24 | # capabilities: 25 | # drop: 26 | # - ALL 27 | # readOnlyRootFilesystem: true 28 | # runAsNonRoot: true 29 | # runAsUser: 1000 30 | # allowPrivilegeEscalation: false 31 | # privileged: false 32 | 33 | service: 34 | type: ClusterIP 35 | port: 8080 36 | 37 | ingress: 38 | enabled: false 39 | className: "nginx" 40 | annotations: 41 | # cert-manager.io/cluster-issuer: "letsencrypt-prod" 42 | # kubernetes.io/tls-acme: "true" 43 | # nginx.ingress.kubernetes.io/rewrite-target: / 44 | hosts: 45 | - host: netfetch.example.com # Update this to reflect your domain 46 | paths: 47 | - path: / 48 | pathType: ImplementationSpecific 49 | tls: 50 | - hosts: 51 | - netfetch.example.com 52 | secretName: netfetch-tls 53 | 54 | resources: {} 55 | 56 | autoscaling: 57 | enabled: false 58 | minReplicas: 1 59 | maxReplicas: 100 60 | targetCPUUtilizationPercentage: 80 61 | 62 | nodeSelector: {} 63 | 64 | tolerations: [] 65 | 66 | affinity: {} 67 | 68 | rbac: 69 | create: true 70 | clusterWideAccess: true -------------------------------------------------------------------------------- /formula/netfetch.rb: -------------------------------------------------------------------------------- 1 | class Netfetch < Formula 2 | desc "CLI tool to scan for network policies in Kubernetes clusters/namespaces and provide a score based on the amount of untargeted workloads" 3 | homepage "https://github.com/deggja/netfetch" 4 | 5 | if OS.mac? 6 | url "https://github.com/deggja/netfetch/releases/download/v0.5.4/netfetch_0.5.4_darwin_amd64.tar.gz" 7 | sha256 "ada24b740c746bdf14e67c2153dbc02440462aa765e7ac31b87439aff845d48c" 8 | elsif OS.linux? 9 | url "https://github.com/deggja/netfetch/releases/download/v0.5.4/netfetch_0.5.4_linux_amd64.tar.gz" 10 | sha256 "8757efca2f1196777acc45299773da105d8ce40a260e5de8d9f72d942f6f896b" 11 | end 12 | 13 | def install 14 | bin.install "netfetch" 15 | end 16 | 17 | test do 18 | system "#{bin}/netfetch", "version" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /frontend/dash/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /frontend/dash/README.md: -------------------------------------------------------------------------------- 1 | # dash 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /frontend/dash/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /frontend/dash/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "baseUrl": "./", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "@/*": [ 9 | "src/*" 10 | ] 11 | }, 12 | "lib": [ 13 | "esnext", 14 | "dom", 15 | "dom.iterable", 16 | "scripthost" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/dash/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dash", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^1.6.2", 12 | "core-js": "^3.8.3", 13 | "d3": "^7.8.5", 14 | "js-yaml": "^4.1.0", 15 | "vue": "^3.2.13" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.12.16", 19 | "@babel/eslint-parser": "^7.12.16", 20 | "@types/d3": "^7.4.3", 21 | "@vue/cli-plugin-babel": "~5.0.0", 22 | "@vue/cli-plugin-eslint": "~5.0.0", 23 | "@vue/cli-service": "~5.0.0", 24 | "eslint": "^7.32.0", 25 | "eslint-plugin-vue": "^8.0.3" 26 | }, 27 | "eslintConfig": { 28 | "root": true, 29 | "env": { 30 | "node": true 31 | }, 32 | "extends": [ 33 | "plugin:vue/vue3-essential", 34 | "eslint:recommended" 35 | ], 36 | "parserOptions": { 37 | "parser": "@babel/eslint-parser" 38 | }, 39 | "rules": {} 40 | }, 41 | "browserslist": [ 42 | "> 1%", 43 | "last 2 versions", 44 | "not dead", 45 | "not ie 11" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /frontend/dash/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deggja/netfetch/ba0a8d616aeeaa675e3dc82d570dc8bec48dc683/frontend/dash/public/favicon.ico -------------------------------------------------------------------------------- /frontend/dash/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Netfetch Dashboard 8 | 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/dash/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deggja/netfetch/ba0a8d616aeeaa675e3dc82d570dc8bec48dc683/frontend/dash/public/logo.png -------------------------------------------------------------------------------- /frontend/dash/src/Viz.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 636 | 637 | 711 | -------------------------------------------------------------------------------- /frontend/dash/src/assets/logo_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deggja/netfetch/ba0a8d616aeeaa675e3dc82d570dc8bec48dc683/frontend/dash/src/assets/logo_blue.png -------------------------------------------------------------------------------- /frontend/dash/src/assets/new-clustermap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deggja/netfetch/ba0a8d616aeeaa675e3dc82d570dc8bec48dc683/frontend/dash/src/assets/new-clustermap.png -------------------------------------------------------------------------------- /frontend/dash/src/assets/new-dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deggja/netfetch/ba0a8d616aeeaa675e3dc82d570dc8bec48dc683/frontend/dash/src/assets/new-dash.png -------------------------------------------------------------------------------- /frontend/dash/src/assets/new-ns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deggja/netfetch/ba0a8d616aeeaa675e3dc82d570dc8bec48dc683/frontend/dash/src/assets/new-ns.png -------------------------------------------------------------------------------- /frontend/dash/src/assets/new-suggestpolicy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deggja/netfetch/ba0a8d616aeeaa675e3dc82d570dc8bec48dc683/frontend/dash/src/assets/new-suggestpolicy.png -------------------------------------------------------------------------------- /frontend/dash/src/assets/small-demo.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deggja/netfetch/ba0a8d616aeeaa675e3dc82d570dc8bec48dc683/frontend/dash/src/assets/small-demo.mov -------------------------------------------------------------------------------- /frontend/dash/src/assets/theme.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deggja/netfetch/ba0a8d616aeeaa675e3dc82d570dc8bec48dc683/frontend/dash/src/assets/theme.css -------------------------------------------------------------------------------- /frontend/dash/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 41 | 42 | 43 | 59 | -------------------------------------------------------------------------------- /frontend/dash/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import './assets/theme.css'; 4 | 5 | createApp(App).mount('#app') -------------------------------------------------------------------------------- /frontend/dash/vue.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('@vue/cli-service') 2 | module.exports = defineConfig({ 3 | transpileDependencies: true 4 | }) 5 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.21 2 | 3 | use ./backend 4 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 2 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 3 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 4 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 5 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 6 | github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 7 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 8 | github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 9 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 10 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 11 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 12 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 13 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 14 | k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= 15 | -------------------------------------------------------------------------------- /index.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | entries: 3 | netfetch: 4 | - apiVersion: v2 5 | appVersion: 0.5.3 6 | created: "2024-11-09T13:37:27.102997+01:00" 7 | description: A Helm chart enabling netfetch to run the netfetch dashboard in a 8 | Kubernetes deployment. 9 | digest: 6a65043e2ff880e5078c644fc8fd29535a7613c3c4a1a32dc00635f8a968979f 10 | name: netfetch 11 | type: application 12 | urls: 13 | - https://deggja.github.io/netfetch/netfetch-0.5.3.tgz 14 | version: 0.5.3 15 | generated: "2024-11-09T13:37:27.100022+01:00" 16 | -------------------------------------------------------------------------------- /netfetch-0.5.3.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deggja/netfetch/ba0a8d616aeeaa675e3dc82d570dc8bec48dc683/netfetch-0.5.3.tgz -------------------------------------------------------------------------------- /tests/cnp.yml: -------------------------------------------------------------------------------- 1 | apiVersion: "cilium.io/v2" 2 | kind: CiliumClusterwideNetworkPolicy 3 | metadata: 4 | name: "clusterwide-policy-example" 5 | spec: 6 | description: "Policy for selective ingress allow to a pod from only a pod with given label" 7 | endpointSelector: 8 | matchLabels: 9 | name: leia 10 | ingress: 11 | - fromEndpoints: 12 | - matchLabels: 13 | name: luke 14 | --- 15 | apiVersion: "cilium.io/v2" 16 | kind: CiliumClusterwideNetworkPolicy 17 | metadata: 18 | name: "wildcard-from-endpoints" 19 | spec: 20 | description: "Policy for ingress allow to kube-dns from all Cilium managed endpoints in the cluster" 21 | endpointSelector: 22 | matchLabels: 23 | k8s:io.kubernetes.pod.namespace: kube-system 24 | k8s-app: kube-dns 25 | ingress: 26 | - fromEndpoints: 27 | - {} 28 | toPorts: 29 | - ports: 30 | - port: "53" 31 | protocol: UDP 32 | --- 33 | apiVersion: "cilium.io/v2" 34 | kind: CiliumClusterwideNetworkPolicy 35 | metadata: 36 | name: "cilium-health-checks" 37 | spec: 38 | endpointSelector: 39 | matchLabels: 40 | 'reserved:health': '' 41 | ingress: 42 | - fromEntities: 43 | - remote-node 44 | egress: 45 | - toEntities: 46 | - remote-node 47 | --- 48 | apiVersion: "cilium.io/v2" 49 | kind: CiliumClusterwideNetworkPolicy 50 | metadata: 51 | name: "deny-all-grafana" 52 | spec: 53 | endpointSelector: 54 | matchLabels: 55 | io.kubernetes.pod.namespace: grafana 56 | ingress: 57 | - {} # Deny all incoming traffic 58 | egress: 59 | - {} # Deny all outgoing traffic 60 | --- 61 | apiVersion: "cilium.io/v2" 62 | kind: CiliumClusterwideNetworkPolicy 63 | metadata: 64 | name: "deny-all-crossplane-system" 65 | spec: 66 | endpointSelector: 67 | matchLabels: 68 | io.kubernetes.pod.namespace: crossplane-system 69 | ingress: 70 | - {} # Deny all incoming traffic 71 | egress: 72 | - {} # Deny all outgoing traffic 73 | -------------------------------------------------------------------------------- /tests/cnp2.yml: -------------------------------------------------------------------------------- 1 | apiVersion: cilium.io/v2 2 | kind: CiliumClusterwideNetworkPolicy 3 | metadata: 4 | name: deny-crossplane-provider-kubernetes 5 | spec: 6 | endpointSelector: 7 | matchLabels: 8 | pkg.crossplane.io/provider: provider-kubernetes 9 | pkg.crossplane.io/revision: provider-kubernetes-fd7ab5be249e 10 | ingress: 11 | - {} # Empty ingress means no traffic is allowed in. 12 | egress: 13 | - {} # Empty egress means no traffic is allowed out. 14 | --- 15 | apiVersion: cilium.io/v2 16 | kind: CiliumClusterwideNetworkPolicy 17 | metadata: 18 | name: grafana-network-policy 19 | spec: 20 | endpointSelector: 21 | matchLabels: 22 | app.kubernetes.io/name: grafana 23 | ingress: 24 | - {} 25 | egress: 26 | - {} 27 | -------------------------------------------------------------------------------- /tests/cnp3.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "cilium.io/v2" 3 | kind: CiliumClusterwideNetworkPolicy 4 | metadata: 5 | name: "cluster-default-kube-dns-ingress" 6 | spec: 7 | description: "Policy for ingress allow to kube-dns from all Cilium managed endpoints in the cluster" 8 | endpointSelector: 9 | matchLabels: 10 | k8s:io.kubernetes.pod.namespace: kube-system 11 | k8s-app: kube-dns 12 | ingress: 13 | - fromEndpoints: 14 | - {} 15 | toPorts: 16 | - ports: 17 | - port: "53" 18 | protocol: "ANY" 19 | --- 20 | apiVersion: "cilium.io/v2" 21 | kind: CiliumClusterwideNetworkPolicy 22 | metadata: 23 | name: "cluster-default-kube-dns-egress" 24 | spec: 25 | description: "Policy for egress allow to kube-dns from all Cilium managed endpoints in the cluster" 26 | endpointSelector: {} 27 | egress: 28 | - toEndpoints: 29 | - matchLabels: 30 | "k8s:io.kubernetes.pod.namespace": kube-system 31 | "k8s:k8s-app": kube-dns 32 | toPorts: 33 | - ports: 34 | - port: "53" 35 | protocol: "ANY" 36 | rules: 37 | dns: 38 | - matchPattern: "*" --------------------------------------------------------------------------------