From 16b13e667ff0fb32209991a1b684e8eadb85c401 Mon Sep 17 00:00:00 2001 From: Angus Warren Date: Tue, 9 Jun 2020 01:28:48 +0800 Subject: [PATCH] Default whitelist compatibility updates. Better logging. --- AzGluePS/AzGlueForwarder/run.ps1 | 93 +++++++--- AzGluePS/whitelisted-endpoints.yml | 278 ++++++++++++++++++++++++++++- README.md | 47 ++++- itglue-endpoints.yml | Bin 44124 -> 47194 bytes 4 files changed, 386 insertions(+), 32 deletions(-) diff --git a/AzGluePS/AzGlueForwarder/run.ps1 b/AzGluePS/AzGlueForwarder/run.ps1 index c729ac2..d4b0b86 100644 --- a/AzGluePS/AzGlueForwarder/run.ps1 +++ b/AzGluePS/AzGlueForwarder/run.ps1 @@ -1,43 +1,61 @@ using namespace System.Net param($Request, $TriggerMetadata) -# TEMP: DEBUG -Write-Host ($request | convertto-json -depth 5) +Write-Information ("Incoming {0} {1}" -f $Request.Method,$Request.Url) -Function ImmediateFailure ($message) { - Write-Host $message +Function ImmediateFailure ($Message) { + Write-Error $Message Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ headers = @{'content-type' = 'application\json' } StatusCode = [httpstatuscode]::OK - Body = @{"Error" = $message } | convertto-json + Body = @{"Error" = $Message } | convertto-json }) exit 1 } -function Build-Body ($whitelistObj, $sourceObj) { +function Build-Body ($whitelistObj, $sourceObj, $depth) { + # store a depth counter to avoid looping. + if ($depth -isnot [int]) { + $depth = 1 + } else { + $depth++ + } + if ($depth -gt 8) { + Write-Error "Possible recursion loop or source object is deeper than expected." + Return + } if (-not $sourceObj) { Return } if ($whitelistObj -is [hashtable] -or $whitelistObj -is [System.Collections.Specialized.OrderedDictionary]) { + # When the whitelist object is a dictionary, loop over the keys and if they exist in the + # source object, recurse. Note that any extra keys will not be checked or logged. + $counter = 0 $newObject = @{} foreach ($key in $whitelistObj.keys) { if ($sourceObj.$key) { - $newObject[$key] = Build-Body $whitelistObj[$key] $sourceObj.$key + $counter++ + $newObject[$key] = Build-Body -whitelistObj $whitelistObj[$key] -sourceObj $sourceObj.$key -depth $depth } + Write-Debug ("{0}/{1} keys were whitelisted from the source dictionary." -f $counter, $sourceObj.length) } - } elseif ($whitelistObj -is [System.Collections.Generic.List`1[System.Object]]) { + } elseif ($whitelistObj -is [System.Collections.Generic.List`1[System.Object]] -and $whitelistObj.length -eq 1) { + # When the whitelist object is a list with a single member, loop over the source and store the results in an array. $newObject = @() foreach ($item in $sourceObj) { - $newObject += Build-Body $whitelistObj[0] $item + $newObject += Build-Body -whitelistObj $whitelistObj[0] -sourceObj $item -depth $depth } } elseif ($whitelistObj -is [string]) { + # When the whitelist object is a string, store the value of the source object and move on. + # Mote that if the value is a list/dict, it will still add everything. + # TODO: Validate source object data types. $newObject = $sourceObj } else { - Write-Error "Unexpected type found $whitelistObj" + Write-Error "Unexpected format of whitelist object. Check your configuration: $($whitelistObj | ConvertTo-Json -Depth 2)" + Return } Return $newObject } - $clientToken = $request.headers.'x-api-key' # Check if the client's API token matches our stored version and that it's not too short. @@ -60,13 +78,15 @@ if (!$AllowedOrgs) { } ## Whitelisting endpoints & data. -# TODO: This takes about 800ms to import the first time. Need to confirm that subsequent queries are faster. -Measure-Command { Import-Module powershell-yaml -Function ConvertFrom-Yaml } +Import-Module powershell-yaml -Function ConvertFrom-Yaml $endpoints = Get-Content .\whitelisted-endpoints.yml | ConvertFrom-Yaml $resourceUri = $request.Query.ResourceURI $resourceUri_generic = ([string]$resourceUri).TrimEnd("/") -replace "/\d+","/:id" +# Log the body of the request if the debug level is trace. +Write-Verbose ("Incoming Body: {0}" -f ($Request.Body|ConvertTo-Json -depth 6)) + # Check to see if the called API endpoint & method has been whitelisted. foreach ($key in $endpoints.keys) { if ($endpoints[$key].endpoints -contains $resourceUri_generic -and $endpoints[$key].methods -contains $request.Method) { @@ -81,11 +101,9 @@ if (-not $endpointKey) { # Build new query string from required and whitelisted parameters $itgQuery = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) foreach ($filter in $endpoints[$endpointKey].required_parameters.Keys) { - Write-Host $filter $itgQuery.Add($filter, $endpoints[$endpointKey].required_parameters.$filter) } foreach ($filter in $endpoints[$endpointKey].allowed_parameters) { - Write-Host $filter if ($request.Query.$filter) { $itgQuery.Add($filter, $request.Query.$filter) } @@ -95,30 +113,38 @@ foreach ($filter in $endpoints[$endpointKey].allowed_parameters) { $uriBuilder = [System.UriBuilder]("{0}{1}" -f $ENV:ITGlueURI,$resourceUri) $uriBuilder.Query = $itgQuery.ToString() $itgUri = $uriBuilder.Uri.OriginalString +Write-Information ("Outgoing {0} {1}" -f $Request.Method,$itgUri) # Construct new request for IT Glue -# TODO: Move this to Key Vault $itgHeaders = @{"x-api-key" = $ENV:ITGlueAPIKey} -$itgMethod = $Request.method -$oldBody = $request.body | convertfrom-json -$itgBody = Build-Body $endpoints[$endpointKey].createbody $oldBody -$itgBodyJson = $itgBody | ConvertTo-Json -Depth 5 -Write-Information "Outgoing body: $itgBodyJson" +$itgMethod = $Request.Method +if ($request.body) { + $oldBody = $request.body | convertfrom-json + $itgBody = Build-Body $endpoints[$endpointKey].createbody $oldBody + $itgBodyJson = $itgBody | ConvertTo-Json -Depth 6 +} else { + $itgBodyJson = $null +} + +# Log outgoing body if the debug level is trace. +Write-Verbose "Outgoing body: $itgBodyJson" # Send request to IT Glue $SuccessfullQuery = $false $attempt = 2 while ($attempt -gt 0 -and -not $SuccessfullQuery) { try { - $itgRequest = Invoke-RestMethod -Method $itgMethod -ContentType "application/vnd.api+json" -Uri $itgUri -Body $itgBodyJson -Headers $itgHeaders + $itgRequest = Invoke-RestMethod -Method $itgMethod -ContentType "application/vnd.api+json" ` + -Uri $itgUri -Body $itgBodyJson -Headers $itgHeaders $SuccessfullQuery = $true } catch { $attempt-- if ($attempt -eq 0) { - # don't include $_.Exception.Message to avoid leaking any unexpected information. - ImmediateFailure "$($_.Exception.Response.StatusCode.value__) - Failed after 3 attempts to $itgUri." + Write-Debug $_.Exception.Message + # don't respond with $_.Exception.Message to avoid leaking any unexpected information. + ImmediateFailure "$($_.Exception.Response.StatusCode.value__) - Failed after 2 attempts to $itgUri." } - start-sleep (get-random -Minimum 0 -Maximum 10) + start-sleep (get-random -Minimum 1 -Maximum 10) } } @@ -133,9 +159,20 @@ if ($itgRequest.data.type -contains "organizations" -or } # Strip out any paramaters from the body which haven't been explicitly whitelisted. -$itgReturnBody = Build-Body $endpoints[$endpointKey].returnbody $itgRequest -if ($itgRequest.meta) {$itgReturnBody.meta = $itgRequest.meta} -if ($itgRequest.links) {$itgReturnBody.links = $itgRequest.links} +if ($endpoints[$endpointKey].returnbody) { + $itgReturnBody = Build-Body $endpoints[$endpointKey].returnbody $itgRequest + if ($itgRequest.meta) { + $itgReturnBody.meta = $itgRequest.meta + } + if ($itgRequest.links) { + $itgReturnBody.links = $itgRequest.links + } +} else { + $itgReturnBody = @{} +} + +# Log response body if the debug level is trace. +Write-Verbose ("Response body: {0}" -f ($itgReturnBody | Convertto-Json -Depth 6)) # Return the final object. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ diff --git a/AzGluePS/whitelisted-endpoints.yml b/AzGluePS/whitelisted-endpoints.yml index b3219ce..821940f 100644 --- a/AzGluePS/whitelisted-endpoints.yml +++ b/AzGluePS/whitelisted-endpoints.yml @@ -29,6 +29,8 @@ passwords: password: string url: string notes: string + resource_id: int + resource_type: string password-category-id: int password-category-name: string returnbody: @@ -69,4 +71,278 @@ organizations: organization-type-id: int organization-type-name: string updated-at: datetime - created-at: datetime \ No newline at end of file + created-at: datetime + +configurations: + endpoints: + - /configurations + - /configurations/:id + - /organizations/:id/relationships/configurations + - /organizations/:id/relationships/configurations/:id + methods: + - GET + - PATCH + - POST + allowed_parameters: + - filter[name] + - filter[psa_id] + - filter[serial_number] + - page[number] + - page[size] + required_parameters: + createbody: + data: + - type: string + attributes: + organization-id: int + name: string + hostname: string + configuration-type-id: string + configuration-status-id: string + manufacturer-id: string + model-id: string + primary-ip: string + serial-number: string + mac-address: string + asset-tag: string + notes: string + relationships: + configuration-interfaces: + data: + - id: int + type: string + returnbody: + data: + - id: int + type: string + attributes: + organization-id: int + name: string + hostname: string + serial-number: string + asset-tag: string + notes: string + relationships: + configuration-interfaces: + data: + - id: int + type: string + +flexible_assets: + endpoints: + - /flexible_assets + - /flexible_assets/:id + methods: + - GET + - PATCH + - POST + allowed_parameters: + - filter[flexible_asset_type_id] + - filter[name] + - filter[organization_id] + - page[number] + - page[size] + required_parameters: + createbody: + data: + - type: string + attributes: + id: int + organization-id: int + flexible-asset-type-id: int + traits: + ### Used for "Device logbook - Autodoc" + device-name: string + events: string + user-profiles: string + installed-updates: string + installed-software: string + device: int + ### ADDS Documentation + domain-name: string + forest-summary: string + site-summary: string + domain-controllers: string + fsmo-roles: string + optional-features: string + upn-suffixes: string + default-password-policies: string + domain-admins: string + user-count: string + ### Used for "Unifi - Sites" + site-name: string + wan: string + lan: string + vpn: string + wi-fi: string + port-forwards: string + switches: string + ### Used for "ITGLue AutoDoc - SQL Server" + instance-name: string + instance-settings: string + databases: string + tagged-devices: [int] + ### User for "Fileshare" + name: string + share-name: string + share-path: string + full-control-permissions: string + read-permissions: string + modify-permissions: string + deny-permissions: string + #tagged-devices: string #duplicate + csv-file: + content: base64file + file_name: string + # Used for "Hyper-v AutoDoc v2" + host-name: string + virtual-machines: string + network-settings: string + replication-settings: string + host-settings: string + returnbody: + errors: list + data: + - id: int + type: string + attributes: + organization-id: int + flexible-asset-type-id: int + name: string + traits: + device-name: string + domain-name: string + instance-name: string + device: int + name: string + host-name: string + created-at: datetime + updated-at: datetime + +flexible_asset_types: + endpoints: + - /flexible_asset_types + - /flexible_asset_types/:id + methods: + - GET + - POST + allowed_parameters: + - filter[enabled] + - filter[id] + - filter[name] + required_parameters: + createbody: + data: + - type: string + attributes: + name: string + description: string + icon: string + show-in-menu: bool + relationships: + flexible-asset-fields: + data: + - type: string + attributes: + order: int + name: string + kind: string + hint: string + tag-type: string + required: bool + options: string + use-for-title: bool + show-in-list: bool + returnbody: + data: + - id: int + type: string + attributes: + name: string + description: string + created-at: datetime + updated-at: datetime + icon: string + show-in-menu: bool + relationships: + flexible-asset-fields: + data: + - id: int + type: string + +configuration_types: + endpoints: + - /configuration_types + - /configuration_types/:id + methods: + - GET + - POST + allowed_parameters: + - filter[name] + required_parameters: + createbody: + data: + - type: string + attributes: + name: string + returnbody: + data: + - id: int + type: string + attributes: + name: string + configurations-count: int + created-at: datetime + updated-at: datetime + +models: + endpoints: + - /models + - /models/:id + methods: + - GET + - POST + allowed_parameters: + - filter[id] + - page[number] + - page[size] + required_parameters: + createbody: + data: + - type: string + attributes: + id: int + name: string + manufacturer-id: int + returnbody: + data: + - id: int + type: string + attributes: + name: string + created-at: datetime + updated-at: datetime + +manufacturers: + endpoints: + - /manufacturers + - /manufacturers/:id + methods: + - GET + - POST + allowed_parameters: + - filter[name] + required_parameters: + createbody: + data: + - type: string + attributes: + name: string + returnbody: + data: + - id: int + type: string + attributes: + name: string + created-at: datetime + updated-at: datetime diff --git a/README.md b/README.md index 219c036..8538307 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # AzGlue, a secure API gateway for IT Glue This project has been forked from [Kelvin Tegelaar](https://github.com/KelvinTegelaar)'s repo hosted on [KelvinTegelaar/AzGlue](https://github.com/KelvinTegelaar/AzGlue) and originally posted to his (fantasic) blog [cyberdrain.com](https://www.cyberdrain.com/documenting-with-powershell-handling-it-glue-api-security-and-rate-limiting/). -I'll be aiming to implement the following features: +The first release will maintain backwards compatibilty with Kelvin's existing gateway and public scripts. + +Once this is complete, I will be implmenting some new features which would require existing scripts to be reworked. + +### Goals for first release: - [x] Allow local dev, testing and deployment with VSCode's [Azure Functions extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions). - [x] Prevent misconfigured gateways from accepting empty API keys. - [x] Restrict returned data from the /organizations endpoint to honor OrgId whitelisting. @@ -12,12 +16,49 @@ I'll be aiming to implement the following features: - [x] Query string paramaters. - [x] Payload data sent to IT Glue. - [x] Payload data returned to the client. +- [x] Move IT Glue API key to Azure Key Vault. +- [ ] Set up default whitelisted-endpoints.yml file to work with Kelvin Tegelaar's existing scripts. + +### Goals for second release: - [ ] Per-client API keys - [ ] System to only returned data relevant to the specific PC making the request. -- [ ] Move IT Glue API key to Azure Key Vault. -### Basic usage: +### Progress setting up whitelisted-endpoints.yml defaults: + - [x] IT-Glue-ADDS-Documentation.ps1 + - [ ] IT-Glue-ADGroups-Documentation.ps1 + - [ ] IT-Glue-AzureADSettings-Documentation.ps1 + - [x] IT-glue-BitLocker-Documentation.ps1 + - [x] ITGlue-Device-AuditLog.ps1 + - [x] ITGlue-DeviceSync.ps1 + - [x] IT-Glue-FileSharePermissions-Documentation.ps1 + - [x] IT-Glue-HyperV-Documentation.ps1 + - [ ] IT-Glue-intuneApplication-Documentation.ps1 + - [x] IT-Glue-LAPSAlternative-Documentation.ps1 + - [ ] IT-Glue-Network-Documentation.ps1 + - [ ] IT-Glue-O365-MailboxPermissions-Documentation.ps1 + - [ ] IT-Glue-O365-Teams-Documentation.ps1 + - [ ] IT-Glue-O365-UsageReports-Documentation.ps1 + - [ ] IT-Glue-Server-Documentation.ps1 + - [x] IT-Glue-SQL-Documentation.ps1 + - [x] IT-Glue-Unifi-Documentation.ps1 + +### Basic setup +1. Install the [Azure Functions extensions](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) for VS Code. +2. Copy the local.settings.json.example file, and remove the .example extension. +3. Populate the AzAPIKey, ITGlueAPIKey & ITGlueURI environmental variables here. +4. Copy OrgList.csv.example and remove the .example extension. +5. Update to match your environment. +6. Right click on the "AzGluePS" direction, and select "open with Code" +7. Open the "run.ps1" file and press F5. +8. Test it locally using the "http://localhost:7071/api/${functionName}?ResourceURI=" URI. +9. Open the Azure tab on the left, open Functions, click the "Deploy to Function App.." button to create/deploy the app in Azure. +11. Open the App Service in the Azure Portal, and enable a system managed identity from Settings > Identity. +10. Set up application settings: + 1. Open the Azure portal, open your App Service, open Configuration > Application settings. + 2. Add AzAPIKey, ITGlueAPIKey & ITGlueURI environmental variables here. + 3. If you've got a Key Vault, you can authorise the system managed identity and provide access to the key through the Application settings [using this process](https://docs.microsoft.com/en-us/azure/app-service/app-service-key-vault-references) +### Basic usage: Once the gateway is deployed to Azure Functions, you can use the standard IT Glue Powershell module to query it. ```PowerShell Import-Module ITGlueAPI diff --git a/itglue-endpoints.yml b/itglue-endpoints.yml index 21920b5948ac7b11e9be39cabc2063ca654111c7..2681c5d697626a0cd6217058be42de0109b79be2 100644 GIT binary patch delta 1845 zcmaJ?T}+#06nSkYB9$gNMmPOGo&ElXL={MGSfbzvFuw#~NbyiU$#ICRe#;N#OU{@)2 zAc3`Vcxx|LjbY{(qM#Zv^h+=gA`8MHXTLBXyX@T+4V46AzTr@=qViNle7xlaXP#dq;PS zqDy!cwA)2&7E#BT3eR8w#UdG#qC!Y~hvDIw4cIz;7E+td@Yktc@g!-{Iqxp^P?q{$ zGK8nhlO0?ayI?&$xas!%PnzF{SCI51rcw}arL_pQYZt`O@M(~d^0`D*22Nh=`LE=2 zZEk__`QKg#GV|eN2m;M7 z#ClP2$MoeGXfe5H-iPQ+)5{W*o8wK!%8*DZDr~6zBB)AB)-0C|{jrb7uD0>rsjS5gpEM6*)apfTs%tJ}E(|{A; z3JyU&sC$8UB`IgHskpdJmt@{vlh=Ve*o#VC_&;OY3 zzj^6V)hbqTan?Z_oXhHi{gun>czvN0wqI)E&c#{_2=+&QBD-_2Cs<#Q__>@~>+AW} zZ#=&`Q#DloG5Feh4{Co9z7Jp1SFj@f``mFec(`gHa`HDkU{Z2m-S`u>%>l@-N_^(l zw>8MN+h0ojn|p6nQ3+0W3r^T+XolT_6+Uel6yun_|9K71^DU*8qu?LYAi*a74iCYnqmbp@tC3nWiOH5fdUwK$9Rrv}l_lmRcp$5{tz)r0LP3bqG`< zEg7U<^G0f+Le!-wq*uS=T)=jsC7u~9F6owSxq@W;x5Oc6z@USVxdMKZ)&wK z#(7L{`;h2&()%-q9a;_iOPiGR1v%I@9IO`?Eq!o1Jdj45SY1`_dhae$fg*gtF`e;P zJd#5>7UBjE>6!!X>s^SiSkV-0D8IghFJT|{ye{~oHaxXnKs{o`kH}Ex6f?`*XJ(iT z=E7?RMJ3XJe9D5O%jZtdDA*?z_>YwJ0pnG^DJG*hA1muT*0#3I-DvB91VBV!7#6X6LHthl0Q-7P)~YLzMcL*49KD9YEEk2`Pkk< zmQwH(D*Z}pL`k0b)Mb>AEB)E3DKDcu>!b4GSL3}nmZaPbeMJ9Kwv`l8&O+axWm+ZQ z-|0d9El07HQT_Jur>+*7+F8&xx*R&M%tjc(Lk{w~jq)#wMhNq?_i|hxT&1r%<;)W* vzcCqI>NHjUf*9Cxx6#mtgg&)855+-_#