mirror of
https://github.com/BishopFox/cirro.git
synced 2026-11-09 17:07:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d0f7532aa | ||
|
|
229f16c11c | ||
|
|
e4dbdb8461 | ||
|
|
cb298b7751 | ||
|
|
6f190b187e | ||
|
|
ae93706be7 | ||
|
|
7ad8d94596 | ||
|
|
9300fc2d6f | ||
|
|
30242d25d6 | ||
|
|
b43fd05c53 | ||
|
|
e6337a796f | ||
|
|
3b53d223af | ||
|
|
764aea00cb | ||
|
|
64f09986ad | ||
|
|
754a21ad3a | ||
|
|
8301c64176 | ||
|
|
d0f5723493 | ||
|
|
36f12788a9 | ||
|
|
6f08291c70 | ||
|
|
73839f1e98 | ||
|
|
bf862723d6 | ||
|
|
cdc8795d88 | ||
|
|
eac3b286e0 | ||
|
|
8ace4163e2 | ||
|
|
28f785fced | ||
|
|
bc1c617c56 | ||
|
|
d0ea34d71c | ||
|
|
19ea3a1ac2 | ||
|
|
7364321e93 | ||
|
|
b1826298de | ||
|
|
b99fa73eed | ||
|
|
af36fd9141 | ||
|
|
7eabae58f7 | ||
|
|
6ec3b0359c | ||
|
|
c26cd4c972 | ||
|
|
7fd63ad662 | ||
|
|
bce94268a7 | ||
|
|
75d124a6a3 | ||
|
|
5c228a5701 | ||
|
|
0ed7fd04c4 | ||
|
|
118b91685d | ||
|
|
ad3d8f16d0 | ||
|
|
a07d3e1387 | ||
|
|
f91e667746 | ||
|
|
e6adb1784a | ||
|
|
ff302f3970 | ||
|
|
473b7ce739 |
No files matched your search
@@ -0,0 +1 @@
|
||||
* @BishopFox/bf-red-team
|
||||
@@ -15,7 +15,7 @@ Primary sources of truth:
|
||||
|
||||
Primary documentation targets:
|
||||
- `docs/analysis/**`
|
||||
- `mkdocs.yml` nav entries
|
||||
- `zensical.toml` nav entries
|
||||
|
||||
## When To Use
|
||||
|
||||
@@ -37,7 +37,7 @@ Trigger examples:
|
||||
- `docs/analysis/tailscale-nodes/*.md`
|
||||
- `docs/analysis/edges/*.md`
|
||||
- `docs/analysis/post-processing/*.md`
|
||||
- Also inspect the `nav:` section in `mkdocs.yml`.
|
||||
- Also inspect the `nav:` section in `zensical.toml`.
|
||||
|
||||
3. Scan config YAML.
|
||||
- `src/graph/config/azure/arm/**/*.tera.yaml`
|
||||
@@ -50,13 +50,13 @@ Trigger examples:
|
||||
- Missing node docs from `LABELS.*`
|
||||
- Missing edge docs from `REL.*` (convert `UPPER_SNAKE` to `lower-kebab`)
|
||||
- Missing post-processing docs from files in `src/graph/config/post_processing/`
|
||||
- Missing `mkdocs.yml` nav entries for all docs under `docs/analysis/`
|
||||
- Missing `zensical.toml` nav entries for all docs under `docs/analysis/`
|
||||
|
||||
5. Create or update docs.
|
||||
- Prefer minimal edits and keep style consistent with nearby files.
|
||||
- Create missing directories before creating files.
|
||||
|
||||
6. Update `mkdocs.yml` nav.
|
||||
6. Update `zensical.toml` nav.
|
||||
- Place docs in the correct analysis sections.
|
||||
- Keep entries alphabetically sorted within each section.
|
||||
- Preserve existing indentation and structure.
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
name: docs-deploy
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "mkdocs.yml"
|
||||
- ".github/workflows/docs-deploy.yml"
|
||||
branches:
|
||||
- main
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Configure Git Credentials
|
||||
run: |
|
||||
git config user.name github-actions[bot]
|
||||
git config user.email github-actions[bot]@users.noreply.github.com
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
key: mkdocs-material-${{ env.cache_id }}
|
||||
path: ~/.cache
|
||||
restore-keys: |
|
||||
mkdocs-material-
|
||||
- run: pip install mkdocs-material
|
||||
- run: mkdocs gh-deploy --force
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Documentation
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "zensical.toml"
|
||||
- ".github/workflows/docs.yml"
|
||||
branches:
|
||||
- main
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
jobs:
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/configure-pages@v6
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: pip install zensical
|
||||
- run: zensical build --clean
|
||||
- uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: site
|
||||
- uses: actions/deploy-pages@v5
|
||||
id: deployment
|
||||
+1
-1
@@ -9,7 +9,7 @@ target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
# Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
Vendored
+2
-1
@@ -13,5 +13,6 @@
|
||||
],
|
||||
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||
"python-envs.pythonProjects": [],
|
||||
"rust-analyzer.cargo.target": "x86_64-pc-windows-gnu"
|
||||
"rust-analyzer.cargo.target": "x86_64-pc-windows-gnu",
|
||||
"git.addAICoAuthor": "off"
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.4.0] - 2026-11-05
|
||||
|
||||
### Added
|
||||
|
||||
- Restricted guest Microsoft Graph collection through service principal relationships with `--restricted-guest`.
|
||||
- Custom application ID seeds with `--restricted-appids` and an optional Microsoft application catalog with `--restricted-guest-msapps`.
|
||||
|
||||
## [1.3.0] - 2026-09-22
|
||||
|
||||
### Added
|
||||
|
||||
- Add user manager and password change datetime by @daddycocoaman in [#36](https://github.com/bishopfox/cirro/pull/36)
|
||||
- Add odata types and agent id relations by @daddycocoaman in [#35](https://github.com/bishopfox/cirro/pull/35)
|
||||
|
||||
### Changed
|
||||
|
||||
- Process createdByAppId by @daddycocoaman in [#34](https://github.com/bishopfox/cirro/pull/34)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Use correct base64 decoding for tokens by @daddycocoaman in [#38](https://github.com/bishopfox/cirro/pull/38)
|
||||
- Check site serverfarmid for null by @daddycocoaman in [#37](https://github.com/bishopfox/cirro/pull/37)
|
||||
|
||||
## [1.2.1] - 2026-06-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix merges in loadbalancer by @daddycocoaman in [#33](https://github.com/bishopfox/cirro/pull/33)
|
||||
|
||||
## [1.2.0] - 2026-05-12
|
||||
|
||||
### Added
|
||||
|
||||
- Add network LoadBalancer nodes and VirtualHub by @daddycocoaman in [#30](https://github.com/bishopfox/cirro/pull/30)
|
||||
- Add subject for FIC collection by @daddycocoaman in [#28](https://github.com/bishopfox/cirro/pull/28)
|
||||
- Add KustoCluster node by @daddycocoaman in [#25](https://github.com/bishopfox/cirro/pull/25)
|
||||
- Add SyntexDocumentProcessor by @daddycocoaman in [#24](https://github.com/bishopfox/cirro/pull/24)
|
||||
- Add azure stack nodes and edges by @daddycocoaman in [#23](https://github.com/bishopfox/cirro/pull/23)
|
||||
- Add oracle autonomous db by @daddycocoaman in [#22](https://github.com/bishopfox/cirro/pull/22)
|
||||
- Add multiple Hybrid nodes and parity with virtual machines by @daddycocoaman in [#21](https://github.com/bishopfox/cirro/pull/21)
|
||||
- Add ManagedHSM by @daddycocoaman in [#15](https://github.com/bishopfox/cirro/pull/15)
|
||||
- Add AnalysisServiceServer and AnalysisServiceFirewallRule by @daddycocoaman in [#14](https://github.com/bishopfox/cirro/pull/14)
|
||||
- Add composite index and constraints syntax by @daddycocoaman in [#13](https://github.com/bishopfox/cirro/pull/13)
|
||||
- Add GithubNetworkSettings by @daddycocoaman in [#12](https://github.com/bishopfox/cirro/pull/12)
|
||||
- Add dry run switch for ingest by @daddycocoaman in [#11](https://github.com/bishopfox/cirro/pull/11)
|
||||
- Add argument validation to applicable arguments by @daddycocoaman in [#10](https://github.com/bishopfox/cirro/pull/10)
|
||||
|
||||
### Changed
|
||||
|
||||
- Update docs by @daddycocoaman in [#31](https://github.com/bishopfox/cirro/pull/31)
|
||||
- Update cypher to Cypher 25 syntax by @daddycocoaman in [#27](https://github.com/bishopfox/cirro/pull/27)
|
||||
- Update netapp nodes by @daddycocoaman in [#26](https://github.com/bishopfox/cirro/pull/26)
|
||||
- Update docs skill for zensical by @daddycocoaman in [#20](https://github.com/bishopfox/cirro/pull/20)
|
||||
- Migrate docs to zensical by @daddycocoaman in [#19](https://github.com/bishopfox/cirro/pull/19)
|
||||
- Turn off git AI CoAuthor setting by @daddycocoaman in [#16](https://github.com/bishopfox/cirro/pull/16)
|
||||
- Update restore point changes by @daddycocoaman in [#9](https://github.com/bishopfox/cirro/pull/9)
|
||||
- Improve restorepointcollections by @daddycocoaman in [#8](https://github.com/bishopfox/cirro/pull/8)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Re-add WITH before CALLs in Cypher and minor bugs by @daddycocoaman in [#29](https://github.com/bishopfox/cirro/pull/29)
|
||||
- Change generic resource MI checks to CONTAINS by @daddycocoaman in [#17](https://github.com/bishopfox/cirro/pull/17)
|
||||
|
||||
## [1.1.0] - 2026-04-13
|
||||
|
||||
### Added
|
||||
|
||||
- Add NetworkWatcher, KeyVaultCertificate, KeyVaultCertificateValue by @daddycocoaman in [#3](https://github.com/BishopFox/cirro/pull/3)
|
||||
- Add CODEOWNERS for bf-red-team (CLD-10728) by @grobbinsBF
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump version to 1.1.0
|
||||
- Sync docs with schemas by @daddycocoaman
|
||||
|
||||
### New Contributors
|
||||
* @grobbinsBF made their first contribution in [#2](https://github.com/BishopFox/cirro/pull/2)
|
||||
|
||||
## [1.0.0] - 2026-03-31
|
||||
|
||||
### Initial Release
|
||||
* Initial release by @daddycocoaman
|
||||
|
||||
[1.1.0]: https://github.com/BishopFox/cirro/compare/v1.0.0...v1.1.0
|
||||
|
||||
<!-- generated by git-cliff -->
|
||||
Generated
+3782
File diff suppressed because it is too large.
Load diff
+7
-2
@@ -2,7 +2,7 @@
|
||||
edition = "2024"
|
||||
name = "cirro"
|
||||
repository = "https://github.com/bishopfox/cirro"
|
||||
version = "1.0.0"
|
||||
version = "1.4.0"
|
||||
|
||||
[features]
|
||||
azure = ["collector"]
|
||||
@@ -15,6 +15,7 @@ collector = [
|
||||
"dep:jsonwebtoken",
|
||||
"dep:once_cell",
|
||||
"dep:pem",
|
||||
"dep:regex",
|
||||
"dep:reqwest",
|
||||
"dep:rsa",
|
||||
"dep:sha1",
|
||||
@@ -52,9 +53,13 @@ base64 = { version = "0.22.1", optional = true }
|
||||
chrono = { version = "0.4.41", optional = true }
|
||||
dashmap = { version = "5.5.3", optional = true }
|
||||
futures = { version = "0.3.31", optional = true }
|
||||
jsonwebtoken = { version = "9.3.1", features = ["use_pem"], optional = true }
|
||||
jsonwebtoken = { version = "10.3.0", features = [
|
||||
"use_pem",
|
||||
"aws_lc_rs",
|
||||
], optional = true }
|
||||
once_cell = { version = "1.19.0", optional = true }
|
||||
pem = { version = "3.0.5", optional = true }
|
||||
regex = { version = "1.12.3", optional = true }
|
||||
reqwest = { version = "0.13.2", features = [
|
||||
"json",
|
||||
"query",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Cirro is an extensible security research platform that enables researchers and penetration testers to collect, analyze, and visualize cloud environments and identity relationships through graph databases. Built with a modular architecture, Cirro can be extended to support multiple platforms and data sources.
|
||||
|
||||
You can check out the [Documentation](https://crispy-adventure-qre5p1k.pages.github.io/) for more info.
|
||||
You can check out the [Documentation](https://bishopfox.github.io/cirro) for more info.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -12,6 +12,10 @@ You can check out the [Documentation](https://crispy-adventure-qre5p1k.pages.git
|
||||
- **Cross-platform**: Available for Windows, macOS, and Linux
|
||||
- **Modular Design**: Optional platform functionality through feature flags and extensible plugin architecture
|
||||
- **Network Topology Analysis**: Support for network infrastructure platforms like Tailscale
|
||||
- **Entra ID Enrichment**: User managers and password-change timestamps, application creators, agent identity properties, and published permission scopes
|
||||
- **Restricted Guest Collection**: Traverse accessible application relationships with built-in, custom, or Microsoft catalog seeds
|
||||
|
||||
See the [collection guide](docs/usage/cirro.md) for usage examples and collection coverage.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -118,6 +122,24 @@ cirro graph ingest --type az --file cirro_output.db \
|
||||
--password password
|
||||
```
|
||||
|
||||
## Dry-Run Mode
|
||||
|
||||
Preview what would be ingested and post-processed without writing any data to the graph database:
|
||||
|
||||
```bash
|
||||
# Preview Azure ingestion and see which resource types have no implemented specs
|
||||
cirro graph ingest --type az --file cirro_output.db --dry-run
|
||||
|
||||
# Preview Tailscale ingestion
|
||||
cirro graph ingest --type ts --file cirro_ts_socket.json --dry-run
|
||||
```
|
||||
|
||||
In dry-run mode, Cirro will:
|
||||
|
||||
- Report each spec and its row count that **would** be processed
|
||||
- Skip all Neo4j writes (no ingestion or post-processing queries execute)
|
||||
- For Azure, list resource types in the input data that have no implemented spec
|
||||
|
||||
## Dashboard
|
||||
|
||||
CirroDash can be located here: [https://github.com/bishopfox/cirrodash](https://github.com/bishopfox/cirrodash)
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[changelog]
|
||||
# A Tera template to be rendered as the changelog's header.
|
||||
# See https://keats.github.io/tera/docs/#introduction
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n
|
||||
"""
|
||||
# A Tera template to be rendered for each release in the changelog.
|
||||
# See https://keats.github.io/tera/docs/#introduction
|
||||
body = """
|
||||
{%- macro remote_url() -%}
|
||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
||||
{%- endmacro -%}
|
||||
|
||||
{% if version -%}
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else -%}
|
||||
## [Unreleased]
|
||||
{% endif -%}
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits | unique(attribute="message") %}
|
||||
- {{ commit.message | split(pat="\n") | first | upper_first | trim }}\
|
||||
{% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
|
||||
{% if commit.remote.pr_number %} in \
|
||||
[#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
||||
### New Contributors
|
||||
{%- endif -%}
|
||||
|
||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
||||
* @{{ contributor.username }} made their first contribution
|
||||
{%- if contributor.pr_number %} in \
|
||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
||||
{%- endif %}
|
||||
{%- endfor %}\n
|
||||
|
||||
{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}{% raw %}\n{% endraw -%}{% endif %}
|
||||
|
||||
"""
|
||||
# A Tera template to be rendered as the changelog's footer.
|
||||
# See https://keats.github.io/tera/docs/#introduction
|
||||
footer = """
|
||||
{%- macro remote_url() -%}
|
||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
||||
{%- endmacro -%}
|
||||
|
||||
{% for release in releases -%}
|
||||
{% if release.version -%}
|
||||
{% if release.previous.version -%}
|
||||
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
||||
{{ self::remote_url() }}/compare/{{ release.previous.version }}...{{ release.version }}
|
||||
{% endif -%}
|
||||
{% else -%}
|
||||
[unreleased]: {{ self::remote_url() }}/compare/{{ release.previous.version }}...HEAD
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
# Remove leading and trailing whitespaces from the changelog's body.
|
||||
trim = true
|
||||
|
||||
[git]
|
||||
# Parse commits according to the conventional commits specification.
|
||||
# See https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# Exclude commits that do not match the conventional commits specification.
|
||||
filter_unconventional = true
|
||||
# An array of regex based parsers to modify commit messages prior to further processing.
|
||||
commit_preprocessors = [
|
||||
# Remove issue numbers.
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
|
||||
]
|
||||
# An array of regex based parsers for extracting data from the commit message.
|
||||
# Assigns commits to groups.
|
||||
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
|
||||
commit_parsers = [
|
||||
{ message = "^[a|A]dd", group = "Added" },
|
||||
{ message = "^[s|S]upport", group = "Added" },
|
||||
{ message = "^[r|R]emove", group = "Removed" },
|
||||
{ message = "^.*: add", group = "Added" },
|
||||
{ message = "^.*: support", group = "Added" },
|
||||
{ message = "^.*: remove", group = "Removed" },
|
||||
{ message = "^.*: delete", group = "Removed" },
|
||||
{ message = "^test", group = "Fixed" },
|
||||
{ message = "^fix", group = "Fixed" },
|
||||
{ message = "^.*: fix", group = "Fixed" },
|
||||
{ message = "^.*", group = "Changed" },
|
||||
{ body = "$^", skip = true },
|
||||
]
|
||||
# Exclude commits that are not matched by any commit parser.
|
||||
filter_commits = false
|
||||
# Order releases topologically instead of chronologically.
|
||||
topo_order = false
|
||||
# Order of commits in each group/release within the changelog.
|
||||
# Allowed values: newest, oldest
|
||||
sort_commits = "newest"
|
||||
@@ -24,5 +24,6 @@ install-path = "CARGO_HOME"
|
||||
install-updater = false
|
||||
|
||||
[dist.github-custom-runners]
|
||||
aarch64-apple-darwin = "macos-15"
|
||||
aarch64-unknown-linux-gnu.container = "ghcr.io/rust-cross/cargo-zigbuild"
|
||||
aarch64-unknown-linux-musl.container = "ghcr.io/rust-cross/cargo-zigbuild"
|
||||
@@ -15,6 +15,13 @@ These nodes are not direct objects in Entra ID or Azure but are created as a res
|
||||
- **[HybridExtension](additional-nodes/hybrid-extension.md)** - Extensions attached to hybrid machines
|
||||
- **[HybridIPAddress](additional-nodes/hybrid-ip-address.md)** - IP addresses discovered for hybrid machines
|
||||
- **[IPConfig](additional-nodes/ip-config.md)** - IP configuration details for network interfaces
|
||||
- **[KeyVaultCertificate](additional-nodes/key-vault-certificate.md)** - Key Vault certificate metadata
|
||||
- **[KeyVaultCertificateValue](additional-nodes/key-vault-certificate-value.md)** - Versioned Key Vault certificate data and policy details
|
||||
- **[KeyVaultSecret](additional-nodes/key-vault-secret.md)** - Key Vault secret metadata
|
||||
- **[KeyVaultSecretValue](additional-nodes/key-vault-secret-value.md)** - Versioned Key Vault secret values
|
||||
- **[LoadBalancerBackendAddressPool](additional-nodes/load-balancer-backend-address-pool.md)** - Backend address pools attached to load balancers
|
||||
- **[LoadBalancerFrontendIPConfiguration](additional-nodes/load-balancer-frontend-ip-configuration.md)** - Frontend IP configurations for load balancers
|
||||
- **[LoadBalancerRule](additional-nodes/load-balancer-rule.md)** - Rule definitions attached to load balancers
|
||||
- **[NetworkGateway](additional-nodes/network-gateway.md)** - Virtual network gateways
|
||||
- **[NetworkPeering](additional-nodes/network-peering.md)** - Virtual network peering connections
|
||||
- **[NetworkRoute](additional-nodes/network-route.md)** - Individual routes within route tables
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# AnalysisServiceFirewallRule
|
||||
|
||||
Represents firewall rule entries attached to an Analysis Services server.
|
||||
|
||||
**Labels:** `:AnalysisServiceFirewallRule`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `name` - Firewall rule name
|
||||
- `serverId` - Parent Analysis Services server resource ID (used with `name` as a composite key)
|
||||
- `rangeStart` - Start of allowed IPv4 range
|
||||
- `rangeEnd` - End of allowed IPv4 range
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **AnalysisServiceServer** → `HAS_RULE` → **AnalysisServiceFirewallRule** - Server that owns the firewall rule
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// List firewall rules per Analysis Services server
|
||||
MATCH (s:AnalysisServiceServer)-[:HAS_RULE]->(r:AnalysisServiceFirewallRule)
|
||||
RETURN s.serverFullName, r.name, r.rangeStart, r.rangeEnd
|
||||
ORDER BY s.serverFullName, r.name
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
# AzureStackEdgeMachineNic
|
||||
|
||||
Represents network interface cards attached to Azure Stack HCI edge machines (not an ARM resource itself).
|
||||
|
||||
**Labels:** `:AzureStackEdgeMachineNic`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `machineId` - Parent edge machine ID (composite key with `adapterName`)
|
||||
- `adapterName` - Network adapter name (composite key with `machineId`)
|
||||
- `dnsServers` - DNS server addresses
|
||||
- `ip4Address` - IPv4 address
|
||||
- `macAddress` - MAC address
|
||||
- `rdmaCapability` - RDMA capability of the adapter
|
||||
- `subnetMask` - Subnet mask
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **AzureStackEdgeMachine** → `HAS_NIC` → **AzureStackEdgeMachineNic** - Parent edge machine
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find edge machines and their NIC details
|
||||
MATCH (em:AzureStackEdgeMachine)-[:HAS_NIC]->(nic:AzureStackEdgeMachineNic)
|
||||
RETURN em.id, nic.adapterName, nic.ip4Address, nic.macAddress, nic.rdmaCapability
|
||||
```
|
||||
@@ -1,38 +0,0 @@
|
||||
# HybridExtension
|
||||
|
||||
Represents extensions installed on hybrid machines (not an ARM resource itself).
|
||||
|
||||
**Labels:** `:HybridExtension`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Extension ID (primary key)
|
||||
- `name` - Extension name
|
||||
- `type` - Extension type
|
||||
- `location` - Extension location
|
||||
- `typeHandlerVersion` - Type handler version
|
||||
- `autoUpgradeMinorVersion` - Auto upgrade minor version setting
|
||||
- `enableAutomaticUpgrade` - Enable automatic upgrade setting
|
||||
- `statusMessage` - Extension status message
|
||||
- `provisioningState` - Provisioning state
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **HybridMachine** → `HAS_EXTENSION` → HybridExtension - Parent hybrid machine
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find all hybrid machine extensions
|
||||
MATCH (hm:HybridMachine)-[:HAS_EXTENSION]->(ext:HybridExtension)
|
||||
RETURN hm.displayName, ext.name, ext.type, ext.provisioningState
|
||||
```
|
||||
|
||||
```cypher
|
||||
// Find extensions with auto upgrade enabled
|
||||
MATCH (ext:HybridExtension)
|
||||
WHERE ext.enableAutomaticUpgrade = true
|
||||
RETURN ext.name, ext.typeHandlerVersion
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
# HybridRunCommandParameter
|
||||
|
||||
Represents a parameter for a hybrid machine run command.
|
||||
|
||||
**Labels:** `:HybridRunCommandParameter`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `name` - Parameter name (composite key with `commandId`)
|
||||
- `commandId` - Parent run command ID (composite key)
|
||||
- `value` - Parameter value
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **HybridRunCommand** → `HAS_PARAMETER` → **HybridRunCommandParameter** - Parent run command
|
||||
@@ -0,0 +1,34 @@
|
||||
# HybridRunCommand
|
||||
|
||||
Represents a run command resource associated with an Azure Arc-enabled hybrid machine.
|
||||
|
||||
**Labels:** `:ArmResource:HybridRunCommand`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Run command resource ID (primary key)
|
||||
- `asyncExecution` - Whether execution is asynchronous
|
||||
- `runAsUser` - User context used to run the command
|
||||
- `timeoutInSeconds` - Command timeout in seconds
|
||||
- `treatFailureAsDeploymentFailure` - Failure handling behavior
|
||||
- `script` - Script body
|
||||
- `output` - Command output from the last execution
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **HybridMachine** → `HAS_RUN_COMMAND` → **HybridRunCommand** - Parent hybrid machine
|
||||
|
||||
### Outgoing
|
||||
|
||||
- **HybridRunCommand** → `HAS_PARAMETER` → **HybridRunCommandParameter** - Parameters bound to the run command
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find hybrid run commands and their parameters
|
||||
MATCH (hm:HybridMachine)-[:HAS_RUN_COMMAND]->(cmd:HybridRunCommand)
|
||||
OPTIONAL MATCH (cmd)-[:HAS_PARAMETER]->(p:HybridRunCommandParameter)
|
||||
RETURN hm.displayName, cmd.id, collect(p.name) AS parameterNames
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# KeyVaultCertificateValue
|
||||
|
||||
Represents versioned Azure Key Vault certificate value objects, including certificate material and issuance policy metadata.
|
||||
|
||||
**Labels:** `:KeyVaultCertificateValue`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Certificate value version ID (primary key)
|
||||
- `name` - Certificate name
|
||||
- `cer` - Encoded certificate content
|
||||
- `contentType` - Certificate content type
|
||||
- `kid` - Related Key Vault key ID
|
||||
- `sid` - Related Key Vault secret ID
|
||||
- `x509Thumbprint` - Thumbprint value
|
||||
- `x509ThumbprintHex` - Hex-encoded thumbprint
|
||||
- `enabled` - Whether the certificate value is enabled
|
||||
- `notBefore` - Time before which the certificate value is invalid
|
||||
- `expires` - Expiration time
|
||||
- `created` - Creation time
|
||||
- `updated` - Last update time
|
||||
- `recoverableDays` - Number of recoverable days
|
||||
- `recoveryLevel` - Recovery level
|
||||
- `issuerTransparency` - Certificate transparency setting from the issuer policy
|
||||
- `issuerType` - Issuer certificate type
|
||||
- `issuerName` - Issuer name
|
||||
- `exportable` - Whether the private key is exportable
|
||||
- `keyType` - Key algorithm or key type
|
||||
- `secretContentType` - Content type used for the backing secret
|
||||
- `keyUsage` - X.509 key usage values
|
||||
- `x509SubjectName` - X.509 subject name from the policy
|
||||
- `x509DnsNames` - DNS subject alternative names
|
||||
- `x509Emails` - Email subject alternative names
|
||||
- `x509Upns` - UPN subject alternative names
|
||||
- `tags` - Certificate tags as `key:value` strings
|
||||
|
||||
**Relationships:**
|
||||
|
||||
- `HAS_VERSION` ← KeyVaultCertificate - Parent certificate metadata node
|
||||
|
||||
## Example Queries
|
||||
|
||||
Find versioned Key Vault certificate values and their issuance details:
|
||||
|
||||
```cypher
|
||||
MATCH (c:KeyVaultCertificate)-[:HAS_VERSION]->(v:KeyVaultCertificateValue)
|
||||
RETURN c.name, v.x509SubjectName, v.secretContentType, v.expires
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
# KeyVaultCertificate
|
||||
|
||||
Represents Azure Key Vault certificate metadata.
|
||||
|
||||
**Labels:** `:KeyVaultCertificate`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Certificate ID (primary key)
|
||||
- `name` - Certificate name
|
||||
- `subject` - Certificate subject
|
||||
- `x509Thumbprint` - Thumbprint value
|
||||
- `x509ThumbprintHex` - Hex-encoded thumbprint
|
||||
- `enabled` - Whether the certificate is enabled
|
||||
- `notBefore` - Time before which the certificate is invalid
|
||||
- `expires` - Expiration time
|
||||
- `created` - Creation time
|
||||
- `updated` - Last update time
|
||||
- `recoverableDays` - Number of recoverable days
|
||||
- `recoveryLevel` - Recovery level
|
||||
- `tags` - Certificate tags as `key:value` strings
|
||||
|
||||
**Relationships:**
|
||||
|
||||
- `HAS_CERTIFICATE` ← KeyVault - Parent Key Vault
|
||||
- `HAS_VERSION` → KeyVaultCertificateValue - Versioned certificate value objects
|
||||
- `MATCHES_CERT` → Certificate - Matched application or service principal certificates by thumbprint
|
||||
|
||||
## Example Queries
|
||||
|
||||
Find Key Vault certificates, their versions, and any matched Entra certificates:
|
||||
|
||||
```cypher
|
||||
MATCH (kv:KeyVault)-[:HAS_CERTIFICATE]->(c:KeyVaultCertificate)
|
||||
OPTIONAL MATCH (c)-[:HAS_VERSION]->(v:KeyVaultCertificateValue)
|
||||
OPTIONAL MATCH (c)-[:MATCHES_CERT]->(cert:Certificate)
|
||||
RETURN kv.name, c.name, collect(DISTINCT v.id) AS versions, collect(DISTINCT cert.thumbprint) AS matchedThumbprints
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
# KeyVaultSecretValue
|
||||
|
||||
Represents versioned Azure Key Vault secret values.
|
||||
|
||||
**Labels:** `:KeyVaultSecretValue`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Secret value version ID (primary key)
|
||||
- `name` - Secret name
|
||||
- `value` - Secret value collected from Key Vault
|
||||
- `managed` - Whether Azure manages the secret
|
||||
- `contentType` - Secret content type
|
||||
- `kid` - Related Key Vault key ID, when present
|
||||
- `enabled` - Whether the secret value is enabled
|
||||
- `notBefore` - Time before which the secret value is invalid
|
||||
- `expires` - Expiration time
|
||||
- `created` - Creation time
|
||||
- `updated` - Last update time
|
||||
- `recoverableDays` - Number of recoverable days
|
||||
- `recoveryLevel` - Recovery level
|
||||
- `tags` - Secret tags as `key:value` strings
|
||||
|
||||
**Relationships:**
|
||||
|
||||
- `HAS_VERSION` ← KeyVaultSecret - Parent secret metadata node
|
||||
- `MATCHES_SECRET` → ClientSecret - Matched client secrets discovered during post-processing
|
||||
|
||||
## Example Queries
|
||||
|
||||
Find versioned Key Vault secret values and any matched client secrets:
|
||||
|
||||
```cypher
|
||||
MATCH (s:KeyVaultSecret)-[:HAS_VERSION]->(v:KeyVaultSecretValue)
|
||||
OPTIONAL MATCH (v)-[:MATCHES_SECRET]->(c:ClientSecret)
|
||||
RETURN s.name, v.id, c.hint
|
||||
```
|
||||
@@ -1,23 +1,38 @@
|
||||
# KeyVaultSecret
|
||||
|
||||
Represents a reference to an Azure Key Vault secret.
|
||||
Represents Azure Key Vault secret metadata.
|
||||
|
||||
**Labels:** `:KeyVaultSecret`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Derived from `{keyVaultId}/secrets/{keyVaultSecretName}` (lowercased)
|
||||
- `id` - Secret ID (primary key)
|
||||
- `name` - Secret name
|
||||
- `managed` - Whether Azure manages the secret
|
||||
- `contentType` - Secret content type
|
||||
- `kid` - Related Key Vault key ID, when present
|
||||
- `enabled` - Whether the secret is enabled
|
||||
- `notBefore` - Time before which the secret is invalid
|
||||
- `expires` - Expiration time
|
||||
- `created` - Creation time
|
||||
- `updated` - Last update time
|
||||
- `recoverableDays` - Number of recoverable days
|
||||
- `recoveryLevel` - Recovery level
|
||||
- `tags` - Secret tags as `key:value` strings
|
||||
|
||||
**Relationships:**
|
||||
|
||||
- `HAS_SECRET` ← KeyVault — The Key Vault that owns this secret reference
|
||||
- `USED_BY` → WebCertificate — The web certificate backed by this secret
|
||||
- `HAS_SECRET` ← KeyVault - Parent Key Vault
|
||||
- `HAS_VERSION` → KeyVaultSecretValue - Versioned secret values
|
||||
- `USED_BY` → WebCertificate - Web certificates backed by this secret
|
||||
|
||||
## Example Queries
|
||||
|
||||
Find all web certificates backed by Key Vault secrets, along with the owning Key Vault:
|
||||
Find Key Vault secrets, their versions, and any web certificates that consume them:
|
||||
|
||||
```cypher
|
||||
MATCH (kv:KeyVault)-[:HAS_SECRET]->(kvs:KeyVaultSecret)-[:USED_BY]->(cert:WebCertificate)
|
||||
RETURN kv.id AS keyVault, kvs.id AS secret, cert.id AS certificate
|
||||
MATCH (kv:KeyVault)-[:HAS_SECRET]->(s:KeyVaultSecret)
|
||||
OPTIONAL MATCH (s)-[:HAS_VERSION]->(v:KeyVaultSecretValue)
|
||||
OPTIONAL MATCH (s)-[:USED_BY]->(wc:WebCertificate)
|
||||
RETURN kv.name, s.name, collect(DISTINCT v.id) AS versions, collect(DISTINCT wc.name) AS webCertificates
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
# LoadBalancerBackendAddressPool
|
||||
|
||||
Represents backend address pool objects attached to a load balancer.
|
||||
|
||||
**Labels:** `:LoadBalancerBackendAddressPool`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Backend address pool resource ID (primary key)
|
||||
- `name` - Backend pool name
|
||||
- `provisioningState` - Current provisioning state
|
||||
- `type` - Azure resource type value
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **LoadBalancer** -> `HAS_BACKEND_POOL` -> **LoadBalancerBackendAddressPool** - Parent load balancer
|
||||
|
||||
### Outgoing
|
||||
|
||||
- **LoadBalancerBackendAddressPool** -> `ASSOCIATED_WITH` -> **NetworkInterface** - Backend NIC configuration reference
|
||||
@@ -0,0 +1,23 @@
|
||||
# LoadBalancerFrontendIPConfiguration
|
||||
|
||||
Represents frontend IP configuration objects for a load balancer.
|
||||
|
||||
**Labels:** `:LoadBalancerFrontendIPConfiguration`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Frontend configuration resource ID (primary key)
|
||||
- `name` - Frontend configuration name
|
||||
- `privateIPAllocationMethod` - Private IP allocation method
|
||||
- `provisioningState` - Current provisioning state
|
||||
- `type` - Azure resource type value
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **LoadBalancer** -> `HAS_LB_FRONTEND` -> **LoadBalancerFrontendIPConfiguration** - Parent load balancer
|
||||
|
||||
### Outgoing
|
||||
|
||||
- **LoadBalancerFrontendIPConfiguration** -> `HAS_IP` -> **PublicIPAddress** - Associated public IP (if present)
|
||||
@@ -0,0 +1,29 @@
|
||||
# LoadBalancerRule
|
||||
|
||||
Represents load balancing rule definitions attached to a load balancer.
|
||||
|
||||
**Labels:** `:LoadBalancerRule`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Rule resource ID (primary key)
|
||||
- `name` - Rule name
|
||||
- `protocol` - Protocol used by the rule
|
||||
- `frontendPort` - Frontend listening port
|
||||
- `backendPort` - Backend target port
|
||||
- `allowBackendPortConflict` - Whether backend port conflict is allowed
|
||||
- `disableConnectionTracking` - Whether connection tracking is disabled
|
||||
- `disableOutboundSnat` - Whether outbound SNAT is disabled
|
||||
- `enableConnectionTracking` - Whether connection tracking is enabled
|
||||
- `enableDestinationServiceEndpoint` - Whether destination service endpoint is enabled
|
||||
- `enableFloatingIP` - Whether floating IP is enabled
|
||||
- `enableTcpReset` - Whether TCP reset is enabled
|
||||
- `idleTimeoutInMinutes` - Idle timeout value
|
||||
- `provisioningState` - Current provisioning state
|
||||
- `type` - Azure resource type value
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **LoadBalancer** -> `HAS_RULE` -> **LoadBalancerRule** - Load balancer that owns the rule
|
||||
@@ -10,8 +10,11 @@ Represents an Active Directory domain configuration for an Azure NetApp Files ac
|
||||
- `administrators` - List of administrator accounts
|
||||
- `aesEncryption` - Whether AES encryption is enabled
|
||||
- `allowLocalNfsUsersWithLdap` - Whether local NFS users are allowed with LDAP
|
||||
- `backupOperators` - Backup operator accounts for the AD domain
|
||||
- `dns` - DNS server configuration
|
||||
- `domain` - Active Directory domain name
|
||||
- `encryptDCConnections` - Whether domain controller connections are encrypted
|
||||
- `kdcIP` - Kerberos KDC IP address
|
||||
- `ldapOverTLS` - Whether LDAP over TLS is enabled
|
||||
- `ldapSigning` - LDAP signing configuration
|
||||
- `organizationalUnit` - Organizational unit for computer accounts
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Represents an extension attached to an Azure virtual machine.
|
||||
|
||||
**Labels:** `:VMExtension`
|
||||
**Labels:** `:ArmResource:VMExtension`
|
||||
|
||||
**Properties:**
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ Represents a parameter for a virtual machine run command.
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `name` - Parameter name (primary key)
|
||||
- `name` - Parameter name (composite key with `commandId`)
|
||||
- `commandId` - Parent run command ID (composite key)
|
||||
- `value` - Parameter value
|
||||
|
||||
## Relationships
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# AnalysisServiceServer
|
||||
|
||||
Represents Azure Analysis Services server resources.
|
||||
|
||||
**Labels:** `:ArmResource:AnalysisServiceServer`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Resource ID (primary key)
|
||||
- `managedMode` - Indicates whether the server is managed
|
||||
- `serverFullName` - Full server name
|
||||
- `serverMonitorMode` - Monitoring mode for the server
|
||||
- `state` - Current state of the server
|
||||
- `administrators` - Raw administrator member entries from the resource
|
||||
- `enablePowerBIService` - Whether Power BI service access is enabled
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **GraphServicePrincipal** → `IS_ADMIN` → **AnalysisServiceServer** - Service principal listed as administrator
|
||||
- **GraphObject** → `IS_ADMIN` → **AnalysisServiceServer** - Directory object listed as administrator
|
||||
- **GraphUser** → `IS_ADMIN` → **AnalysisServiceServer** - User listed as administrator
|
||||
|
||||
### Outgoing
|
||||
|
||||
- **AnalysisServiceServer** → `HAS_RULE` → **AnalysisServiceFirewallRule** - Firewall rules defined on the server
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find Analysis Services servers and administrator principals
|
||||
MATCH (admin)-[:IS_ADMIN]->(s:AnalysisServiceServer)
|
||||
RETURN s.serverFullName, labels(admin), admin
|
||||
```
|
||||
|
||||
```cypher
|
||||
// Find Analysis Services servers and their firewall rules
|
||||
MATCH (s:AnalysisServiceServer)-[:HAS_RULE]->(r:AnalysisServiceFirewallRule)
|
||||
RETURN s.serverFullName, r.name, r.rangeStart, r.rangeEnd
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
# AzureStackEdgeMachine
|
||||
|
||||
Represents Azure Stack HCI edge machine resources.
|
||||
|
||||
**Labels:** `:ArmResource:AzureStackEdgeMachine`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Edge machine resource ID (primary key)
|
||||
- `arcMachineResourceGroupId` - Resource group ID of the associated Arc machine
|
||||
- `arcMachineResourceId` - Resource ID of the associated Arc machine
|
||||
- `cloudId` - Cloud identifier this edge machine is registered with
|
||||
- `connectivityStatus` - Connectivity status of the edge machine
|
||||
- `devicePoolResourceId` - Device pool resource ID
|
||||
- `edgeMachineKind` - Kind/type of edge machine
|
||||
- `lastUpdated` - Last update timestamp
|
||||
- `lastSyncTimestamp` - Last synchronization timestamp
|
||||
- `machineState` - Current machine state
|
||||
|
||||
## Relationships
|
||||
|
||||
### Outgoing
|
||||
|
||||
- **AzureStackEdgeMachine** → `REGISTERED_BY` → **GraphObject** - Identity that registered this edge machine
|
||||
- **AzureStackEdgeMachine** → `HAS_NIC` → **AzureStackEdgeMachineNic** - Network interfaces attached to this edge machine
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find all edge machines and their connectivity status
|
||||
MATCH (em:AzureStackEdgeMachine)
|
||||
RETURN em.id, em.edgeMachineKind, em.connectivityStatus, em.machineState
|
||||
```
|
||||
|
||||
```cypher
|
||||
// Find edge machines and their network interfaces
|
||||
MATCH (em:AzureStackEdgeMachine)-[:HAS_NIC]->(nic:AzureStackEdgeMachineNic)
|
||||
RETURN em.id, nic.adapterName, nic.ip4Address, nic.macAddress
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
# AzureStackRegistration
|
||||
|
||||
Represents Azure Stack registration resources, which link on-premises Azure Stack hardware to an Azure subscription.
|
||||
|
||||
**Labels:** `:ArmResource:AzureStackRegistration`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Registration resource ID (primary key)
|
||||
- `cloudId` - Cloud identifier for the registered Azure Stack
|
||||
|
||||
## Relationships
|
||||
|
||||
### Outgoing
|
||||
|
||||
- **AzureStackRegistration** → `REGISTERED_BY` → **GraphObject** - Identity that registered this Azure Stack
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find all Azure Stack registrations
|
||||
MATCH (r:AzureStackRegistration)
|
||||
RETURN r.id, r.cloudId
|
||||
```
|
||||
|
||||
```cypher
|
||||
// Find registrations and who registered them
|
||||
MATCH (r:AzureStackRegistration)-[:REGISTERED_BY]->(obj:GraphObject)
|
||||
RETURN r.id, r.cloudId, obj.id
|
||||
```
|
||||
@@ -7,6 +7,8 @@ Represents VM restore point collections.
|
||||
**Properties:**
|
||||
|
||||
- `id` - Restore point collection resource ID (primary key)
|
||||
- `restorePointCollectionId` - Collection identifier used for VM correlation
|
||||
|
||||
**Relationships:**
|
||||
- `HAS_RESTOREPOINT` ← ArmResource (source VM)
|
||||
- `HAS_RESTOREPOINTS` <- VirtualMachine (source VM)
|
||||
- `HAS_INSTANCE` -> RestorePoint (instances in the collection)
|
||||
@@ -0,0 +1,14 @@
|
||||
# RestorePoint
|
||||
|
||||
Represents individual restore point instances inside a restore point collection.
|
||||
|
||||
**Labels:** `:ArmResource:RestorePoint`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Restore point ARM resource ID (primary key)
|
||||
- `consistencyMode` - Snapshot consistency mode
|
||||
- `timeCreated` - Time the restore point was created
|
||||
|
||||
**Relationships:**
|
||||
- `HAS_INSTANCE` <- RestorePointCollection (parent collection)
|
||||
@@ -1,20 +1,41 @@
|
||||
# HybridExtension
|
||||
|
||||
Represents extensions installed on hybrid machines (not an ARM resource itself).
|
||||
Represents extensions installed on Azure Arc-enabled hybrid machines.
|
||||
|
||||
**Labels:** `:HybridExtension`
|
||||
**Labels:** `:ArmResource:HybridExtension`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Extension ID (primary key)
|
||||
- `id` - Extension resource ID (primary key)
|
||||
- `name` - Extension name
|
||||
- `type` - Extension type
|
||||
- `location` - Extension location
|
||||
- `type` - Resource type (`microsoft.hybridcompute/machines/extensions`)
|
||||
- `location` - Resource location
|
||||
- `extType` - Extension type (e.g. `CustomScriptExtension`)
|
||||
- `typeHandlerVersion` - Type handler version
|
||||
- `autoUpgradeMinorVersion` - Auto upgrade minor version
|
||||
- `enableAutomaticUpgrade` - Enable automatic upgrade
|
||||
- `statusMessage` - Status message
|
||||
- `statusMessage` - Extension status message
|
||||
- `provisioningState` - Provisioning state
|
||||
- `publisher` - Extension publisher
|
||||
- `settings` - Serialized extension settings (JSON)
|
||||
|
||||
**Relationships:**
|
||||
- `HAS_EXTENSION` ← HybridMachine
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **HybridMachine** → `HAS_EXTENSION` → **HybridExtension** - Parent hybrid machine
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find all hybrid machine extensions
|
||||
MATCH (hm:HybridMachine)-[:HAS_EXTENSION]->(ext:HybridExtension)
|
||||
RETURN hm.displayName, ext.name, ext.extType, ext.provisioningState
|
||||
```
|
||||
|
||||
```cypher
|
||||
// Find extensions with auto upgrade enabled
|
||||
MATCH (ext:HybridExtension)
|
||||
WHERE ext.enableAutomaticUpgrade = true
|
||||
RETURN ext.name, ext.extType, ext.typeHandlerVersion
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
# HybridGateway
|
||||
|
||||
Represents an Azure Arc gateway resource used to connect hybrid machines to Azure through a managed endpoint.
|
||||
|
||||
**Labels:** `:ArmResource:HybridGateway`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Gateway resource ID (primary key)
|
||||
- `allowedFeatures` - Features allowed through the gateway
|
||||
- `gatewayEndpoint` - Gateway endpoint URL
|
||||
- `gatewayId` - Unique gateway identifier
|
||||
- `gatewayType` - Type of gateway
|
||||
- `lastUpdateTime` - Last update timestamp
|
||||
- `provisioningState` - Provisioning state
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find all hybrid gateways
|
||||
MATCH (gw:HybridGateway)
|
||||
RETURN gw.id, gw.gatewayType, gw.gatewayEndpoint, gw.provisioningState
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# HybridPrivateLinkScope
|
||||
|
||||
Represents an Azure Arc private link scope resource used to connect hybrid machines over private endpoints.
|
||||
|
||||
**Labels:** `:ArmResource:HybridPrivateLinkScope`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Private link scope resource ID (primary key)
|
||||
- `privateLinkScopeId` - Unique private link scope identifier
|
||||
- `provisioningState` - Provisioning state
|
||||
- `publicNetworkAccess` - Public network access setting
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find private link scopes with public access enabled
|
||||
MATCH (pls:HybridPrivateLinkScope)
|
||||
WHERE pls.publicNetworkAccess = "Enabled"
|
||||
RETURN pls.id, pls.privateLinkScopeId
|
||||
```
|
||||
@@ -19,3 +19,5 @@ Represents Azure Key Vault instances.
|
||||
|
||||
**Relationships:**
|
||||
- `HAS_POLICY` → GraphObject (access policies for users/service principals)
|
||||
- `HAS_SECRET` → KeyVaultSecret
|
||||
- `HAS_CERTIFICATE` → KeyVaultCertificate
|
||||
@@ -0,0 +1,31 @@
|
||||
# ManagedHSM
|
||||
|
||||
Represents Azure Key Vault Managed HSM resources.
|
||||
|
||||
**Labels:** `:ManagedHSM`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Managed HSM resource ID (primary key)
|
||||
- `enablePurgeProtection` - Whether purge protection is enabled
|
||||
- `enableSoftDelete` - Whether soft delete is enabled
|
||||
- `hsmUri` - Managed HSM URI
|
||||
- `publicNetworkAccess` - Public network access setting
|
||||
- `activationStatus` - Security domain activation status
|
||||
- `activationStatusMessage` - Security domain activation status message
|
||||
- `softDeleteRetentionInDays` - Soft delete retention period
|
||||
- `statusMessage` - Service status message
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **GraphObject** → `IS_ADMIN` → **ManagedHSM** - Directory object configured as initial HSM administrator
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find Managed HSM resources and their administrators
|
||||
MATCH (go:GraphObject)-[:IS_ADMIN]->(hsm:ManagedHSM)
|
||||
RETURN hsm.id, hsm.hsmUri, go.id
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
# KustoCluster
|
||||
|
||||
Represents Azure Data Explorer (Kusto) cluster resources.
|
||||
|
||||
**Labels:** `:ArmResource:KustoCluster`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Cluster resource ID (primary key)
|
||||
- `acceptedAudiences` - Audiences accepted by the cluster
|
||||
- `allowedFqdnList` - List of allowed fully qualified domain names
|
||||
- `allowedIpRangeList` - List of allowed IP ranges
|
||||
- `dataIngestionUri` - URI for data ingestion
|
||||
- `enableAutoStop` - Whether auto-stop is enabled
|
||||
- `enableDiskEncryption` - Whether disk encryption is enabled
|
||||
- `enableDoubleEncryption` - Whether double encryption is enabled
|
||||
- `enablePurge` - Whether purge is enabled
|
||||
- `enableStreamingIngest` - Whether streaming ingestion is enabled
|
||||
- `engineType` - Type of engine (e.g., V3)
|
||||
- `publicIpType` - Type of public IP configuration
|
||||
- `publicNetworkAccess` - Public network access setting
|
||||
- `restrictOutboundNetworkAccess` - Whether outbound network access is restricted
|
||||
- `state` - Cluster state (e.g., Running, Unavailable)
|
||||
- `stateReason` - Reason for the cluster state
|
||||
- `trustedExternalTenants` - List of trusted external tenant IDs
|
||||
- `uri` - Cluster URI/endpoint
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find all Kusto clusters and their states
|
||||
MATCH (kc:KustoCluster)
|
||||
RETURN kc.id, kc.state, kc.uri, kc.enableAutoStop
|
||||
```
|
||||
|
||||
```cypher
|
||||
// Find Kusto clusters with specific security settings
|
||||
MATCH (kc:KustoCluster)
|
||||
WHERE kc.enableDoubleEncryption = true AND kc.publicNetworkAccess = 'Disabled'
|
||||
RETURN kc.id, kc.uri
|
||||
```
|
||||
@@ -15,4 +15,5 @@ Represents an Azure NetApp Files account for managing file storage pools and vol
|
||||
|
||||
- **IS_DEPLOYED** → **NetAppAccountADDomain** - Links to Active Directory domain configurations
|
||||
- **CONTAINS** → **NetAppCapacityPool** - Contains capacity pools
|
||||
- **HAS_BACKUP_VAULT** → **NetAppBackupVault** - Associated backup vaults
|
||||
- **HAS_POLICY** → **NetAppSnapshotPolicy** - Associated snapshot policies
|
||||
@@ -0,0 +1,14 @@
|
||||
# NetAppBackupVault
|
||||
|
||||
Represents a backup vault for Azure NetApp Files account backups.
|
||||
|
||||
**Labels:** `:ArmResource`, `:NetAppBackupVault`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Resource identifier (primary key)
|
||||
- `provisioningState` - Current provisioning state
|
||||
|
||||
## Relationships
|
||||
|
||||
- **NetAppAccount** → **HAS_BACKUP_VAULT** → **NetAppBackupVault** - Associated backup vault for the NetApp account
|
||||
@@ -10,6 +10,7 @@ Represents a volume within an Azure NetApp Files capacity pool.
|
||||
- `baremetalTenantId` - Bare metal tenant identifier
|
||||
- `coolAccess` - Cool access configuration
|
||||
- `creationToken` - Unique creation token for the volume
|
||||
- `enableSubvolumes` - Whether subvolumes are enabled
|
||||
- `encryptionKeySource` - Source of encryption keys
|
||||
- `fileSystemId` - File system identifier
|
||||
- `isDefaultQuotaEnabled` - Whether default quota is enabled
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# GitHub Network Settings
|
||||
|
||||
Represents Azure GitHub network settings resources used to associate GitHub connectivity settings with a subnet.
|
||||
|
||||
**Labels:** `:ArmResource:GithubNetworkSettings`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Resource ID (primary key)
|
||||
- `businessId` - GitHub business identifier associated with the network setting
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **ArmResource** → `HAS_GITHUB_SETTING` → **GithubNetworkSettings** - Subnet resource associated with this GitHub network setting
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find GitHub network settings and their linked subnet resources
|
||||
MATCH (sub:ArmResource)-[:HAS_GITHUB_SETTING]->(setting:GithubNetworkSettings)
|
||||
RETURN sub.id, setting.businessId
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
# LoadBalancer
|
||||
|
||||
Represents Azure Load Balancer resources.
|
||||
|
||||
**Labels:** `:ArmResource:LoadBalancer`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Load balancer resource ID (primary key)
|
||||
- `resourceGuid` - Azure-generated unique resource GUID
|
||||
- `provisioningState` - Current provisioning state
|
||||
|
||||
## Relationships
|
||||
|
||||
### Outgoing
|
||||
|
||||
- **LoadBalancer** -> `HAS_RULE` -> **LoadBalancerRule** - Load balancing rules attached to the load balancer
|
||||
- **LoadBalancer** -> `HAS_LB_FRONTEND` -> **LoadBalancerFrontendIPConfiguration** - Frontend IP configurations
|
||||
- **LoadBalancer** -> `HAS_BACKEND_POOL` -> **LoadBalancerBackendAddressPool** - Backend address pools
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find load balancers with frontend and backend topology
|
||||
MATCH (lb:LoadBalancer)
|
||||
OPTIONAL MATCH (lb)-[:HAS_LB_FRONTEND]->(fe:LoadBalancerFrontendIPConfiguration)
|
||||
OPTIONAL MATCH (lb)-[:HAS_BACKEND_POOL]->(be:LoadBalancerBackendAddressPool)
|
||||
RETURN lb.name, count(DISTINCT fe) AS frontendCount, count(DISTINCT be) AS backendPoolCount
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
# NetworkWatcher
|
||||
|
||||
Represents Azure Network Watcher resources.
|
||||
|
||||
**Labels:** `:ArmResource:NetworkWatcher`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Network Watcher resource ID (primary key)
|
||||
- `provisioningState` - Current provisioning state
|
||||
|
||||
No additional relationships are created for NetworkWatcher nodes by the current ingestion config.
|
||||
@@ -0,0 +1,27 @@
|
||||
# VirtualHub
|
||||
|
||||
Represents Azure Virtual Hub resources.
|
||||
|
||||
**Labels:** `:ArmResource:VirtualHub`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Virtual hub resource ID (primary key)
|
||||
- `allowBranchToBranchTraffic` - Whether branch-to-branch traffic is allowed
|
||||
- `hubRoutingPreference` - Hub routing preference mode
|
||||
- `provisioningState` - Current provisioning state
|
||||
- `routingState` - Current routing state
|
||||
- `virtualRouterAsn` - ASN used by the virtual router
|
||||
- `virtualRouterIps` - Virtual router IP addresses
|
||||
|
||||
## Relationships
|
||||
|
||||
No direct relationships are created in ingestion for this node type.
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find virtual hubs and routing posture
|
||||
MATCH (vh:VirtualHub)
|
||||
RETURN vh.name, vh.hubRoutingPreference, vh.routingState, vh.virtualRouterAsn
|
||||
```
|
||||
@@ -0,0 +1,54 @@
|
||||
# OracleAutonomousDB
|
||||
|
||||
Represents Oracle Autonomous Database resources discovered from Azure ARM.
|
||||
|
||||
**Labels:** `:ArmResource:OracleAutonomousDB`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Resource ID (primary key)
|
||||
- `actualUsedDataStorageSizeInTbs` - Actual used storage size in TB
|
||||
- `allocatedStorageSizeInTbs` - Allocated storage size in TB
|
||||
- `backupRententionPeriodInDays` - Backup retention period in days
|
||||
- `characterSet` - Database character set
|
||||
- `databaseType` - Oracle database type (from ARM `dataBaseType`)
|
||||
- `dbVersion` - Database version
|
||||
- `displayName` - Display name
|
||||
- `isLocalDataGuardEnabled` - Whether local Data Guard is enabled
|
||||
- `isMtlsConnectionRequired` - Whether mTLS is required for connections
|
||||
- `isRemoteDataGuardEnabled` - Whether remote Data Guard is enabled
|
||||
- `localDisasterRecoveryType` - Local disaster recovery type
|
||||
- `ociUrl` - Oracle Cloud URL
|
||||
- `ocid` - Oracle Cloud identifier
|
||||
- `openMode` - Database open mode
|
||||
- `permissionLevel` - Permission/access level
|
||||
- `privateEndpoint` - Private endpoint resource
|
||||
- `privateEndpointIp` - Private endpoint IP address
|
||||
- `privateEndpointLabel` - Private endpoint label
|
||||
- `timeCreated` - Creation timestamp
|
||||
- `timeLocalDataGuardEnabled` - Timestamp when local Data Guard was enabled
|
||||
- `highConnectionString` - High workload connection string
|
||||
- `mediumConnectionString` - Medium workload connection string
|
||||
- `lowConnectionString` - Low workload connection string
|
||||
- Dynamic URL properties from `connectionUrls` (e.g. `apexUrl`, `ordsUrl`, `sqlDevWebUrl`, `graphStudioUrl`, `machineLearningNotebookUrl`, `databaseTransformsUrl`)
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **ArmResource** → `HAS_ORACLE_RESOURCE` → **OracleAutonomousDB** - Subnet-linked parent resource contains this database
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find Oracle autonomous databases and connection info
|
||||
MATCH (db:OracleAutonomousDB)
|
||||
RETURN db.displayName, db.dbVersion, db.openMode, db.permissionLevel
|
||||
```
|
||||
|
||||
```cypher
|
||||
// Find databases with mTLS not required
|
||||
MATCH (db:OracleAutonomousDB)
|
||||
WHERE db.isMtlsConnectionRequired = false
|
||||
RETURN db.displayName, db.privateEndpoint
|
||||
```
|
||||
@@ -0,0 +1,19 @@
|
||||
# SyntexDocumentProcessor
|
||||
|
||||
Represents Microsoft Syntex document processor resources, which provide AI-based document processing capabilities integrated with SharePoint Online.
|
||||
|
||||
**Labels:** `:ArmResource:SyntexDocumentProcessor`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Resource ID (primary key)
|
||||
- `spoTenantId` - SharePoint Online tenant ID
|
||||
- `spoTenantUrl` - SharePoint Online tenant URL
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find all Syntex document processors
|
||||
MATCH (sdp:SyntexDocumentProcessor)
|
||||
RETURN sdp.id, sdp.spoTenantId, sdp.spoTenantUrl
|
||||
```
|
||||
@@ -29,4 +29,7 @@ Represents Azure App Service web apps.
|
||||
- `inProgressOperationId` - In-progress operation ID
|
||||
|
||||
**Relationships:**
|
||||
- `HOSTS_SITE` ← ServerFarm
|
||||
|
||||
- `HOSTS_SITE` ← ServerFarm - Created only when `properties.serverFarmId` is non-null
|
||||
|
||||
Sites without a `serverFarmId` are still ingested, without a hosting ServerFarm relationship.
|
||||
@@ -0,0 +1,21 @@
|
||||
# CREATED
|
||||
|
||||
Links a creator application to an application or service principal using `createdByAppId`. The edge is created only when that property is present.
|
||||
|
||||
## Usage
|
||||
|
||||
- **GraphObject** → `CREATED` → **GraphApplication** - Creator matched by `appId`, stored in lowercase
|
||||
- **GraphApplication** → `CREATED` → **GraphServicePrincipal** - Creator matched by `appId`
|
||||
|
||||
A creator can initially be a placeholder with only an `appId`; its full details need not have been collected.
|
||||
|
||||
## Properties
|
||||
|
||||
No additional properties on the relationship.
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
MATCH (creator)-[:CREATED]->(created)
|
||||
RETURN creator.appId, created.displayName, labels(created)
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
# HAS_BACKEND_POOL
|
||||
|
||||
Represents the relationship between a load balancer and its backend address pools.
|
||||
|
||||
## Usage
|
||||
|
||||
- **LoadBalancer** -> `HAS_BACKEND_POOL` -> **LoadBalancerBackendAddressPool** - Backend pool attached to a load balancer
|
||||
|
||||
## Properties
|
||||
|
||||
No additional properties on the relationship.
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
MATCH (lb:LoadBalancer)-[:HAS_BACKEND_POOL]->(pool:LoadBalancerBackendAddressPool)
|
||||
RETURN lb.name, pool.name, pool.provisioningState
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
# HAS_BACKUP_VAULT
|
||||
|
||||
Links NetApp accounts to their associated NetApp backup vault resources.
|
||||
|
||||
**Relationship:** `:HAS_BACKUP_VAULT`
|
||||
|
||||
**Direction:** `(:NetAppAccount)-[:HAS_BACKUP_VAULT]->(:NetAppBackupVault)`
|
||||
|
||||
## Description
|
||||
|
||||
This relationship indicates which backup vault belongs to a specific NetApp account.
|
||||
|
||||
## Examples
|
||||
|
||||
### NetAppAccount → NetAppBackupVault
|
||||
Connects an Azure NetApp Files account to the backup vault used to store and manage backups for that account.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Identifying backup coverage for NetApp accounts
|
||||
- Mapping backup dependencies for NetApp storage workloads
|
||||
- Tracing account-to-vault backup topology
|
||||
@@ -0,0 +1,21 @@
|
||||
# HAS_GITHUB_SETTING
|
||||
|
||||
Represents an association between a subnet resource and a GitHub network settings resource.
|
||||
|
||||
## Usage
|
||||
|
||||
This relationship connects subnet resources to their GitHub network settings:
|
||||
|
||||
- **ArmResource** → `HAS_GITHUB_SETTING` → **GithubNetworkSettings** - Subnet resource linked to a GitHub network setting
|
||||
|
||||
## Properties
|
||||
|
||||
No additional properties on the relationship.
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find subnet resources with GitHub network settings
|
||||
MATCH (sub:ArmResource)-[:HAS_GITHUB_SETTING]->(setting:GithubNetworkSettings)
|
||||
RETURN sub.id, setting.businessId
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# HAS_INSTANCE
|
||||
|
||||
Represents the relationship between container resources and their concrete instances.
|
||||
|
||||
## Usage
|
||||
|
||||
This relationship connects restore point collections to their restore point instances:
|
||||
|
||||
- **RestorePointCollection** -> `HAS_INSTANCE` -> **RestorePoint** - Collection to restore point instance
|
||||
|
||||
## Properties
|
||||
|
||||
No additional properties on the relationship.
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find restore point collections and their instances
|
||||
MATCH (rpc:RestorePointCollection)-[:HAS_INSTANCE]->(rp:RestorePoint)
|
||||
RETURN rpc.id, rp.id, rp.timeCreated
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
# HAS_LB_FRONTEND
|
||||
|
||||
Represents the relationship between a load balancer and its frontend IP configurations.
|
||||
|
||||
## Usage
|
||||
|
||||
- **LoadBalancer** -> `HAS_LB_FRONTEND` -> **LoadBalancerFrontendIPConfiguration** - Frontend configuration attached to a load balancer
|
||||
|
||||
## Properties
|
||||
|
||||
No additional properties on the relationship.
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
MATCH (lb:LoadBalancer)-[:HAS_LB_FRONTEND]->(fe:LoadBalancerFrontendIPConfiguration)
|
||||
RETURN lb.name, fe.name, fe.privateIPAllocationMethod
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# HAS_ORACLE_RESOURCE
|
||||
|
||||
Represents the relationship between an ARM resource and Oracle database resources hosted within it.
|
||||
|
||||
**Direction:** `(subnet)-[:HAS_ORACLE_RESOURCE]->(oracleDb)`
|
||||
|
||||
**Description:** Indicates that an ARM resource (typically a subnet) contains or is associated with an Oracle Autonomous Database resource.
|
||||
|
||||
**Properties:** None
|
||||
|
||||
## Usage
|
||||
|
||||
- **ArmResource** → `HAS_ORACLE_RESOURCE` → **OracleAutonomousDB** - Subnet or parent resource associated with an Oracle Autonomous Database
|
||||
|
||||
## Query Examples
|
||||
|
||||
```cypher
|
||||
// Find all Oracle databases and their parent resources
|
||||
MATCH (src:ArmResource)-[:HAS_ORACLE_RESOURCE]->(db:OracleAutonomousDB)
|
||||
RETURN src.id, db.displayName, db.dbVersion
|
||||
|
||||
// Find Oracle databases accessible via private endpoint
|
||||
MATCH (src:ArmResource)-[:HAS_ORACLE_RESOURCE]->(db:OracleAutonomousDB)
|
||||
WHERE db.privateEndpointIp IS NOT NULL
|
||||
RETURN src.id, db.displayName, db.privateEndpointIp
|
||||
```
|
||||
@@ -0,0 +1,20 @@
|
||||
# HAS_PUBLISHED_SCOPE
|
||||
|
||||
Links a service principal to a permission scope in its `publishedPermissionScopes` collection.
|
||||
|
||||
## Usage
|
||||
|
||||
- **GraphServicePrincipal** → `HAS_PUBLISHED_SCOPE` → **GraphApplicationScope** - Published permission scope
|
||||
|
||||
This relationship describes a published scope, not a consent grant or role assignment.
|
||||
|
||||
## Properties
|
||||
|
||||
No additional properties on the relationship.
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
MATCH (sp:GraphServicePrincipal)-[:HAS_PUBLISHED_SCOPE]->(scope:GraphApplicationScope)
|
||||
RETURN sp.displayName, scope.value, scope.isEnabled
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
# HAS_RESTOREPOINT
|
||||
|
||||
Represents the relationship between virtual machines and their restore points.
|
||||
|
||||
## Usage
|
||||
|
||||
This relationship connects virtual machines to their restore point collections:
|
||||
|
||||
- **VirtualMachine** → `HAS_RESTOREPOINT` → **RestorePointCollection** - Virtual machines to their restore points
|
||||
|
||||
## Properties
|
||||
|
||||
No additional properties on the relationship.
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find all virtual machines with restore points
|
||||
MATCH (vm:VirtualMachine)-[:HAS_RESTOREPOINT]->(rp:RestorePointCollection)
|
||||
RETURN vm.name, rp.name, rp.restorePointCollectionSourceId
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# HAS_RESTOREPOINTS
|
||||
|
||||
Represents the relationship between virtual machines and restore point collections.
|
||||
|
||||
## Usage
|
||||
|
||||
This relationship connects virtual machines to their restore point collections:
|
||||
|
||||
- **VirtualMachine** -> `HAS_RESTOREPOINTS` -> **RestorePointCollection** - VM to collection ownership
|
||||
|
||||
## Properties
|
||||
|
||||
No additional properties on the relationship.
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find all virtual machines with restore point collections
|
||||
MATCH (vm:VirtualMachine)-[:HAS_RESTOREPOINTS]->(rpc:RestorePointCollection)
|
||||
RETURN vm.name, rpc.id, rpc.restorePointCollectionId
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
# IS_ADMIN
|
||||
|
||||
Represents an administrative relationship where a principal administers a target resource.
|
||||
|
||||
## Usage
|
||||
|
||||
This relationship is used to connect principals to resources they administer:
|
||||
|
||||
- **GraphServicePrincipal** → `IS_ADMIN` → **AnalysisServiceServer** - Service principal administrator
|
||||
- **GraphObject** → `IS_ADMIN` → **AnalysisServiceServer** - Object-based administrator reference
|
||||
- **GraphUser** → `IS_ADMIN` → **AnalysisServiceServer** - User administrator
|
||||
- **GraphObject** → `IS_ADMIN` → **ManagedHSM** - Initial Managed HSM administrator
|
||||
|
||||
## Properties
|
||||
|
||||
No additional properties on the relationship.
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find principals that administer Analysis Services servers
|
||||
MATCH (p)-[:IS_ADMIN]->(s:AnalysisServiceServer)
|
||||
RETURN labels(p), p, s.serverFullName
|
||||
|
||||
// Find GraphObjects that administer Managed HSM resources
|
||||
MATCH (go:GraphObject)-[:IS_ADMIN]->(hsm:ManagedHSM)
|
||||
RETURN go.id, hsm.id, hsm.hsmUri
|
||||
```
|
||||
@@ -9,6 +9,7 @@ Represents management relationships between resources.
|
||||
**Common Patterns:**
|
||||
- Automation accounts manage runbooks
|
||||
- SQL servers manage databases
|
||||
- Entra ID managers manage enabled member users
|
||||
|
||||
**Properties:** None
|
||||
|
||||
@@ -26,4 +27,8 @@ RETURN path
|
||||
// Find management relationships
|
||||
MATCH path=(manager)-[:MANAGES]->(managed)
|
||||
RETURN path
|
||||
|
||||
// Find Entra ID users and their managers
|
||||
MATCH path=(manager:GraphUser)-[:MANAGES]->(user:GraphUser)
|
||||
RETURN path
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
# REGISTERED_BY
|
||||
|
||||
Represents the relationship between an Azure Stack resource and the identity that registered it.
|
||||
|
||||
**Direction:** `(resource)-[:REGISTERED_BY]->(graphObject)`
|
||||
|
||||
**Description:** Indicates that an Azure Stack resource (registration or edge machine) was registered by a specific identity (Graph object).
|
||||
|
||||
**Properties:** None
|
||||
|
||||
## Usage
|
||||
|
||||
- **AzureStackRegistration** → `REGISTERED_BY` → **GraphObject** - Azure Stack registration associated with its registering identity
|
||||
- **AzureStackEdgeMachine** → `REGISTERED_BY` → **GraphObject** - Edge machine associated with its registering identity
|
||||
|
||||
## Query Examples
|
||||
|
||||
```cypher
|
||||
// Find all Azure Stack resources and their registering identities
|
||||
MATCH (r:ArmResource)-[:REGISTERED_BY]->(obj:GraphObject)
|
||||
WHERE r:AzureStackRegistration OR r:AzureStackEdgeMachine
|
||||
RETURN r.id, labels(r), obj.id
|
||||
```
|
||||
@@ -1,9 +1,10 @@
|
||||
# Graph Nodes
|
||||
# GraphObject
|
||||
|
||||
<h2>GraphObject (Base Label)</h2>
|
||||
|
||||
All Microsoft Graph entities inherit from the base `GraphObject` label and include the `id` property.
|
||||
Microsoft Graph directory entities use the base `GraphObject` label. Fully collected entities include an `id`; creator placeholders introduced by [`CREATED`](edges/created.md) can initially have only an `appId`.
|
||||
|
||||
**Common Properties:**
|
||||
|
||||
- `id` - Unique identifier (primary key)
|
||||
- `id` - Unique object identifier (primary key)
|
||||
- `odataType` - Lowercase `@odata.type`, when present, on users, groups, applications, service principals, devices, and administrative units
|
||||
@@ -6,6 +6,8 @@ Represents Entra ID administrative units collected from Microsoft Graph.
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `odataType` - Lowercase `@odata.type`, when returned by Microsoft Graph
|
||||
|
||||
- `description` - Administrative unit description
|
||||
- `displayName` - Administrative unit display name
|
||||
- `isMemberManagementRestricted` - Whether member management is restricted
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# GraphApplicationScope
|
||||
|
||||
Represents a permission scope from a service principal's `publishedPermissionScopes` collection.
|
||||
|
||||
**Labels:** `:GraphApplicationScope`
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Permission scope ID (merge key)
|
||||
- `adminConsentDescription` - Description shown for administrator consent
|
||||
- `adminConsentDisplayName` - Display name shown for administrator consent
|
||||
- `isEnabled` - Whether the scope is enabled
|
||||
- `type` - Consent type returned by Microsoft Graph
|
||||
- `userConsentDescription` - Description shown for user consent
|
||||
- `userConsentDisplayName` - Display name shown for user consent
|
||||
- `value` - Permission scope value
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **GraphServicePrincipal** → [`HAS_PUBLISHED_SCOPE`](../edges/has-published-scope.md) → **GraphApplicationScope** - Service principal publishing the scope
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
MATCH (sp:GraphServicePrincipal)-[:HAS_PUBLISHED_SCOPE]->(scope:GraphApplicationScope)
|
||||
RETURN sp.displayName, scope.value, scope.type
|
||||
```
|
||||
@@ -6,6 +6,21 @@ Represents Entra ID applications collected from Microsoft Graph.
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `description` - Application description
|
||||
- `notes` - Application notes
|
||||
- `createdDateTime` - Creation timestamp
|
||||
- `createdByAppId` - Application ID of the creator
|
||||
- `applicationTemplateId` - Application template ID
|
||||
- `isAuthorizationServiceEnabled` - Authorization service flag
|
||||
- `isDeviceOnlyAuthSupported` - Device-only authentication support flag
|
||||
- `isDisabled` - Whether the application is disabled
|
||||
- `isFallbackPublicClient` - Fallback public client flag
|
||||
- `isManagementRestricted` - Management restriction flag
|
||||
- `nativeAuthenticationApisEnabled` - Native authentication API configuration
|
||||
- `defaultRedirectUri` - First URI in `web.redirectUris`
|
||||
|
||||
- `odataType` - Lowercase `@odata.type`, when returned by Microsoft Graph
|
||||
|
||||
- `id` - Application object ID (primary key)
|
||||
- `displayName` - Application's display name
|
||||
- `appId` - Application ID (client ID)
|
||||
@@ -23,15 +38,18 @@ Represents Entra ID applications collected from Microsoft Graph.
|
||||
|
||||
### Incoming
|
||||
|
||||
- **GraphObject** → [`CREATED`](../edges/created.md) → **GraphApplication** - Creator identified by `createdByAppId`
|
||||
|
||||
- **GraphObject** → `OWNS` → **GraphApplication** - Owners of the application
|
||||
- **GraphObject** → `APPROLE` → **GraphApplication** - Objects with app role assignments
|
||||
- **FederatedIdentityCredential** → `AUTHENTICATES` → **GraphApplication** - Federated identity credentials
|
||||
- **ClientSecret** → `AUTHENTICATES` → **GraphApplication** - Client secrets for authentication
|
||||
- **Certificate** → `AUTHENTICATES` → **GraphApplication** - Certificates for authentication
|
||||
|
||||
### Outgoing
|
||||
|
||||
- **GraphApplication** → [`CREATED`](../edges/created.md) → **GraphServicePrincipal** - Service principals created by the application
|
||||
|
||||
- **GraphApplication** → `HAS_APPROLE` → **GraphAppRole** - App roles defined by the application
|
||||
- **GraphApplication** → `FEDERATED_CREDENTIAL` → **FederatedIdentityCredential** - Federated identity credentials
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -50,6 +68,12 @@ RETURN app.displayName, collect(owner.displayName) AS owners
|
||||
|
||||
```cypher
|
||||
// Find applications with federated credentials
|
||||
MATCH (app:GraphApplication)-[:FEDERATED_CREDENTIAL]->(cred:FederatedIdentityCredential)
|
||||
MATCH (cred:FederatedIdentityCredential)-[:AUTHENTICATES]->(app:GraphApplication)
|
||||
RETURN app.displayName, cred.issuer, cred.subject
|
||||
```
|
||||
|
||||
```cypher
|
||||
// Find applications created by another application
|
||||
MATCH (creator:GraphObject)-[:CREATED]->(app:GraphApplication)
|
||||
RETURN creator.appId, app.displayName, app.createdDateTime, app.applicationTemplateId
|
||||
```
|
||||
@@ -6,6 +6,8 @@ Represents Entra ID devices collected from Microsoft Graph.
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `odataType` - Lowercase `@odata.type`, when returned by Microsoft Graph
|
||||
|
||||
- `displayName` - Device display name
|
||||
- `accountEnabled` - Whether the device account is enabled
|
||||
- `deviceId` - Device identifier
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# Eligible Role Assignments
|
||||
|
||||
Represents eligible role assignments from Microsoft Graph PIM (Privileged Identity Management).
|
||||
|
||||
**Labels:** `:GraphObject`, `:GraphRole`
|
||||
|
||||
This configuration creates `IS_ROLE_ELIGIBLE` relationships between principals and their eligible Graph roles.
|
||||
|
||||
**Relationship Properties (on IS_ROLE_ELIGIBLE):**
|
||||
|
||||
- `createdDateTime` - Creation date/time
|
||||
- `directoryScopeId` - Directory scope ID
|
||||
- `memberType` - Member type
|
||||
- `modifiedDateTime` - Modified date/time
|
||||
- `expirationType` - Expiration type
|
||||
- `startDateTime` - Start date/time
|
||||
- `endDateTime` - End date/time
|
||||
- `recurrence` - Recurrence schedule
|
||||
|
||||
## Relationships
|
||||
|
||||
- **GraphObject** → `IS_ROLE_ELIGIBLE` → **GraphRole** - Principal is eligible for the role
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
// Find users eligible for directory roles
|
||||
MATCH (u:GraphUser)-[r:IS_ROLE_ELIGIBLE]->(role:GraphRole)
|
||||
RETURN u.displayName, role.displayName, r.expirationType, r.endDateTime
|
||||
```
|
||||
@@ -6,6 +6,8 @@ Represents Entra ID groups collected from Microsoft Graph.
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `odataType` - Lowercase `@odata.type`, when returned by Microsoft Graph
|
||||
|
||||
- `id` - Group object ID (primary key)
|
||||
- `displayName` - Group display name
|
||||
- `groupTypes` - Array of group types (e.g., ["DynamicMembership", "Unified"])
|
||||
|
||||
@@ -6,6 +6,13 @@ Represents Entra ID service principals collected from Microsoft Graph.
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `id` - Service principal object ID (primary key)
|
||||
- `createdByAppId` - Application ID of the creator
|
||||
- `agentAppId` - Agent application ID, when present
|
||||
- `agentIdentityBlueprintId` - Agent identity blueprint ID, when present
|
||||
|
||||
- `odataType` - Lowercase `@odata.type`, when returned by Microsoft Graph
|
||||
|
||||
- `displayName` - Service principal's display name
|
||||
- `accountEnabled` - Whether the service principal is enabled
|
||||
- `alternativeNames` - Alternative names for the service principal
|
||||
@@ -17,3 +24,38 @@ Represents Entra ID service principals collected from Microsoft Graph.
|
||||
- `logoutUrl` - Logout URL for the service principal
|
||||
- `replyUrls` - Array of reply URLs
|
||||
- `servicePrincipalNames` - Array of service principal names
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **GraphApplication** → [`CREATED`](../edges/created.md) → **GraphServicePrincipal** - Creator identified by `createdByAppId`
|
||||
- **GraphApplication** → [`REPRESENTED_BY`](../edges/represented-by.md) → **GraphServicePrincipal** - Application represented by the service principal, matched by `appId`
|
||||
- **GraphObject** → [`ASSIGNED_APPROLE`](../edges/assigned-approle.md) → **GraphServicePrincipal** - Principal assigned an app role on this resource
|
||||
- **GraphObject** → [`OWNS`](../edges/owns.md) → **GraphServicePrincipal** - Owner of the service principal
|
||||
|
||||
### Outgoing
|
||||
|
||||
- **GraphServicePrincipal** → [`HAS_PUBLISHED_SCOPE`](../edges/has-published-scope.md) → **GraphApplicationScope** - Scopes from `publishedPermissionScopes`
|
||||
- **GraphServicePrincipal** → [`HAS_APPROLE`](../edges/has-approle.md) → **GraphAppRole** - Roles defined by the service principal
|
||||
|
||||
## Examples
|
||||
|
||||
```cypher
|
||||
MATCH (sp:GraphServicePrincipal)-[:HAS_PUBLISHED_SCOPE]->(scope:GraphApplicationScope)
|
||||
RETURN sp.displayName, scope.value, scope.adminConsentDisplayName, scope.isEnabled
|
||||
```
|
||||
|
||||
```cypher
|
||||
// Review assignments, including principals with only an object ID
|
||||
MATCH (principal:GraphObject)-[assignment:ASSIGNED_APPROLE]->(resource:GraphServicePrincipal)
|
||||
RETURN coalesce(principal.displayName, principal.id) AS principal,
|
||||
resource.displayName, assignment.appRoleId
|
||||
```
|
||||
|
||||
```cypher
|
||||
// Find service principals with agent identity metadata
|
||||
MATCH (sp:GraphServicePrincipal)
|
||||
WHERE sp.agentAppId IS NOT NULL OR sp.agentIdentityBlueprintId IS NOT NULL
|
||||
RETURN sp.displayName, sp.odataType, sp.agentAppId, sp.agentIdentityBlueprintId
|
||||
```
|
||||
@@ -6,6 +6,12 @@ Represents Entra ID users collected from Microsoft Graph.
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `agentIdentityBlueprintId` - Agent identity blueprint ID, when present
|
||||
- `identityParentId` - Parent identity ID, when present
|
||||
- `identityParentType` - Parent identity type, when present
|
||||
|
||||
- `odataType` - Lowercase `@odata.type`, when returned by Microsoft Graph
|
||||
|
||||
- `id` - User object ID (primary key)
|
||||
- `accountEnabled` - Whether the user account is enabled
|
||||
- `city` - User's city
|
||||
@@ -16,6 +22,7 @@ Represents Entra ID users collected from Microsoft Graph.
|
||||
- `displayName` - User's display name
|
||||
- `givenName` - User's first name
|
||||
- `jobTitle` - User's job title
|
||||
- `lastPasswordChangeDateTime` - When the user's password was last changed or created
|
||||
- `mail` - User's email address
|
||||
- `mailNickname` - User's mail nickname
|
||||
- `mobilePhone` - User's mobile phone number
|
||||
@@ -37,6 +44,10 @@ Represents Entra ID users collected from Microsoft Graph.
|
||||
|
||||
## Relationships
|
||||
|
||||
### Incoming
|
||||
|
||||
- **GraphUser** ← `MANAGES` ← **GraphObject** - The user's manager
|
||||
|
||||
### Outgoing
|
||||
|
||||
- **GraphUser** → `MEMBER_OF` → **GraphObject** - Groups the user is a member of
|
||||
@@ -62,3 +73,26 @@ RETURN u.displayName, u.mail, u.companyName
|
||||
MATCH (u:GraphUser)-[:MEMBER_OF]->(g:GraphGroup)
|
||||
RETURN u.displayName, collect(g.displayName) AS groups
|
||||
```
|
||||
|
||||
```cypher
|
||||
// Find enabled member users and their managers
|
||||
MATCH (manager:GraphObject)-[:MANAGES]->(user:GraphUser)
|
||||
WHERE user.accountEnabled = true AND user.userType = "Member"
|
||||
RETURN user.displayName, coalesce(manager.displayName, manager.id) AS manager
|
||||
```
|
||||
|
||||
```cypher
|
||||
// Review collected password-change timestamps
|
||||
MATCH (user:GraphUser)
|
||||
WHERE user.lastPasswordChangeDateTime IS NOT NULL
|
||||
RETURN user.userPrincipalName, user.lastPasswordChangeDateTime
|
||||
ORDER BY user.lastPasswordChangeDateTime
|
||||
```
|
||||
|
||||
```cypher
|
||||
// Find users with agent identity metadata
|
||||
MATCH (user:GraphUser)
|
||||
WHERE user.agentIdentityBlueprintId IS NOT NULL OR user.identityParentId IS NOT NULL
|
||||
RETURN user.displayName, user.odataType, user.agentIdentityBlueprintId,
|
||||
user.identityParentId, user.identityParentType
|
||||
```
|
||||
@@ -14,6 +14,7 @@ Post-processing steps run after all resources and graph objects have been ingest
|
||||
| [Set ARM Eligible Properties](post-processing/set-arm-eligible-properties.md) | 0 | Enrich IS_RBAC_ELIGIBLE relationships |
|
||||
| [Set HasScopedRole Properties](post-processing/set-hasscopedrole-properties.md) | 0 | Enrich HAS_SCOPED_ROLE relationships |
|
||||
| [Deduplicate Container Registry](post-processing/deduplicate-container-registry.md) | 1000 | Merge container registries by login server |
|
||||
| [Deduplicate Key Vault by URI](post-processing/deduplicate-keyvault-by-uri.md) | 1000 | Merge Key Vault nodes by vault URI |
|
||||
| [Deduplicate Graph Applications](post-processing/deduplicate-graph-applications.md) | 1000 | Merge graph applications by app ID |
|
||||
| [Deduplicate Graph Roles](post-processing/deduplicate-graph-roles.md) | 1000 | Merge graph roles by role template ID |
|
||||
| [Deduplicate Service Principals](post-processing/deduplicate-service-principals.md) | 1000 | Merge service principals by app ID |
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Deduplicate Key Vault by URI
|
||||
|
||||
Merges duplicate KeyVault nodes that share the same `vaultUri`.
|
||||
|
||||
**Priority:** 1000
|
||||
|
||||
## Details
|
||||
|
||||
When multiple KeyVault nodes have the same `vaultUri`, this step merges them into a single node, combining properties and relationships. The surviving `vaultUri` is normalized to lowercase so later lookups stay consistent.
|
||||
|
||||
## Cypher
|
||||
|
||||
```cypher
|
||||
CALL apoc.periodic.iterate(
|
||||
"
|
||||
MATCH (n:KeyVault)
|
||||
WHERE n.vaultUri IS NOT NULL
|
||||
WITH toLower(n.vaultUri) AS lid, collect(n) AS ns
|
||||
WHERE size(ns) > 1
|
||||
RETURN lid, ns
|
||||
",
|
||||
"
|
||||
WITH lid, ns
|
||||
WITH
|
||||
head(ns) AS keep,
|
||||
tail(ns) AS dups,
|
||||
lid
|
||||
UNWIND dups AS d
|
||||
CALL apoc.refactor.mergeNodes(
|
||||
[keep, d],
|
||||
{properties:'combine', mergeRels:true}
|
||||
)
|
||||
YIELD node
|
||||
SET keep.vaultUri = lid
|
||||
RETURN 1
|
||||
",
|
||||
{batchSize: 1000, parallel: false}
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
# Merge VMs by vmId
|
||||
|
||||
Merges duplicate `VirtualMachine` nodes that share the same `vmId`.
|
||||
|
||||
**Priority:** 1000
|
||||
|
||||
## Details
|
||||
|
||||
Restore point collections can introduce VM placeholders keyed by `vmId`. This step merges those placeholders with full VM nodes so relationships resolve to a single VM identity.
|
||||
|
||||
## Cypher
|
||||
|
||||
```cypher
|
||||
CALL apoc.periodic.iterate(
|
||||
"
|
||||
MATCH (n:VirtualMachine)
|
||||
WHERE n.vmId IS NOT NULL
|
||||
WITH toLower(n.vmId) AS lid, collect(n) AS ns
|
||||
WHERE size(ns) > 1
|
||||
RETURN lid, ns
|
||||
",
|
||||
"
|
||||
WITH lid, ns
|
||||
WITH
|
||||
head(ns) AS keep,
|
||||
tail(ns) AS dups,
|
||||
lid
|
||||
UNWIND dups AS d
|
||||
CALL apoc.refactor.mergeNodes(
|
||||
[keep, d],
|
||||
{properties:'combine', mergeRels:true}
|
||||
)
|
||||
YIELD node
|
||||
SET keep.vmId = lid
|
||||
RETURN 1
|
||||
",
|
||||
{batchSize: 1000, parallel: false}
|
||||
)
|
||||
```
|
||||
+4
-1
@@ -30,6 +30,8 @@ Get up and running with Cirro in minutes. Install, collect data, ingest into Neo
|
||||
|
||||
- :material-cloud-outline: **Multi-source Inputs**: Collect from supported cloud, identity, and network APIs
|
||||
- :material-account-group: **Relationship Data**: Capture principals, resources, memberships, and trust links
|
||||
- :material-account-search: **User Enrichment**: Collect user managers and password-change timestamps
|
||||
- :material-account-key: **Restricted Guest Collection**: Discover accessible application relationships from selected application IDs
|
||||
- :material-puzzle: **Extensible Modules**: Add new collectors through feature-based platform modules
|
||||
|
||||
=== "Authentication"
|
||||
@@ -47,6 +49,7 @@ Get up and running with Cirro in minutes. Install, collect data, ingest into Neo
|
||||
|
||||
- :simple-neo4j: **Neo4j Ingestion**: Load collected data into a graph database for analysis
|
||||
- :material-graph-outline: **Schema Mapping**: Transform platform data into consistent nodes and edges
|
||||
- :material-application: **Application Context**: Analyze creator relationships, agent identity properties, and published permission scopes
|
||||
- :material-export-variant: **Export Support**: Export graph data for downstream tooling
|
||||
|
||||
=== "Schema Extensibility"
|
||||
@@ -84,4 +87,4 @@ Strengthen platform security posture by analyzing:
|
||||
- Configuration hardening
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -24,6 +24,7 @@ cirro graph ingest --type <platform> --file <data-file> [options]
|
||||
- `--user`, `-u`: Database username (default: `neo4j`)
|
||||
- `--password`, `-p`: Database password (default: `password`)
|
||||
- `--db-name`, `-d`: Database name (default: `neo4j`)
|
||||
- `--dry-run`: Preview what would be ingested without writing any data to the graph database
|
||||
- `--debug`: Enable debug logging
|
||||
|
||||
### Supported Database Schemes
|
||||
@@ -58,8 +59,36 @@ cirro graph ingest \
|
||||
--type az \
|
||||
--file cirro_output.db \
|
||||
--server bolt+s://secure-server:7687
|
||||
|
||||
# Dry-run: preview Azure ingestion and see unimplemented resource types
|
||||
cirro graph ingest --type az --file cirro_output.db --dry-run
|
||||
|
||||
# Dry-run: preview Tailscale ingestion
|
||||
cirro graph ingest --type ts --file cirro_ts_socket.json --dry-run
|
||||
```
|
||||
|
||||
### Restricted guest results
|
||||
|
||||
Ingest restricted guest output with the normal Azure ingestion command:
|
||||
|
||||
```bash
|
||||
cirro graph ingest --type az --file cirro_restricted.db --dry-run
|
||||
cirro graph ingest --type az --file cirro_restricted.db
|
||||
```
|
||||
|
||||
### Dry-Run Mode
|
||||
|
||||
The `--dry-run` flag lets you preview an ingestion without writing any data to the graph database.
|
||||
This is useful for validating a data file or auditing spec coverage before a full ingest.
|
||||
|
||||
When `--dry-run` is specified, Cirro will:
|
||||
|
||||
- Log each spec and the number of rows that **would** be processed
|
||||
- Skip all Neo4j writes (no ingest or post-processing queries are executed)
|
||||
- Skip transaction finalization
|
||||
- Print the list of post-processing specs that would run, in priority order
|
||||
- **Azure only**: compare the resource types present in the input data against all implemented specs and report any resource types that have no spec (i.e., would be silently skipped during a real ingest)
|
||||
|
||||
!!! warning "Security Notice"
|
||||
|
||||
- **Passwords**: Avoid default credentials in production
|
||||
@@ -113,4 +142,4 @@ docker-compose up -d
|
||||
This starts Neo4j with:
|
||||
- Web UI at `http://localhost:7477`
|
||||
- Bolt protocol at `bolt://localhost:7688`
|
||||
- Default credentials: `neo4j/password`
|
||||
- Default credentials: `neo4j/password`
|
||||
+35
-1
@@ -46,7 +46,41 @@ cirro collect ts <auth-method> --output-path cirro_ts_socket.json
|
||||
cirro collect az azcli --debug
|
||||
```
|
||||
|
||||
## Restricted Guest Collection
|
||||
|
||||
Use `--restricted-guest` to collect Microsoft Graph data as a restricted Entra ID guest. These options are available with all Azure authentication commands:
|
||||
|
||||
| Option | Usage |
|
||||
| --- | --- |
|
||||
| `--restricted-guest` | Enable restricted guest collection using the default application IDs. |
|
||||
| `--restricted-appids <GUID> ...` | Replace the default application/client IDs. Accepts comma-separated, space-separated or repeated values. |
|
||||
| `--restricted-guest-msapps` | Use the Microsoft application catalog. Prompts for confirmation before collection. |
|
||||
|
||||
`--restricted-appids` and `--restricted-guest-msapps` both require `--restricted-guest` and cannot be combined.
|
||||
|
||||
```bash
|
||||
# Use the default application IDs
|
||||
cirro collect az azcli --tenant-id <tenant-id> --mode graph --restricted-guest \
|
||||
--output-path cirro_restricted.db
|
||||
|
||||
# Use an existing Microsoft Graph token
|
||||
cirro collect az access-token --token <GRAPH_ACCESS_TOKEN> --restricted-guest \
|
||||
--output-path cirro_restricted.db
|
||||
|
||||
# Use a specific application ID
|
||||
cirro collect az azcli --tenant-id <tenant-id> --mode graph --restricted-guest \
|
||||
--restricted-appids <APP_ID>
|
||||
|
||||
# Use the Microsoft application catalog
|
||||
cirro collect az azcli --tenant-id <tenant-id> --mode graph \
|
||||
--restricted-guest --restricted-guest-msapps
|
||||
```
|
||||
|
||||
Use `--mode graph` for Graph-only collection or `--mode both` to include ARM. Restricted guest options are ignored in `--mode arm`. With `access-token`, supply a Microsoft Graph token and omit `--mode`.
|
||||
|
||||
Normal Graph selection flags such as `--users`, `--no-default`, `--caps` and `--graph-pim` do not apply in restricted guest mode. Ingest the output using the normal [Azure ingestion command](cirro-graph.md#restricted-guest-results).
|
||||
|
||||
## Notes
|
||||
|
||||
- Ensure you have appropriate permissions for your target environment.
|
||||
- Debug logging can include sensitive values; handle output carefully.
|
||||
- Debug logging can include sensitive values; handle output carefully.
|
||||
@@ -41,6 +41,15 @@ cirro collect az client-secret \
|
||||
--tenant-id <TENANT_ID>
|
||||
```
|
||||
|
||||
### Collect as a restricted guest
|
||||
|
||||
```bash
|
||||
cirro collect az azcli --tenant-id <TENANT_ID> --mode graph \
|
||||
--restricted-guest --output-path cirro_restricted.db
|
||||
```
|
||||
|
||||
See [restricted guest options](cirro.md#restricted-guest-collection) to specify application IDs or use the Microsoft application catalog. Ingest `cirro_restricted.db` with the command below, replacing the input filename.
|
||||
|
||||
## Ingest into Graph Database
|
||||
|
||||
Neo4j is supported as the graph database backend. Set up your database before ingesting cloud data. There is a docker-compose file in the [tools](https://github.com/bishopfox/cirro/tools) directory to assist with containerized database setup.
|
||||
@@ -63,6 +72,6 @@ cirro graph ingest --type az --file cirro_output.db \
|
||||
After completing the quick start:
|
||||
|
||||
1. **Explore Your Data**: Use your graph database's query interface to explore the collected data
|
||||
2. **Learn Query Patterns**: Check out our [dashboard examples](../analysis/dashboard.md) for common analysis patterns
|
||||
2. **Learn Query Patterns**: Explore [user and manager queries](../analysis/graph-nodes/graph-user.md#examples) and [service principal relationships](../analysis/graph-nodes/graph-service-principal.md#examples)
|
||||
3. **Set Up Visualization**: Configure dashboards and visualizations for your specific use case
|
||||
4. **Advanced Features**: Explore data enrichment and custom collection options
|
||||
4. **Advanced Features**: Explore [collection options](cirro.md) and [application metadata](../analysis/graph-nodes/graph-application.md)
|
||||
-330
@@ -1,330 +0,0 @@
|
||||
# yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json
|
||||
|
||||
site_name: Cirro Documentation
|
||||
site_url: https://cirro.dev
|
||||
repo_url: https://github.com/bishopfox/cirro
|
||||
theme:
|
||||
name: material
|
||||
logo: assets/images/favicon.png
|
||||
icon:
|
||||
repo: fontawesome/brands/github-alt
|
||||
font:
|
||||
code: Roboto Mono
|
||||
palette:
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
primary: black
|
||||
accent: light blue
|
||||
toggle:
|
||||
icon: material/weather-night
|
||||
name: Switch to dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
primary: black
|
||||
accent: light blue
|
||||
toggle:
|
||||
icon: material/weather-sunny
|
||||
name: Switch to light mode
|
||||
features:
|
||||
- navigation.indexes
|
||||
- navigation.instant
|
||||
- navigation.instant.prefetch
|
||||
- navigation.path
|
||||
- navigation.sections
|
||||
- navigation.tabs
|
||||
- navigation.tabs.sticky
|
||||
- search.suggest
|
||||
- toc.integrate
|
||||
markdown_extensions:
|
||||
- attr_list
|
||||
- md_in_html
|
||||
- admonition
|
||||
- pymdownx.details
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Architecture: architecture.md
|
||||
- Usage:
|
||||
- Quick Start: usage/quick-start.md
|
||||
- Collection: usage/cirro.md
|
||||
- Graph Operations: usage/cirro-graph.md
|
||||
- Analysis:
|
||||
- Overview: analysis/cirro-graph-analysis.md
|
||||
- Nodes and Edges:
|
||||
- Graph Nodes:
|
||||
- analysis/graph-nodes.md
|
||||
- analysis/graph-nodes/graph-org.md
|
||||
- analysis/graph-nodes/graph-policy.md
|
||||
- analysis/graph-nodes/graph-user.md
|
||||
- analysis/graph-nodes/graph-group.md
|
||||
- analysis/graph-nodes/graph-application.md
|
||||
- analysis/graph-nodes/graph-service-principal.md
|
||||
- analysis/graph-nodes/graph-device.md
|
||||
- analysis/graph-nodes/graph-role.md
|
||||
- analysis/graph-nodes/graph-administrative-unit.md
|
||||
- analysis/graph-nodes/graph-named-location.md
|
||||
- analysis/graph-nodes/graph-conditional-access-policy.md
|
||||
- analysis/graph-nodes/graph-eligible-role-assignment.md
|
||||
- ARM Nodes:
|
||||
- analysis/arm-nodes.md
|
||||
- App:
|
||||
- analysis/arm-nodes/app/app-builder.md
|
||||
- analysis/arm-nodes/app/app-managed-environment.md
|
||||
- analysis/arm-nodes/app/container-app.md
|
||||
- Automation:
|
||||
- analysis/arm-nodes/automation/automation-account.md
|
||||
- analysis/arm-nodes/automation/automation-runbook.md
|
||||
- Azure Arc:
|
||||
- analysis/arm-nodes/azurearc/arc-sql-server.md
|
||||
- analysis/arm-nodes/azurearc/arc-sql-database.md
|
||||
- Azure Active Directory:
|
||||
- analysis/arm-nodes/azureactivedirectory/b2c-directory.md
|
||||
- Azure Core:
|
||||
- analysis/arm-nodes/azure-core/tenant.md
|
||||
- analysis/arm-nodes/azure-core/subscription.md
|
||||
- analysis/arm-nodes/azure-core/resource-group.md
|
||||
- analysis/arm-nodes/azure-core/management-group.md
|
||||
- CDN:
|
||||
- analysis/arm-nodes/cdn/cdn-profile.md
|
||||
- analysis/arm-nodes/cdn/afd-endpoint.md
|
||||
- Cognitive Services:
|
||||
- analysis/arm-nodes/cognitiveservices/cognitive-services-account.md
|
||||
- Communication:
|
||||
- analysis/arm-nodes/communication/communication-services.md
|
||||
- analysis/arm-nodes/communication/email-service.md
|
||||
- analysis/arm-nodes/communication/email-service-domain.md
|
||||
- Compute:
|
||||
- analysis/arm-nodes/compute/virtual-machine.md
|
||||
- analysis/arm-nodes/compute/virtual-machine-scale-set.md
|
||||
- analysis/arm-nodes/compute/gallery.md
|
||||
- analysis/arm-nodes/compute/gallery-app.md
|
||||
- analysis/arm-nodes/compute/gallery-app-version.md
|
||||
- analysis/arm-nodes/compute/gallery-image.md
|
||||
- analysis/arm-nodes/compute/gallery-image-version.md
|
||||
- analysis/arm-nodes/compute/vm-application.md
|
||||
- analysis/arm-nodes/compute/disk.md
|
||||
- analysis/arm-nodes/compute/disk-encryption-set.md
|
||||
- analysis/arm-nodes/compute/snapshot.md
|
||||
- analysis/arm-nodes/compute/availability-set.md
|
||||
- analysis/arm-nodes/compute/restore-point-collection.md
|
||||
- analysis/arm-nodes/compute/ssh-public-key.md
|
||||
- analysis/arm-nodes/compute/vm-extension.md
|
||||
- analysis/arm-nodes/compute/compute-image.md
|
||||
- analysis/arm-nodes/compute/vm-run-command.md
|
||||
- Container:
|
||||
- analysis/arm-nodes/container/container-registry.md
|
||||
- analysis/arm-nodes/container/container-group.md
|
||||
- Fabric:
|
||||
- analysis/arm-nodes/fabric/fabric-capacity.md
|
||||
- Dashboard:
|
||||
- analysis/arm-nodes/dashboard/grafana-dashboard.md
|
||||
- Data Factory:
|
||||
- analysis/arm-nodes/datafactory/data-factory.md
|
||||
- Event Grid:
|
||||
- analysis/arm-nodes/eventgrid/event-grid-topic.md
|
||||
- analysis/arm-nodes/eventgrid/event-grid-system-topic.md
|
||||
- Event Hub:
|
||||
- analysis/arm-nodes/eventhub/eventhub-namespace.md
|
||||
- Hybrid Compute:
|
||||
- analysis/arm-nodes/hybridcompute/hybrid-machine.md
|
||||
- analysis/arm-nodes/hybridcompute/hybrid-ip-address.md
|
||||
- analysis/arm-nodes/hybridcompute/hybrid-extension.md
|
||||
- Identity:
|
||||
- analysis/arm-nodes/identity/user-assigned-identity.md
|
||||
- Insights:
|
||||
- analysis/arm-nodes/insights/activity-log-alert.md
|
||||
- Key Vault:
|
||||
- analysis/arm-nodes/keyvault/keyvault.md
|
||||
- Network:
|
||||
- analysis/arm-nodes/network/bastion-host.md
|
||||
- analysis/arm-nodes/network/dns-zone.md
|
||||
- analysis/arm-nodes/network/ipconfig.md
|
||||
- analysis/arm-nodes/network/network-interface.md
|
||||
- analysis/arm-nodes/network/nsg.md
|
||||
- analysis/arm-nodes/network/nsg-rule.md
|
||||
- analysis/arm-nodes/network/expressroute-circuit.md
|
||||
- analysis/arm-nodes/network/private-dns-zone.md
|
||||
- analysis/arm-nodes/network/private-endpoint.md
|
||||
- analysis/arm-nodes/network/public-ip-address.md
|
||||
- analysis/arm-nodes/network/route-table.md
|
||||
- analysis/arm-nodes/network/subnet.md
|
||||
- analysis/arm-nodes/network/virtual-network.md
|
||||
- NetApp:
|
||||
- analysis/arm-nodes/netapp/netapp-account.md
|
||||
- analysis/arm-nodes/netapp/netapp-capacity-pool.md
|
||||
- analysis/arm-nodes/netapp/netapp-volume.md
|
||||
- analysis/arm-nodes/netapp/netapp-snapshot-policy.md
|
||||
- Recovery Services:
|
||||
- analysis/arm-nodes/recoveryservices/recovery-vault.md
|
||||
- SQL:
|
||||
- analysis/arm-nodes/sql/sql-server.md
|
||||
- analysis/arm-nodes/sql/sql-database.md
|
||||
- analysis/arm-nodes/sql/sql-virtual-machine.md
|
||||
- SaaS:
|
||||
- analysis/arm-nodes/saas/saas-resource.md
|
||||
- Storage:
|
||||
- analysis/arm-nodes/storage/storage-account.md
|
||||
- analysis/arm-nodes/storage/classic-storage-account.md
|
||||
- Synapse:
|
||||
- analysis/arm-nodes/synapse/synapse-workspace.md
|
||||
- Visual Studio:
|
||||
- analysis/arm-nodes/visualstudio/visualstudio-account.md
|
||||
- API Management:
|
||||
- analysis/arm-nodes/apimanagement/apim-service.md
|
||||
- App Configuration:
|
||||
- analysis/arm-nodes/appconfiguration/appconfig-store.md
|
||||
- Cache:
|
||||
- analysis/arm-nodes/cache/redis-cache.md
|
||||
- CosmosDB:
|
||||
- analysis/arm-nodes/documentdb/cosmosdb-account.md
|
||||
- Machine Learning:
|
||||
- analysis/arm-nodes/machinelearning/ml-workspace.md
|
||||
- Palo Alto:
|
||||
- analysis/arm-nodes/paloalto/pan-cloudngfw-firewall.md
|
||||
- analysis/arm-nodes/paloalto/pan-cloudngfw-localrulestack.md
|
||||
- Purview:
|
||||
- analysis/arm-nodes/purview/purview-account.md
|
||||
- Service Bus:
|
||||
- analysis/arm-nodes/servicebus/servicebus-namespace.md
|
||||
- SignalR:
|
||||
- analysis/arm-nodes/signalr/signalr.md
|
||||
- Web:
|
||||
- analysis/arm-nodes/web/server-farm.md
|
||||
- analysis/arm-nodes/web/web-site.md
|
||||
- analysis/arm-nodes/web/web-certificate.md
|
||||
- analysis/arm-nodes/web/static-website.md
|
||||
- Additional Nodes:
|
||||
- analysis/additional-nodes.md
|
||||
- analysis/additional-nodes/bastion-ip-config.md
|
||||
- analysis/additional-nodes/certificate.md
|
||||
- analysis/additional-nodes/client-secret.md
|
||||
- analysis/additional-nodes/container.md
|
||||
- analysis/additional-nodes/container-image.md
|
||||
- analysis/additional-nodes/env-var.md
|
||||
- analysis/additional-nodes/custom-dns-config.md
|
||||
- analysis/additional-nodes/expressroute-circuit-peering.md
|
||||
- analysis/additional-nodes/federated-identity-credential.md
|
||||
- analysis/additional-nodes/graph-app-role.md
|
||||
- analysis/additional-nodes/hybrid-extension.md
|
||||
- analysis/additional-nodes/hybrid-ip-address.md
|
||||
- analysis/additional-nodes/ip-config.md
|
||||
- analysis/additional-nodes/key-vault-secret.md
|
||||
- analysis/additional-nodes/netapp-account-ad-domain.md
|
||||
- analysis/additional-nodes/netapp-volume-mount.md
|
||||
- analysis/additional-nodes/network-gateway.md
|
||||
- analysis/additional-nodes/network-peering.md
|
||||
- analysis/additional-nodes/network-route.md
|
||||
- analysis/additional-nodes/nsg-rule.md
|
||||
- analysis/additional-nodes/private-endpoint-connection.md
|
||||
- analysis/additional-nodes/private-link-service-connection.md
|
||||
- analysis/additional-nodes/registry-credential.md
|
||||
- analysis/additional-nodes/runbook-parameter.md
|
||||
- analysis/additional-nodes/service-principal-endpoint.md
|
||||
- analysis/additional-nodes/storage-account-key.md
|
||||
- analysis/additional-nodes/storage-account-dfs.md
|
||||
- analysis/additional-nodes/subnet.md
|
||||
- analysis/additional-nodes/verified-domain.md
|
||||
- analysis/additional-nodes/vm-extension.md
|
||||
- analysis/additional-nodes/vm-run-command.md
|
||||
- analysis/additional-nodes/vm-run-command-parameter.md
|
||||
- Tailscale Nodes:
|
||||
- analysis/tailscale-nodes.md
|
||||
- analysis/tailscale-nodes/ts-user.md
|
||||
- analysis/tailscale-nodes/ts-device.md
|
||||
- analysis/tailscale-nodes/ts-tailnet.md
|
||||
- Edges:
|
||||
- analysis/edges/assigned-approle.md
|
||||
- analysis/edges/associated-with.md
|
||||
- analysis/edges/authenticates.md
|
||||
- analysis/edges/can-access-registry.md
|
||||
- analysis/edges/contains.md
|
||||
- analysis/edges/contains-image.md
|
||||
- analysis/edges/delegated-to-client.md
|
||||
- analysis/edges/encrypted-by.md
|
||||
- analysis/edges/has-alert.md
|
||||
- analysis/edges/has-application.md
|
||||
- analysis/edges/has-approle.md
|
||||
- analysis/edges/has-config.md
|
||||
- analysis/edges/has-container.md
|
||||
- analysis/edges/has-db.md
|
||||
- analysis/edges/has-dfs-filesystem.md
|
||||
- analysis/edges/has-dns-config.md
|
||||
- analysis/edges/has-envvar.md
|
||||
- analysis/edges/has-endpoint.md
|
||||
- analysis/edges/has-extension.md
|
||||
- analysis/edges/has-gallery-image.md
|
||||
- analysis/edges/has-identity.md
|
||||
- analysis/edges/has-ip.md
|
||||
- analysis/edges/has-linked-domain.md
|
||||
- analysis/edges/has-nic.md
|
||||
- analysis/edges/has-nsg.md
|
||||
- analysis/edges/has-oauth-grant.md
|
||||
- analysis/edges/has-parameter.md
|
||||
- analysis/edges/has-peering.md
|
||||
- analysis/edges/has-policy.md
|
||||
- analysis/edges/has-private-endpoint.md
|
||||
- analysis/edges/has-restorepoint.md
|
||||
- analysis/edges/has-role.md
|
||||
- analysis/edges/has-rulestack.md
|
||||
- analysis/edges/has-route.md
|
||||
- analysis/edges/has-route-table.md
|
||||
- analysis/edges/has-rule.md
|
||||
- analysis/edges/has-run-command.md
|
||||
- analysis/edges/has-runbook.md
|
||||
- analysis/edges/has-scoped-role.md
|
||||
- analysis/edges/has-secret.md
|
||||
- analysis/edges/has-snapshot.md
|
||||
- analysis/edges/has-source.md
|
||||
- analysis/edges/has-sql-vm.md
|
||||
- analysis/edges/has-user-grant.md
|
||||
- analysis/edges/has-version.md
|
||||
- analysis/edges/has-vmapp.md
|
||||
- analysis/edges/hosts.md
|
||||
- analysis/edges/is-deployed.md
|
||||
- analysis/edges/is-rbac-eligible.md
|
||||
- analysis/edges/is-role-eligible.md
|
||||
- analysis/edges/manages.md
|
||||
- analysis/edges/member-of.md
|
||||
- analysis/edges/mounts.md
|
||||
- analysis/edges/owns.md
|
||||
- analysis/edges/deploys.md
|
||||
- analysis/edges/peers-with.md
|
||||
- analysis/edges/references-package.md
|
||||
- analysis/edges/registered-owner.md
|
||||
- analysis/edges/registered-user.md
|
||||
- analysis/edges/represented-by.md
|
||||
- analysis/edges/same-user.md
|
||||
- analysis/edges/used-by.md
|
||||
- analysis/edges/uses-key-vault.md
|
||||
- analysis/edges/uses-storage.md
|
||||
- analysis/edges/uses-storage-dfs.md
|
||||
- analysis/edges/verified-domain.md
|
||||
- Dashboard: analysis/dashboard.md
|
||||
- Post-Processing:
|
||||
- analysis/post-processing.md
|
||||
- analysis/post-processing/normalize-ids.md
|
||||
- analysis/post-processing/deduplicate-nodes.md
|
||||
- analysis/post-processing/deduplicate-relationships.md
|
||||
- analysis/post-processing/deduplicate-container-registry.md
|
||||
- analysis/post-processing/deduplicate-graph-applications.md
|
||||
- analysis/post-processing/deduplicate-graph-roles.md
|
||||
- analysis/post-processing/deduplicate-service-principals.md
|
||||
- analysis/post-processing/deduplicate-storage.md
|
||||
- analysis/post-processing/fix-dfs-without-id.md
|
||||
- analysis/post-processing/link-tsuser-to-graphuser.md
|
||||
- analysis/post-processing/remove-memberof-graphrole.md
|
||||
- analysis/post-processing/set-approle-properties.md
|
||||
- analysis/post-processing/set-arm-eligible-properties.md
|
||||
- analysis/post-processing/set-hasscopedrole-properties.md
|
||||
- FAQ: faq.md
|
||||
- Developer Notes: developer-notes.md
|
||||
- Getting Help: getting-help.md
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::collect::azure;
|
||||
use crate::collect::azure::cli_utils::{validate_tenant_id, validate_uuid};
|
||||
use crate::collect::logger::setup_logger;
|
||||
use crate::errors::CirroError;
|
||||
|
||||
@@ -50,7 +51,7 @@ pub struct CommonAuthArgs {
|
||||
pub new_state: bool,
|
||||
|
||||
/// Filter ARM collection to specific subscription IDs (comma-separated or repeated)
|
||||
#[arg(long = "subscription-ids", value_name = "ID", value_delimiter = ',', num_args = 1..)]
|
||||
#[arg(long = "subscription-ids", value_name = "ID", value_delimiter = ',', num_args = 1.., value_parser = validate_uuid)]
|
||||
pub subscription_ids: Option<Vec<String>>,
|
||||
|
||||
/// Limit subscription concurrency to avoid throttling (default: 5)
|
||||
@@ -77,7 +78,7 @@ pub struct AccessTokenAuthArgs {
|
||||
pub new_state: bool,
|
||||
|
||||
/// Filter ARM collection to specific subscription IDs (comma-separated or repeated)
|
||||
#[arg(long = "subscription-ids", value_name = "ID", value_delimiter = ',', num_args = 1..)]
|
||||
#[arg(long = "subscription-ids", value_name = "ID", value_delimiter = ',', num_args = 1.., value_parser = validate_uuid)]
|
||||
pub subscription_ids: Option<Vec<String>>,
|
||||
|
||||
/// Limit subscription concurrency to avoid throttling (default: 5)
|
||||
@@ -87,6 +88,30 @@ pub struct AccessTokenAuthArgs {
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct OptionEnumFlags {
|
||||
/// Replace normal Graph enumeration with restricted-guest application-role traversal
|
||||
#[arg(long = "restricted-guest", action = clap::ArgAction::SetTrue)]
|
||||
pub restricted_guest: bool,
|
||||
|
||||
/// Override restricted-guest application IDs (space/comma-separated or repeated)
|
||||
#[arg(
|
||||
long = "restricted-appids",
|
||||
value_name = "GUID",
|
||||
value_delimiter = ',',
|
||||
num_args = 1..,
|
||||
value_parser = validate_uuid,
|
||||
requires = "restricted_guest"
|
||||
)]
|
||||
pub restricted_appids: Vec<String>,
|
||||
|
||||
/// Enumerate the full Microsoft application catalog (warning: large enumeration!)
|
||||
#[arg(
|
||||
long = "restricted-guest-msapps",
|
||||
action = clap::ArgAction::SetTrue,
|
||||
requires = "restricted_guest",
|
||||
conflicts_with = "restricted_appids"
|
||||
)]
|
||||
pub restricted_guest_msapps: bool,
|
||||
|
||||
/// Skip all default graph enumerations; only run explicitly enabled options (e.g. --pim, --caps)
|
||||
#[arg(long = "no-default", action = clap::ArgAction::SetTrue)]
|
||||
pub no_default: bool,
|
||||
@@ -205,11 +230,11 @@ pub enum AzureCommands {
|
||||
/// Authenticate using Azure CLI
|
||||
Azcli {
|
||||
/// Tenant ID
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, value_parser = validate_tenant_id)]
|
||||
tenant_id: Option<String>,
|
||||
|
||||
/// Subscription ID for az login authentication context
|
||||
#[arg(short = 's', long = "login-subscription-id")]
|
||||
#[arg(short = 's', long = "login-subscription-id", value_parser = validate_uuid)]
|
||||
login_subscription_id: Option<String>,
|
||||
|
||||
#[clap(flatten)]
|
||||
@@ -221,7 +246,7 @@ pub enum AzureCommands {
|
||||
/// Authenticate using a client secret
|
||||
ClientSecret {
|
||||
/// Client ID
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, value_parser = validate_uuid)]
|
||||
client_id: String,
|
||||
|
||||
/// Client secret
|
||||
@@ -229,7 +254,7 @@ pub enum AzureCommands {
|
||||
client_secret: String,
|
||||
|
||||
/// Tenant ID
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, value_parser = validate_tenant_id)]
|
||||
tenant_id: String,
|
||||
|
||||
#[clap(flatten)]
|
||||
@@ -241,7 +266,7 @@ pub enum AzureCommands {
|
||||
/// Authenticate using a client certificate
|
||||
ClientCert {
|
||||
/// Client ID
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, value_parser = validate_uuid)]
|
||||
client_id: String,
|
||||
|
||||
/// Path to the client certificate file (PEM format)
|
||||
@@ -249,7 +274,7 @@ pub enum AzureCommands {
|
||||
client_certificate: PathBuf,
|
||||
|
||||
/// Tenant ID
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, value_parser = validate_tenant_id)]
|
||||
tenant_id: String,
|
||||
|
||||
#[clap(flatten)]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use uuid::Uuid;
|
||||
|
||||
static DOMAIN_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"(?i)^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$")
|
||||
.expect("valid domain regex")
|
||||
});
|
||||
|
||||
/// Validates that the input is a valid UUID.
|
||||
pub fn validate_uuid(input: &str) -> Result<String, String> {
|
||||
let _ = Uuid::parse_str(input).map_err(|_| "must be a valid UUID".to_string())?;
|
||||
Ok(input.to_string())
|
||||
}
|
||||
|
||||
/// Validates that the input is either a valid UUID or a valid domain name.
|
||||
pub fn validate_tenant_id(input: &str) -> Result<String, String> {
|
||||
if validate_uuid(input).is_ok() || DOMAIN_RE.is_match(input) {
|
||||
Ok(input.to_string())
|
||||
} else {
|
||||
Err("must be a valid UUID or domain".into())
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::collect::azure::cli::{AzureCloud, EnumerationMode, OptionEnumFlags};
|
||||
use crate::collect::azure::collectors::arm;
|
||||
use crate::collect::azure::collectors::graph;
|
||||
use crate::collect::azure::collectors::restricted_guest;
|
||||
use crate::collect::azure::context::CloudEndpoints;
|
||||
use crate::collect::azure::context::CollectorContext;
|
||||
use crate::collect::azure::credentials::azcli::AzureCliCredential;
|
||||
@@ -10,7 +11,8 @@ use crate::errors::CirroError;
|
||||
|
||||
use futures;
|
||||
use log::{debug, error, info, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::{self, Write};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
@@ -34,8 +36,37 @@ impl DerefMut for Collector {
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_restricted_guest_msapps() -> Result<bool, CirroError> {
|
||||
let application_count = restricted_guest::microsoft_application_count();
|
||||
warn!(
|
||||
"--restricted-guest-msapps will enumerate the full catalog of {} Microsoft applications and may generate many Graph requests",
|
||||
application_count
|
||||
);
|
||||
print!("Continue with Microsoft application enumeration? [y/N]: ");
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut response = String::new();
|
||||
io::stdin().read_line(&mut response)?;
|
||||
|
||||
Ok(matches!(
|
||||
response.trim().to_ascii_lowercase().as_str(),
|
||||
"y" | "yes"
|
||||
))
|
||||
}
|
||||
|
||||
/// Runs the collector, performing the actual data collection
|
||||
async fn run_collector(mut collector: Collector, new_state: bool) -> Result<(), CirroError> {
|
||||
let mut seen_restricted_appids = HashSet::new();
|
||||
collector.option_enum_flags.restricted_appids =
|
||||
std::mem::take(&mut collector.option_enum_flags.restricted_appids)
|
||||
.into_iter()
|
||||
.map(|app_id| app_id.to_ascii_lowercase())
|
||||
.filter(|app_id| seen_restricted_appids.insert(app_id.clone()))
|
||||
.collect();
|
||||
|
||||
let restricted_graph_enabled = collector.option_enum_flags.restricted_guest
|
||||
&& (collector.mode == EnumerationMode::Graph || collector.mode == EnumerationMode::Both);
|
||||
|
||||
info!("Using cloud: {:?}", collector.cloud);
|
||||
info!("Using mode: {:?}", collector.mode);
|
||||
info!(
|
||||
@@ -43,6 +74,22 @@ async fn run_collector(mut collector: Collector, new_state: bool) -> Result<(),
|
||||
collector.output_path.to_path_buf().as_path()
|
||||
);
|
||||
|
||||
if collector.mode == EnumerationMode::Arm {
|
||||
if collector.option_enum_flags.restricted_guest_msapps {
|
||||
warn!("--restricted-guest-msapps is ignored because ARM-only mode is active");
|
||||
} else if collector.option_enum_flags.restricted_guest {
|
||||
info!("Restricted-guest application IDs are unused because ARM-only mode is active");
|
||||
}
|
||||
}
|
||||
|
||||
if restricted_graph_enabled
|
||||
&& collector.option_enum_flags.restricted_guest_msapps
|
||||
&& !confirm_restricted_guest_msapps()?
|
||||
{
|
||||
info!("Microsoft application enumeration cancelled");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create the database writer with bounded channel for backpressure
|
||||
const DB_CHANNEL_CAPACITY: usize = 16_384;
|
||||
let (tx, rx) = tokio::sync::mpsc::channel::<DBWriteMessage>(DB_CHANNEL_CAPACITY);
|
||||
@@ -59,7 +106,7 @@ async fn run_collector(mut collector: Collector, new_state: bool) -> Result<(),
|
||||
let (graph_state, arm_state) = {
|
||||
let db = SqliteDb::new(collector.output_path.clone());
|
||||
if new_state {
|
||||
info!("--new-state: clearing saved enumeration state");
|
||||
info!("Clearing saved enumeration state");
|
||||
db.clear_state(None)
|
||||
.map_err(|e| CirroError::DatabaseError(format!("Failed to clear state: {}", e)))?;
|
||||
(HashMap::new(), HashMap::new())
|
||||
@@ -72,7 +119,7 @@ async fn run_collector(mut collector: Collector, new_state: bool) -> Result<(),
|
||||
.filter(|(k, _)| !k.starts_with("arm:"))
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
if !graph.is_empty() {
|
||||
if !graph.is_empty() && !restricted_graph_enabled {
|
||||
info!(
|
||||
"Resuming graph enumeration for {} resource type(s)",
|
||||
graph.len()
|
||||
@@ -83,6 +130,11 @@ async fn run_collector(mut collector: Collector, new_state: bool) -> Result<(),
|
||||
rt, count, elapsed
|
||||
);
|
||||
}
|
||||
} else if !graph.is_empty() && restricted_graph_enabled {
|
||||
debug!(
|
||||
"Restricted-guest Graph enumeration starts fresh; ignoring {} normal Graph state record(s)",
|
||||
graph.len()
|
||||
);
|
||||
}
|
||||
let arm: HashMap<_, _> = all_state
|
||||
.into_iter()
|
||||
@@ -130,12 +182,19 @@ async fn run_collector(mut collector: Collector, new_state: bool) -> Result<(),
|
||||
// Create a task for Graph enumeration
|
||||
let graph_collector_clone = Arc::clone(&graph_collector);
|
||||
let graph_task = tokio::spawn(async move {
|
||||
debug!("Starting Graph API enumeration task");
|
||||
if let Err(e) = graph::enumerate_graph(graph_collector_clone, graph_state).await
|
||||
{
|
||||
let result = if graph_collector_clone.option_enum_flags.restricted_guest {
|
||||
debug!("Starting restricted-guest Graph API enumeration task");
|
||||
restricted_guest::enumerate_restricted_guest(graph_collector_clone).await
|
||||
} else {
|
||||
debug!("Starting Graph API enumeration task");
|
||||
graph::enumerate_graph(graph_collector_clone, graph_state).await
|
||||
};
|
||||
|
||||
if let Err(e) = &result {
|
||||
error!("Error collecting Graph data: {}", e);
|
||||
}
|
||||
debug!("Graph API enumeration task completed");
|
||||
result
|
||||
});
|
||||
|
||||
tasks.push(graph_task);
|
||||
@@ -162,10 +221,12 @@ async fn run_collector(mut collector: Collector, new_state: bool) -> Result<(),
|
||||
let arm_collector_clone = Arc::clone(&arm_collector);
|
||||
let arm_task = tokio::spawn(async move {
|
||||
debug!("Starting ARM API enumeration task");
|
||||
if let Err(e) = arm::enumerate_arm(arm_collector_clone, arm_state).await {
|
||||
let result = arm::enumerate_arm(arm_collector_clone, arm_state).await;
|
||||
if let Err(e) = &result {
|
||||
error!("Error collecting ARM data: {}", e);
|
||||
}
|
||||
debug!("ARM API enumeration task completed");
|
||||
result
|
||||
});
|
||||
|
||||
tasks.push(arm_task);
|
||||
@@ -179,10 +240,21 @@ async fn run_collector(mut collector: Collector, new_state: bool) -> Result<(),
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all tasks to complete
|
||||
// Wait for all tasks to complete, but keep the DB writer alive until every
|
||||
// collector has flushed its messages.
|
||||
let mut collection_errors = Vec::new();
|
||||
if !tasks.is_empty() {
|
||||
debug!("Waiting for {} enumeration tasks to complete", tasks.len());
|
||||
futures::future::join_all(tasks).await;
|
||||
for result in futures::future::join_all(tasks).await {
|
||||
match result {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(error)) => collection_errors.push(error),
|
||||
Err(error) => collection_errors.push(CirroError::ProcessingError(format!(
|
||||
"Enumeration task failed: {}",
|
||||
error
|
||||
))),
|
||||
}
|
||||
}
|
||||
info!("All enumeration tasks completed");
|
||||
}
|
||||
|
||||
@@ -198,7 +270,11 @@ async fn run_collector(mut collector: Collector, new_state: bool) -> Result<(),
|
||||
.map_err(|e| CirroError::DatabaseError(e.to_string()))?;
|
||||
|
||||
info!("Done");
|
||||
Ok(())
|
||||
match collection_errors.len() {
|
||||
0 => Ok(()),
|
||||
1 => Err(collection_errors.remove(0)),
|
||||
_ => Err(CirroError::MultipleErrors(collection_errors)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects data using Access Token credentials
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,3 +1,5 @@
|
||||
pub mod arm;
|
||||
pub mod common;
|
||||
pub mod graph;
|
||||
pub mod restricted_guest;
|
||||
mod restricted_guest_apps;
|
||||
@@ -0,0 +1,616 @@
|
||||
use super::graph::{GraphPager, graph_get_response};
|
||||
use super::restricted_guest_apps::{application_name, microsoft_apps, well_known_apps};
|
||||
use crate::collect::azure::collect::Collector;
|
||||
use crate::errors::CirroError;
|
||||
|
||||
use log::{debug, info, warn};
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Default)]
|
||||
struct CrawlStats {
|
||||
seeds_found: usize,
|
||||
seeds_missing: usize,
|
||||
seeds_blocked: usize,
|
||||
applications: usize,
|
||||
oauth_grants: usize,
|
||||
assignments: usize,
|
||||
relationship_failures: usize,
|
||||
unresolved_service_principals: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CrawlState {
|
||||
service_principals: HashMap<String, Value>,
|
||||
app_id_to_sp_id: HashMap<String, String>,
|
||||
neighbors: HashMap<String, Vec<String>>,
|
||||
unavailable_service_principals: HashSet<String>,
|
||||
probed_application_ids: HashSet<String>,
|
||||
written_grant_ids: HashSet<String>,
|
||||
pending_assignments: HashMap<String, Vec<Value>>,
|
||||
stats: CrawlStats,
|
||||
}
|
||||
|
||||
enum OptionalResponse {
|
||||
Found(Value),
|
||||
Forbidden,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
struct CollectionResponse {
|
||||
values: Vec<Value>,
|
||||
incomplete: bool,
|
||||
}
|
||||
|
||||
/// Enumerates service principals and applications through relationship surfaces
|
||||
/// that remain readable to a restricted Entra ID guest.
|
||||
pub async fn enumerate_restricted_guest(collector: Arc<Collector>) -> Result<(), CirroError> {
|
||||
let seed_apps = restricted_seed_apps(&collector);
|
||||
let mut state = CrawlState::default();
|
||||
let mut visited = HashSet::new();
|
||||
|
||||
info!(
|
||||
"Starting restricted-guest Graph enumeration with {} application ID seed(s)",
|
||||
seed_apps.len()
|
||||
);
|
||||
|
||||
for (seed_app_id, display_name) in seed_apps {
|
||||
info!("Enumerating {}: {}", seed_app_id, display_name);
|
||||
|
||||
let Some(root_id) =
|
||||
resolve_seed_service_principal(&collector, &seed_app_id, &mut state).await?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut queue = VecDeque::from([root_id]);
|
||||
|
||||
while let Some(service_principal_id) = queue.pop_front() {
|
||||
if !visited.insert(service_principal_id.clone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let neighbors =
|
||||
process_service_principal(&collector, &service_principal_id, &mut state).await?;
|
||||
|
||||
for neighbor_id in neighbors {
|
||||
if !visited.contains(&neighbor_id) {
|
||||
queue.push_back(neighbor_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Restricted-guest Graph enumeration completed: {} seed(s) found, {} missing, {} blocked; {} service principal(s), {} application(s), {} OAuth grant(s), {} app-role assignment(s), {} incomplete relationship(s), {} unresolved service principal(s)",
|
||||
state.stats.seeds_found,
|
||||
state.stats.seeds_missing,
|
||||
state.stats.seeds_blocked,
|
||||
state.service_principals.len(),
|
||||
state.stats.applications,
|
||||
state.stats.oauth_grants,
|
||||
state.stats.assignments,
|
||||
state.stats.relationship_failures,
|
||||
state.stats.unresolved_service_principals,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn microsoft_application_count() -> usize {
|
||||
microsoft_apps().len()
|
||||
}
|
||||
|
||||
fn restricted_seed_apps(collector: &Collector) -> Vec<(String, String)> {
|
||||
let configured = &collector.option_enum_flags.restricted_appids;
|
||||
if !configured.is_empty() {
|
||||
let mut seen = HashSet::new();
|
||||
return configured
|
||||
.iter()
|
||||
.map(|app_id| app_id.to_ascii_lowercase())
|
||||
.filter(|app_id| seen.insert(app_id.clone()))
|
||||
.map(|app_id| {
|
||||
let display_name = application_name(&app_id)
|
||||
.unwrap_or("Unknown application")
|
||||
.to_string();
|
||||
(app_id, display_name)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
if collector.option_enum_flags.restricted_guest_msapps {
|
||||
return microsoft_apps()
|
||||
.iter()
|
||||
.map(|(app_id, catalog_name)| {
|
||||
let display_name = application_name(app_id).unwrap_or(catalog_name).to_string();
|
||||
(app_id.clone(), display_name)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
well_known_apps()
|
||||
.iter()
|
||||
.map(|(app_id, display_name)| ((*app_id).to_string(), (*display_name).to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn resolve_seed_service_principal(
|
||||
collector: &Collector,
|
||||
app_id: &str,
|
||||
state: &mut CrawlState,
|
||||
) -> Result<Option<String>, CirroError> {
|
||||
if let Some(service_principal_id) = state.app_id_to_sp_id.get(app_id) {
|
||||
state.stats.seeds_found += 1;
|
||||
return Ok(Some(service_principal_id.clone()));
|
||||
}
|
||||
|
||||
let uri = format!("servicePrincipals(appId='{}')", app_id);
|
||||
match get_optional_object(collector, &uri).await? {
|
||||
OptionalResponse::Found(service_principal) => {
|
||||
let service_principal_id = register_service_principal(state, service_principal)?;
|
||||
state.stats.seeds_found += 1;
|
||||
debug!(
|
||||
"Resolved restricted-guest seed app ID {} to service principal {}",
|
||||
app_id, service_principal_id
|
||||
);
|
||||
Ok(Some(service_principal_id))
|
||||
}
|
||||
OptionalResponse::Forbidden => {
|
||||
state.stats.seeds_blocked += 1;
|
||||
warn!(
|
||||
"Authorization prevented alternate-key lookup for application ID {}; presence is indeterminate",
|
||||
app_id
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
OptionalResponse::NotFound => {
|
||||
state.stats.seeds_missing += 1;
|
||||
debug!(
|
||||
"No service principal was found for application ID {}",
|
||||
app_id
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_service_principal(
|
||||
collector: &Collector,
|
||||
service_principal_id: &str,
|
||||
state: &mut CrawlState,
|
||||
) -> Result<Vec<String>, CirroError> {
|
||||
if let Some(neighbors) = state.neighbors.get(service_principal_id) {
|
||||
return Ok(neighbors.clone());
|
||||
}
|
||||
if state
|
||||
.unavailable_service_principals
|
||||
.contains(service_principal_id)
|
||||
{
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
if !state.service_principals.contains_key(service_principal_id) {
|
||||
let uri = format!("servicePrincipals/{}", service_principal_id);
|
||||
match get_optional_object(collector, &uri).await? {
|
||||
OptionalResponse::Found(service_principal) => {
|
||||
register_service_principal(state, service_principal)?;
|
||||
}
|
||||
OptionalResponse::Forbidden | OptionalResponse::NotFound => {
|
||||
state
|
||||
.unavailable_service_principals
|
||||
.insert(service_principal_id.to_string());
|
||||
state.stats.unresolved_service_principals += 1;
|
||||
warn!(
|
||||
"Could not resolve discovered service principal {}; skipping its relationships",
|
||||
service_principal_id
|
||||
);
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let app_id = state
|
||||
.service_principals
|
||||
.get(service_principal_id)
|
||||
.and_then(|value| value.get("appId"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_ascii_lowercase);
|
||||
|
||||
if let Some(app_id) = app_id {
|
||||
probe_application(collector, &app_id, state).await?;
|
||||
}
|
||||
|
||||
let inbound =
|
||||
fetch_sp_relationship(collector, service_principal_id, "appRoleAssignedTo").await?;
|
||||
let outbound =
|
||||
fetch_sp_relationship(collector, service_principal_id, "appRoleAssignments").await?;
|
||||
let oauth_grants =
|
||||
fetch_sp_relationship(collector, service_principal_id, "oauth2PermissionGrants").await?;
|
||||
let owners = fetch_sp_relationship(collector, service_principal_id, "owners").await?;
|
||||
|
||||
state.stats.relationship_failures += usize::from(inbound.incomplete)
|
||||
+ usize::from(outbound.incomplete)
|
||||
+ usize::from(oauth_grants.incomplete)
|
||||
+ usize::from(owners.incomplete);
|
||||
|
||||
let mut neighbors = Vec::new();
|
||||
let mut seen_neighbors = HashSet::new();
|
||||
let mut inbound_assignments = Vec::new();
|
||||
|
||||
for assignment in &inbound.values {
|
||||
if let Some(normalized) = normalize_assignment(assignment, None) {
|
||||
push_unique_assignment(&mut inbound_assignments, normalized);
|
||||
state.stats.assignments += 1;
|
||||
}
|
||||
|
||||
let is_service_principal = assignment
|
||||
.get("principalType")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|value| value.eq_ignore_ascii_case("ServicePrincipal"));
|
||||
if is_service_principal
|
||||
&& let Some(principal_id) = assignment
|
||||
.get("principalId")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_ascii_lowercase)
|
||||
{
|
||||
push_unique_neighbor(&mut neighbors, &mut seen_neighbors, principal_id);
|
||||
}
|
||||
}
|
||||
|
||||
let mut cached_targets_to_rewrite = HashSet::new();
|
||||
for assignment in &outbound.values {
|
||||
let Some(resource_id) = assignment
|
||||
.get("resourceId")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_ascii_lowercase)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
push_unique_neighbor(&mut neighbors, &mut seen_neighbors, resource_id.clone());
|
||||
|
||||
if let Some(normalized) = normalize_assignment(assignment, Some(service_principal_id)) {
|
||||
let pending = state
|
||||
.pending_assignments
|
||||
.entry(resource_id.clone())
|
||||
.or_default();
|
||||
push_unique_assignment(pending, normalized);
|
||||
state.stats.assignments += 1;
|
||||
if state.service_principals.contains_key(&resource_id) {
|
||||
cached_targets_to_rewrite.insert(resource_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut grant_rows = Vec::new();
|
||||
for grant in oauth_grants.values {
|
||||
if let Some(resource_id) = grant
|
||||
.get("resourceId")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_ascii_lowercase)
|
||||
{
|
||||
push_unique_neighbor(&mut neighbors, &mut seen_neighbors, resource_id);
|
||||
}
|
||||
|
||||
let Some(grant_id) = grant.get("id").and_then(Value::as_str) else {
|
||||
warn!(
|
||||
"Skipping OAuth2 permission grant without an ID on service principal {}",
|
||||
service_principal_id
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let grant_id = grant_id.to_ascii_lowercase();
|
||||
if state.written_grant_ids.insert(grant_id.clone()) {
|
||||
grant_rows.push((grant_id, grant));
|
||||
}
|
||||
}
|
||||
if !grant_rows.is_empty() {
|
||||
state.stats.oauth_grants += grant_rows.len();
|
||||
collector
|
||||
.write_values_batch_to_db("oauth2PermissionGrants".to_string(), grant_rows)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut owner_ids = Vec::new();
|
||||
let mut seen_owner_ids = HashSet::new();
|
||||
for owner in owners.values {
|
||||
let Some(owner_id) = owner
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_ascii_lowercase)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if seen_owner_ids.insert(owner_id.clone()) {
|
||||
owner_ids.push(owner_id.clone());
|
||||
}
|
||||
|
||||
let is_service_principal = owner
|
||||
.get("@odata.type")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|value| value == "#microsoft.graph.servicePrincipal");
|
||||
if is_service_principal {
|
||||
push_unique_neighbor(&mut neighbors, &mut seen_neighbors, owner_id);
|
||||
}
|
||||
}
|
||||
|
||||
let pending_for_current = state
|
||||
.pending_assignments
|
||||
.get(service_principal_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let current_snapshot = {
|
||||
let service_principal = state
|
||||
.service_principals
|
||||
.get_mut(service_principal_id)
|
||||
.ok_or_else(|| {
|
||||
CirroError::InvalidData(format!(
|
||||
"Missing cached service principal {}",
|
||||
service_principal_id
|
||||
))
|
||||
})?;
|
||||
let service_principal_object = service_principal.as_object_mut().ok_or_else(|| {
|
||||
CirroError::InvalidData("Cached Graph object was not a JSON object".to_string())
|
||||
})?;
|
||||
service_principal_object.insert(
|
||||
"owners".to_string(),
|
||||
Value::Array(owner_ids.iter().cloned().map(Value::String).collect()),
|
||||
);
|
||||
service_principal_object.insert("appRoleAssignedTo".to_string(), Value::Array(Vec::new()));
|
||||
merge_assignment_property(service_principal, &inbound_assignments)?;
|
||||
merge_assignment_property(service_principal, &pending_for_current)?;
|
||||
service_principal.clone()
|
||||
};
|
||||
collector
|
||||
.write_value_to_db(
|
||||
"servicePrincipals".to_string(),
|
||||
service_principal_id.to_string(),
|
||||
current_snapshot,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for target_id in cached_targets_to_rewrite {
|
||||
let pending = state
|
||||
.pending_assignments
|
||||
.get(&target_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let Some(target) = state.service_principals.get_mut(&target_id) else {
|
||||
continue;
|
||||
};
|
||||
merge_assignment_property(target, &pending)?;
|
||||
let target_snapshot = target.clone();
|
||||
collector
|
||||
.write_value_to_db("servicePrincipals".to_string(), target_id, target_snapshot)
|
||||
.await?;
|
||||
}
|
||||
|
||||
state
|
||||
.neighbors
|
||||
.insert(service_principal_id.to_string(), neighbors.clone());
|
||||
|
||||
Ok(neighbors)
|
||||
}
|
||||
|
||||
async fn probe_application(
|
||||
collector: &Collector,
|
||||
app_id: &str,
|
||||
state: &mut CrawlState,
|
||||
) -> Result<(), CirroError> {
|
||||
if !state.probed_application_ids.insert(app_id.to_string()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let uri = format!("applications(appId='{}')", app_id);
|
||||
match get_optional_object(collector, &uri).await? {
|
||||
OptionalResponse::Found(application) => {
|
||||
let application_id = required_id(&application, "application")?;
|
||||
collector
|
||||
.write_value_to_db("applications".to_string(), application_id, application)
|
||||
.await?;
|
||||
state.stats.applications += 1;
|
||||
}
|
||||
OptionalResponse::Forbidden => {
|
||||
debug!(
|
||||
"Backing application lookup for app ID {} was forbidden",
|
||||
app_id
|
||||
);
|
||||
}
|
||||
OptionalResponse::NotFound => {
|
||||
debug!("No local application exists for app ID {}", app_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_optional_object(
|
||||
collector: &Collector,
|
||||
uri: &str,
|
||||
) -> Result<OptionalResponse, CirroError> {
|
||||
let response = graph_get_response(collector, uri).await?;
|
||||
if response.status == StatusCode::FORBIDDEN {
|
||||
return Ok(OptionalResponse::Forbidden);
|
||||
}
|
||||
if response.status == StatusCode::NOT_FOUND {
|
||||
return Ok(OptionalResponse::NotFound);
|
||||
}
|
||||
|
||||
let body = response.into_success()?;
|
||||
if !body.is_object() {
|
||||
return Err(CirroError::InvalidData(format!(
|
||||
"Graph response for {} was not an object",
|
||||
uri
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(OptionalResponse::Found(body))
|
||||
}
|
||||
|
||||
async fn fetch_sp_relationship(
|
||||
collector: &Collector,
|
||||
service_principal_id: &str,
|
||||
relationship: &str,
|
||||
) -> Result<CollectionResponse, CirroError> {
|
||||
fetch_collection(
|
||||
collector,
|
||||
&format!(
|
||||
"servicePrincipals/{}/{}?$top=999",
|
||||
service_principal_id, relationship
|
||||
),
|
||||
service_principal_id,
|
||||
relationship,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch_collection(
|
||||
collector: &Collector,
|
||||
initial_uri: &str,
|
||||
service_principal_id: &str,
|
||||
relationship: &str,
|
||||
) -> Result<CollectionResponse, CirroError> {
|
||||
let mut values = Vec::new();
|
||||
let mut incomplete = false;
|
||||
let mut pager = GraphPager::new(initial_uri);
|
||||
|
||||
while let Some(response) = pager.next_page(collector).await? {
|
||||
let status = response.status;
|
||||
if status == StatusCode::FORBIDDEN || status == StatusCode::NOT_FOUND {
|
||||
debug!(
|
||||
"Relationship request for {}/{} returned HTTP {}; keeping {} collected item(s)",
|
||||
service_principal_id,
|
||||
relationship,
|
||||
status.as_u16(),
|
||||
values.len()
|
||||
);
|
||||
incomplete = true;
|
||||
break;
|
||||
}
|
||||
|
||||
let body = response.into_success()?;
|
||||
let page_values = body.get("value").and_then(Value::as_array).ok_or_else(|| {
|
||||
CirroError::InvalidData(format!(
|
||||
"Graph collection response for {}/{} did not contain a value array",
|
||||
service_principal_id, relationship
|
||||
))
|
||||
})?;
|
||||
values.extend(page_values.iter().cloned());
|
||||
}
|
||||
|
||||
Ok(CollectionResponse { values, incomplete })
|
||||
}
|
||||
|
||||
fn register_service_principal(
|
||||
state: &mut CrawlState,
|
||||
service_principal: Value,
|
||||
) -> Result<String, CirroError> {
|
||||
let service_principal_id = required_id(&service_principal, "service principal")?;
|
||||
if let Some(app_id) = service_principal
|
||||
.get("appId")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_ascii_lowercase)
|
||||
{
|
||||
state
|
||||
.app_id_to_sp_id
|
||||
.insert(app_id, service_principal_id.clone());
|
||||
}
|
||||
state
|
||||
.service_principals
|
||||
.entry(service_principal_id.clone())
|
||||
.or_insert(service_principal);
|
||||
Ok(service_principal_id)
|
||||
}
|
||||
|
||||
fn required_id(value: &Value, object_type: &str) -> Result<String, CirroError> {
|
||||
value
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_ascii_lowercase)
|
||||
.ok_or_else(|| {
|
||||
CirroError::InvalidData(format!(
|
||||
"Graph {} response did not contain an ID",
|
||||
object_type
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_assignment(assignment: &Value, fallback_principal_id: Option<&str>) -> Option<Value> {
|
||||
let principal_id = assignment
|
||||
.get("principalId")
|
||||
.and_then(Value::as_str)
|
||||
.or(fallback_principal_id)?
|
||||
.to_ascii_lowercase();
|
||||
let app_role_id = assignment
|
||||
.get("appRoleId")
|
||||
.and_then(Value::as_str)?
|
||||
.to_ascii_lowercase();
|
||||
|
||||
Some(json!({
|
||||
"principalId": principal_id,
|
||||
"appRoleId": app_role_id,
|
||||
}))
|
||||
}
|
||||
|
||||
fn merge_assignment_property(object: &mut Value, additions: &[Value]) -> Result<(), CirroError> {
|
||||
if additions.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let object = object.as_object_mut().ok_or_else(|| {
|
||||
CirroError::InvalidData("Cached Graph object was not a JSON object".to_string())
|
||||
})?;
|
||||
let assignments = object
|
||||
.entry("appRoleAssignedTo")
|
||||
.or_insert_with(|| Value::Array(Vec::new()))
|
||||
.as_array_mut()
|
||||
.ok_or_else(|| {
|
||||
CirroError::InvalidData(
|
||||
"Cached Graph object appRoleAssignedTo property was not an array".to_string(),
|
||||
)
|
||||
})?;
|
||||
for addition in additions {
|
||||
push_unique_assignment(assignments, addition.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_unique_assignment(assignments: &mut Vec<Value>, assignment: Value) {
|
||||
let key = assignment_key(&assignment);
|
||||
if assignments
|
||||
.iter()
|
||||
.all(|existing| assignment_key(existing) != key)
|
||||
{
|
||||
assignments.push(assignment);
|
||||
}
|
||||
}
|
||||
|
||||
fn assignment_key(assignment: &Value) -> String {
|
||||
let principal_id = assignment
|
||||
.get("principalId")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
let app_role_id = assignment
|
||||
.get("appRoleId")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
if principal_id.is_empty() && app_role_id.is_empty() {
|
||||
assignment.to_string()
|
||||
} else {
|
||||
format!("{}:{}", principal_id, app_role_id)
|
||||
}
|
||||
}
|
||||
|
||||
fn push_unique_neighbor(
|
||||
neighbors: &mut Vec<String>,
|
||||
seen: &mut HashSet<String>,
|
||||
neighbor_id: String,
|
||||
) {
|
||||
if seen.insert(neighbor_id.clone()) {
|
||||
neighbors.push(neighbor_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
const MICROSOFT_GRAPH_APP_COUNT: usize = 1_753;
|
||||
|
||||
/*
|
||||
* Publicly documented, well-known Microsoft application IDs used as the
|
||||
* default starting points for restricted-guest enumeration. Keep the
|
||||
* display-name comments beside each ID so this map remains easy to review.
|
||||
*/
|
||||
static WELL_KNOWN_APP_IDS: Lazy<BTreeMap<&'static str, &'static str>> = Lazy::new(|| {
|
||||
BTreeMap::from([
|
||||
// Windows Azure Active Directory
|
||||
(
|
||||
"00000002-0000-0000-c000-000000000000",
|
||||
"Windows Azure Active Directory",
|
||||
),
|
||||
// Microsoft Graph
|
||||
("00000003-0000-0000-c000-000000000000", "Microsoft Graph"),
|
||||
// Office 365 Exchange Online
|
||||
(
|
||||
"00000002-0000-0ff1-ce00-000000000000",
|
||||
"Office 365 Exchange Online",
|
||||
),
|
||||
// Office 365 SharePoint Online
|
||||
(
|
||||
"00000003-0000-0ff1-ce00-000000000000",
|
||||
"Office 365 SharePoint Online",
|
||||
),
|
||||
// Skype for Business Online
|
||||
(
|
||||
"00000004-0000-0ff1-ce00-000000000000",
|
||||
"Skype for Business Online",
|
||||
),
|
||||
// Dataverse
|
||||
("00000007-0000-0000-c000-000000000000", "Dataverse"),
|
||||
// Power BI Service
|
||||
("00000009-0000-0000-c000-000000000000", "Power BI Service"),
|
||||
// Microsoft Intune
|
||||
("0000000a-0000-0000-c000-000000000000", "Microsoft Intune"),
|
||||
// Microsoft Azure CLI
|
||||
(
|
||||
"04b07795-8ddb-461a-bbee-02f9e1bf7b46",
|
||||
"Microsoft Azure CLI",
|
||||
),
|
||||
// Microsoft Azure PowerShell
|
||||
(
|
||||
"1950a258-227b-4e31-a9cf-717495945fc2",
|
||||
"Microsoft Azure PowerShell",
|
||||
),
|
||||
// Microsoft Teams
|
||||
("1fec8e78-bce4-4aaf-ab1b-5451cc387264", "Microsoft Teams"),
|
||||
// Azure DevOps
|
||||
("499b84ac-1321-427f-aa17-267ca6975798", "Azure DevOps"),
|
||||
// Windows Azure Service Management API
|
||||
(
|
||||
"797f4846-ba00-4fd7-ba43-dac1f8f63013",
|
||||
"Windows Azure Service Management API",
|
||||
),
|
||||
// Azure Portal
|
||||
("c44b4083-3bb0-49c1-b47d-974e53cbdf3c", "Azure Portal"),
|
||||
// Azure Key Vault
|
||||
("cfa8b339-82a2-471a-a3c9-0fc0be7a4093", "Azure Key Vault"),
|
||||
// Microsoft Office
|
||||
("d3590ed6-52b3-4102-aeff-aad2292ab01c", "Microsoft Office"),
|
||||
// Microsoft Intune Enrollment
|
||||
(
|
||||
"d4ebce55-015a-49b5-a083-c84d1797ae8c",
|
||||
"Microsoft Intune Enrollment",
|
||||
),
|
||||
// Azure Storage
|
||||
("e406a681-f3d4-42a8-90b6-c2b029497af1", "Azure Storage"),
|
||||
])
|
||||
});
|
||||
|
||||
/// Microsoft application resource catalog from:
|
||||
/// https://github.com/f-bader/entrascopes.com/blob/b0ccd972055ee7f78b446a5d75e14e7fe4c58ede/resources.json
|
||||
///
|
||||
/// Source SHA-256:
|
||||
/// a20ea8fadb3668f86c1e352697b6b456c5928684c5e820543b5d5cc59ea82bb7
|
||||
static MICROSOFT_GRAPH_APP_IDS: Lazy<BTreeMap<String, String>> = Lazy::new(|| {
|
||||
let resources: Vec<MicrosoftGraphResource> =
|
||||
serde_json::from_str(include_str!("restricted_guest_msapps.json"))
|
||||
.expect("embedded Microsoft application catalog must be valid JSON");
|
||||
let resource_count = resources.len();
|
||||
assert_eq!(
|
||||
resource_count, MICROSOFT_GRAPH_APP_COUNT,
|
||||
"embedded Microsoft application catalog is incomplete"
|
||||
);
|
||||
let applications: BTreeMap<_, _> = resources
|
||||
.into_iter()
|
||||
.map(|resource| {
|
||||
let app_id = Uuid::parse_str(&resource.resource_id)
|
||||
.expect("embedded Microsoft application ID must be a valid UUID")
|
||||
.hyphenated()
|
||||
.to_string();
|
||||
let display_name = resource.display_name.trim();
|
||||
assert!(
|
||||
!display_name.is_empty(),
|
||||
"embedded Microsoft application name must not be empty"
|
||||
);
|
||||
(app_id, display_name.to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
applications.len(),
|
||||
resource_count,
|
||||
"embedded Microsoft application catalog contains duplicate IDs"
|
||||
);
|
||||
applications
|
||||
});
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MicrosoftGraphResource {
|
||||
resource_id: String,
|
||||
display_name: String,
|
||||
}
|
||||
|
||||
pub(super) fn well_known_apps() -> &'static BTreeMap<&'static str, &'static str> {
|
||||
&WELL_KNOWN_APP_IDS
|
||||
}
|
||||
|
||||
pub(super) fn microsoft_apps() -> &'static BTreeMap<String, String> {
|
||||
&MICROSOFT_GRAPH_APP_IDS
|
||||
}
|
||||
|
||||
pub(super) fn application_name(app_id: &str) -> Option<&'static str> {
|
||||
WELL_KNOWN_APP_IDS
|
||||
.get(app_id)
|
||||
.copied()
|
||||
.or_else(|| MICROSOFT_GRAPH_APP_IDS.get(app_id).map(String::as_str))
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -71,10 +71,9 @@ impl Token {
|
||||
return Err(CirroError::AuthError("Invalid JWT token format".into()));
|
||||
}
|
||||
|
||||
let claims_json = base64::engine::general_purpose::STANDARD_NO_PAD
|
||||
let claims_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.decode(token_parts[1])
|
||||
.map_err(|e| CirroError::AuthError(format!("Failed to decode token: {}", e)))?;
|
||||
|
||||
let claims: Claims = serde_json::from_slice(&claims_json)?;
|
||||
Ok(claims)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod cli;
|
||||
pub mod cli_utils;
|
||||
pub mod collect;
|
||||
pub mod collectors;
|
||||
pub mod context;
|
||||
|
||||
+6
-1
@@ -48,6 +48,10 @@ pub enum GraphCommands {
|
||||
#[arg(long, action = clap::ArgAction::SetTrue)]
|
||||
post_process: bool,
|
||||
|
||||
/// Show what would be ingested and post-processed without executing queries
|
||||
#[arg(long, action = clap::ArgAction::SetTrue)]
|
||||
dry_run: bool,
|
||||
|
||||
/// Enable debug logging
|
||||
#[arg(long, action = clap::ArgAction::SetTrue)]
|
||||
debug: bool,
|
||||
@@ -100,6 +104,7 @@ pub async fn handle_graph_command(command: GraphCommands) -> Result<(), CirroErr
|
||||
db_name,
|
||||
labels,
|
||||
post_process,
|
||||
dry_run,
|
||||
debug,
|
||||
} => {
|
||||
if let Err(e) = setup_logger(debug) {
|
||||
@@ -142,7 +147,7 @@ pub async fn handle_graph_command(command: GraphCommands) -> Result<(), CirroErr
|
||||
CirroIngestor::new(r#type, file, server, user, password, db_name, labels).await;
|
||||
|
||||
// Run the ingestor
|
||||
if let Err(e) = ingestor.run(post_process).await {
|
||||
if let Err(e) = ingestor.run(post_process, dry_run).await {
|
||||
return Err(CirroError::Unknown(e.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
name: "Analysis Service Server"
|
||||
label: "{{ LABELS.AnalysisServiceServer }}"
|
||||
table_name: "resources"
|
||||
resource_type: "microsoft.analysisservices/servers"
|
||||
properties:
|
||||
- "/id"
|
||||
- "/properties"
|
||||
index_properties:
|
||||
- "{{ LABELS.AnalysisServiceFirewallRule }}:name+serverId"
|
||||
cypher: |
|
||||
UNWIND $batch AS row
|
||||
MERGE (s:{{ LABELS.ArmResource }} {id: toLower(row.id)})
|
||||
SET s:{{ LABELS.AnalysisServiceServer }}
|
||||
SET s += {
|
||||
managedMode: row.properties.managedMode,
|
||||
serverFullName: row.properties.serverFullName,
|
||||
serverMonitorMode: row.properties.serverMonitorMode,
|
||||
state: row.properties.state,
|
||||
administrators: row.properties.asAdministrators.members,
|
||||
enablePowerBIService: row.properties.ipV4FirewallSettings.enablePowerBIService
|
||||
}
|
||||
|
||||
WITH s, row
|
||||
|
||||
// Members is a list of strings that refer to users, groups, or service principals in the format:
|
||||
// Users use UPN
|
||||
// Service principals use app:<appId>@<tenantId>
|
||||
// Groups use obj:<objectId>@<tenantId>
|
||||
CALL (s, row) {
|
||||
UNWIND coalesce(row.properties.asAdministrators.members, []) AS admin
|
||||
WITH s, admin WHERE admin IS NOT NULL
|
||||
|
||||
// Extract the identifier and tenantId from the member string
|
||||
// It's either going to be a GUID or a domain
|
||||
WITH s, admin,
|
||||
split(admin, '@') AS parts,
|
||||
CASE
|
||||
WHEN admin STARTS WITH 'app:' THEN 'servicePrincipal'
|
||||
WHEN admin STARTS WITH 'obj:' THEN 'obj'
|
||||
ELSE 'user'
|
||||
END AS adminType
|
||||
WITH s, admin, parts, adminType
|
||||
WHERE size(parts) = 2
|
||||
WITH s, admin, parts[0] AS identifier, parts[1] AS tenantId, adminType
|
||||
WHERE identifier IS NOT NULL AND tenantId IS NOT NULL
|
||||
|
||||
// If it's a service principal, match to appId
|
||||
CALL (s, identifier, adminType) {
|
||||
WITH s, identifier, adminType
|
||||
WHERE adminType = 'servicePrincipal'
|
||||
MERGE (sp:{{ LABELS.GraphServicePrincipal }} {appId: toLower(identifier)})
|
||||
MERGE (sp)-[:{{ REL.IS_ADMIN }}]->(s)
|
||||
}
|
||||
|
||||
// If it's a obj, match to id
|
||||
CALL (s, identifier, adminType) {
|
||||
WITH s, identifier, adminType
|
||||
WHERE adminType = 'obj'
|
||||
MERGE (g:{{ LABELS.GraphObject }} {id: toLower(identifier)})
|
||||
MERGE (g)-[:{{ REL.IS_ADMIN }}]->(s)
|
||||
}
|
||||
|
||||
// If it's a user, match to UPN
|
||||
// Double checking that it has a "." for a loose check to a domain, since UPNs should be in an email format
|
||||
CALL (s, identifier, adminType, tenantId) {
|
||||
WITH s, identifier, adminType, tenantId
|
||||
WHERE adminType = 'user' AND tenantId CONTAINS '.'
|
||||
MERGE (u:{{ LABELS.GraphUser }} {userPrincipalName: toLower(identifier + '@' + tenantId)})
|
||||
MERGE (u)-[:{{ REL.IS_ADMIN }}]->(s)
|
||||
}
|
||||
}
|
||||
|
||||
// Next is ipV4FirewallSettings, which is a list of objects with properties name, startIpAddress, and endIpAddress
|
||||
// Names are unique to a server, but not globally, so we need a composite key of server id and rule name to create the firewall rule nodes
|
||||
CALL (s, row) {
|
||||
UNWIND coalesce(row.properties.ipV4FirewallSettings.firewallRules, []) AS rule
|
||||
MERGE (f:{{ LABELS.AnalysisServiceFirewallRule }} {name: rule.firewallRuleName, serverId: s.id})
|
||||
SET f += {
|
||||
rangeStart: rule.rangeStart,
|
||||
rangeEnd: rule.rangeEnd
|
||||
}
|
||||
MERGE (s)-[:{{ REL.HAS_RULE }}]->(f)
|
||||
}
|
||||
@@ -7,9 +7,9 @@ properties:
|
||||
- "/properties"
|
||||
cypher: |
|
||||
UNWIND $batch AS row
|
||||
MERGE (s:{{ LABELS.ArmResource }} {id: toLower(row.id)})
|
||||
SET s:{{ LABELS.ApimService }}
|
||||
SET s += {
|
||||
MERGE (obj:{{ LABELS.ArmResource }} {id: toLower(row.id)})
|
||||
SET obj:{{ LABELS.ApimService }}
|
||||
SET obj += {
|
||||
createdAt: row.properties.createdAt,
|
||||
developerPortalStatus: row.properties.developerPortalStatus,
|
||||
developerPortalUrl: row.properties.developerPortalUrl,
|
||||
@@ -27,10 +27,9 @@ cypher: |
|
||||
scmUrl: row.properties.scmUrl
|
||||
}
|
||||
|
||||
WITH s, row
|
||||
CALL {
|
||||
WITH s, row
|
||||
WITH obj, row
|
||||
CALL (obj, row) {
|
||||
UNWIND coalesce(row.managedByTenants, []) AS tenant
|
||||
MERGE (t:{{ LABELS.Tenant }} {id: toLower(tenant.tenantId)})
|
||||
MERGE (t)-[:{{ REL.MANAGES }}]->(t)
|
||||
}
|
||||
MERGE (t)-[:{{ REL.MANAGES }}]->(obj)
|
||||
}
|
||||
@@ -9,5 +9,4 @@ properties:
|
||||
cypher: |
|
||||
UNWIND $batch AS row
|
||||
MERGE (obj:{{ LABELS.ArmResource }} {id: toLower(row.id)})
|
||||
SET obj:{{ LABELS.AppBuilder }}
|
||||
RETURN count(*) AS _
|
||||
SET obj:{{ LABELS.AppBuilder }}
|
||||
@@ -25,9 +25,10 @@ cypher: |
|
||||
SET env:{{ LABELS.AppManagedEnvironment }}
|
||||
MERGE (env)-[:{{ REL.DEPLOYS }}]->(obj)
|
||||
|
||||
WITH obj, row
|
||||
|
||||
// Link to ContainerRegistry
|
||||
// Link by name since name since it's a unique domain and ContainerApps only reference the registry server, not the full resource ID
|
||||
WITH obj, row
|
||||
CALL (obj, row) {
|
||||
UNWIND coalesce(row.properties.configuration.registries, []) AS registry
|
||||
MERGE (r:{{ LABELS.ContainerRegistry }} {loginServer: toLower(registry.server)})
|
||||
@@ -37,11 +38,9 @@ cypher: |
|
||||
username: registry.username,
|
||||
passwordSecretRef: registry.passwordSecretRef
|
||||
}
|
||||
RETURN count(*) AS _
|
||||
}
|
||||
|
||||
// Iterate over container templates to link to ContainerRegistry by registry server in image reference
|
||||
WITH obj, row
|
||||
CALL (obj, row) {
|
||||
UNWIND coalesce(row.properties.template.containers, []) AS container
|
||||
WITH obj, container WHERE container.image IS NOT NULL
|
||||
@@ -63,7 +62,4 @@ cypher: |
|
||||
value: envVar.value,
|
||||
secretRef: envVar.secretRef
|
||||
}
|
||||
|
||||
RETURN count(*) AS _
|
||||
}
|
||||
RETURN count(*) AS _
|
||||
}
|
||||
@@ -18,6 +18,4 @@ cypher: |
|
||||
mtls: row.properties.peerAuthentication.mtls.enabled,
|
||||
encryption: row.properties.peerTrafficConfiguration.encryption.enabled,
|
||||
staticIp: row.properties.staticIp
|
||||
}
|
||||
|
||||
RETURN count(*) AS _
|
||||
}
|
||||
@@ -25,22 +25,20 @@ cypher: |
|
||||
MERGE (a)-[:{{ REL.HAS_RUNBOOK }}]->(obj)
|
||||
|
||||
WITH obj, row
|
||||
CALL {
|
||||
WITH obj, row
|
||||
WITH obj, row, coalesce(row.properties.parameters, {}) AS parameters
|
||||
UNWIND keys(parameters) AS paramName
|
||||
WITH obj, paramName, parameters[paramName] AS paramMap
|
||||
|
||||
// Create a unique identifier for the parameter to avoid name collisions
|
||||
MERGE (p:RunbookParameter {id: toLower(obj.id + "/param/" + paramName)})
|
||||
SET p += {
|
||||
name: paramName,
|
||||
defaultValue: paramMap.defaultValue,
|
||||
isMandatory: paramMap.isMandatory,
|
||||
position: paramMap.position,
|
||||
type: paramMap.type
|
||||
}
|
||||
MERGE (obj)-[:{{ REL.HAS_PARAMETER }}]->(p)
|
||||
RETURN count(*) AS _
|
||||
}
|
||||
RETURN count(*) AS _
|
||||
CALL (obj, row) {
|
||||
WITH obj, row, coalesce(row.properties.parameters, {}) AS parameters
|
||||
UNWIND keys(parameters) AS paramName
|
||||
WITH obj, paramName, parameters[paramName] AS paramMap
|
||||
|
||||
// Create a unique identifier for the parameter to avoid name collisions
|
||||
MERGE (p:RunbookParameter {id: toLower(obj.id + "/param/" + paramName)})
|
||||
SET p += {
|
||||
name: paramName,
|
||||
defaultValue: paramMap.defaultValue,
|
||||
isMandatory: paramMap.isMandatory,
|
||||
position: paramMap.position,
|
||||
type: paramMap.type
|
||||
}
|
||||
MERGE (obj)-[:{{ REL.HAS_PARAMETER }}]->(p)
|
||||
}
|
||||
@@ -25,9 +25,9 @@ cypher: |
|
||||
version: row.properties.version
|
||||
}
|
||||
|
||||
WITH obj, row
|
||||
CALL (obj, row) {
|
||||
WITH obj, row WHERE row.properties.containerResourceId IS NOT NULL
|
||||
MERGE (h:{{ LABELS.ArmResource }} {id: toLower(row.properties.containerResourceId)})
|
||||
MERGE (h)-[:{{ REL.HOSTS }}]->(obj)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
name: "Azure Stack HCI Edge Machine"
|
||||
label: "{{ LABELS.AzureStackEdgeMachine }}"
|
||||
table_name: "resources"
|
||||
resource_type: "microsoft.azurestackhci/edgemachines"
|
||||
properties:
|
||||
- "/id"
|
||||
- "/properties"
|
||||
index_properties:
|
||||
- "{{ LABELS.AzureStackEdgeMachine }}:cloudId"
|
||||
- "{{ LABELS.AzureStackEdgeMachineNic }}:machineId+adapterName"
|
||||
cypher: |
|
||||
UNWIND $batch AS row
|
||||
MERGE (obj:{{ LABELS.ArmResource }} {id: toLower(row.id)})
|
||||
SET obj:{{ LABELS.AzureStackEdgeMachine }}
|
||||
SET obj += {
|
||||
|
||||
// Not sure how to link these to other resources, so just storing the IDs for now
|
||||
arcMachineResourceGroupId: row.properties.arcMachineResourceGroupId,
|
||||
arcMachineResourceId: row.properties.arcMachineResourceId,
|
||||
|
||||
// The Cloud ID is a reference to the cloud this edge machine is registered with
|
||||
// It looks like it's a HybridMachine name but unsure of how to link it so just storing the ID for now
|
||||
cloudId: row.properties.cloudId,
|
||||
|
||||
|
||||
connectivityStatus: row.properties.connectivityStatus,
|
||||
devicePoolResourceId: row.properties.devicePoolResourceId,
|
||||
edgeMachineKind: row.properties.edgeMachineKind,
|
||||
lastUpdated: row.properties.lastUpdated,
|
||||
lastSyncTimestamp: row.properties.lastSyncTimestamp,
|
||||
machineState: row.properties.machineState
|
||||
}
|
||||
|
||||
WITH obj, row
|
||||
CALL (obj, row) {
|
||||
WITH obj, row
|
||||
WHERE row.properties.objectId IS NOT NULL
|
||||
MERGE (s:{{ LABELS.GraphObject }} {id: toLower(row.properties.objectId)})
|
||||
MERGE (obj)-[:{{ REL.REGISTERED_BY }}]->(s)
|
||||
}
|
||||
|
||||
CALL (obj, row) {
|
||||
UNWIND coalesce(row.properties.networkProfile.nicDetails, []) AS nic
|
||||
MERGE (n:{{ LABELS.AzureStackEdgeMachineNic }} {machineId: toLower(row.id), adapterName: nic.adapterName})
|
||||
SET n += {
|
||||
adapterName: nic.adapterName,
|
||||
dnsServers: nic.dnsServers,
|
||||
ip4Address: nic.ip4Address,
|
||||
macAddress: nic.macAddress,
|
||||
rdmaCapability: nic.rdmaCapability,
|
||||
subnetMask: nic.subnetMask
|
||||
}
|
||||
MERGE (obj)-[:{{ REL.HAS_NIC }}]->(n)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
name: "Azure Stack Registration"
|
||||
label: "{{ LABELS.AzureStackRegistration }}"
|
||||
table_name: "resources"
|
||||
resource_type: "microsoft.azurestack/registrations"
|
||||
properties:
|
||||
- "/id"
|
||||
- "/properties"
|
||||
cypher: |
|
||||
UNWIND $batch AS row
|
||||
MERGE (obj:{{ LABELS.ArmResource }} {id: toLower(row.id)})
|
||||
SET obj:{{ LABELS.AzureStackRegistration }}
|
||||
SET obj += {
|
||||
cloudId: row.properties.cloudId
|
||||
}
|
||||
|
||||
WITH obj, row
|
||||
CALL (obj, row) {
|
||||
WITH obj, row
|
||||
WHERE row.properties.objectId IS NOT NULL
|
||||
MERGE (s:{{ LABELS.GraphObject }} {id: toLower(row.properties.objectId)})
|
||||
MERGE (obj)-[:{{ REL.REGISTERED_BY }}]->(s)
|
||||
}
|
||||
+1
-1
@@ -19,4 +19,4 @@ cypher: |
|
||||
redisVersion: row.properties.redisVersion,
|
||||
updateChannel: row.properties.updateChannel,
|
||||
instancePorts: [instance IN coalesce(row.properties.instances, []) | instance.sslPort]
|
||||
}
|
||||
}
|
||||
Loaded 100 of 182 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user