├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── azure-pipelines.yml ├── PSAction1.psd1 ├── README.md └── PSAction1.psm1 /.gitignore: -------------------------------------------------------------------------------- 1 | #NO PS1 2 | *.ps1 3 | 4 | #NO TXT 5 | *.txt 6 | 7 | #NO ZIP 8 | *.zip -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "PowerShell: Launch Current File", 9 | "type": "PowerShell", 10 | "request": "launch", 11 | "script": "${file}", 12 | "args": [] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Action1Corp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | 3 | variables: 4 | - group: PSACTION1_SIGNATURE 5 | 6 | resources: 7 | repositories: 8 | - repository: self 9 | type: github 10 | name: Action1Corp/PSAction1 11 | endpoint: 'Action1 GitHub' 12 | branch: 'main' 13 | 14 | pool: 15 | vmImage: 'windows-latest' 16 | 17 | jobs: 18 | - job: SignScripts 19 | displayName: 'Sign scripts' 20 | steps: 21 | 22 | - checkout: self 23 | clean: true 24 | fetchDepth: 0 25 | persistCredentials: true 26 | 27 | - task: PowerShell@2 28 | displayName: 'Prepare PowerShell Files to Sign' 29 | inputs: 30 | targetType: inline 31 | script: | 32 | Get-ChildItem -Path "$(Build.SourcesDirectory)" -Recurse -Include *.ps1, *.psm1, *.psd1 -File | 33 | ForEach-Object { 34 | $filePath = "$($_.FullName)" 35 | $fileBaseName = (Get-Item $filePath).BaseName 36 | $fileExtension = (Get-Item $filePath).Extension 37 | $signedFilePath = Join-Path -Path "$(Build.ArtifactStagingDirectory)" -ChildPath "$fileBaseName$fileExtension" 38 | Copy-Item -Path $filePath -Destination $signedFilePath 39 | } 40 | 41 | - task: AzureCLI@2 42 | displayName: 'Prepare Azure Secure Connection' 43 | inputs: 44 | azureSubscription: 'Action1 Azure' 45 | scriptType: 'pscore' 46 | scriptLocation: 'inlineScript' 47 | inlineScript: | 48 | Write-Host "##vso[task.setvariable variable=ARM_CLIENT_ID]$env:servicePrincipalId" 49 | Write-Host "##vso[task.setvariable variable=ARM_TENANT_ID]$env:tenantId" 50 | Write-Host "##vso[task.setvariable variable=ARM_ID_TOKEN]$env:idToken" 51 | addSpnToEnvironment: true 52 | 53 | - task: PowerShell@2 54 | displayName: 'Establish Azure Secure Connection' 55 | inputs: 56 | targetType: 'inline' 57 | script: | 58 | az login --service-principal -u $(ARM_CLIENT_ID) --tenant $(ARM_TENANT_ID) --allow-no-subscriptions --federated-token $(ARM_ID_TOKEN) 59 | 60 | - task: TrustedSigning@0 61 | displayName: 'Azure Trusted Signing' 62 | inputs: 63 | ExcludeSharedTokenCacheCredential: true 64 | ExcludeVisualStudioCredential: true 65 | ExcludeVisualStudioCodeCredential: true 66 | Endpoint: '$(AZURE_TS_ENDPOINT)' 67 | CertificateProfileName: '$(AZURE_TS_CERTIFICATE_PROFILE_NAME)' 68 | FilesFolder: '$(Build.ArtifactStagingDirectory)' 69 | FilesFolderFilter: 'psm1,psd1,ps1' 70 | FilesFolderRecurse: true 71 | FileDigest: 'SHA256' 72 | CodeSigningAccountName: '$(AZURE_TS_ACCOUNT_NAME)' 73 | 74 | - task: PublishBuildArtifacts@1 75 | displayName: 'Publish Internally' 76 | condition: always() 77 | inputs: 78 | PathtoPublish: '$(Build.ArtifactStagingDirectory)' 79 | ArtifactName: 'release' 80 | publishLocation: 'Container' 81 | -------------------------------------------------------------------------------- /PSAction1.psd1: -------------------------------------------------------------------------------- 1 | # Name: PSAction1 2 | # Description: Powershell module for working with the Aciton1 API. 3 | # Copyright (C) 2024 Action1 Corporation 4 | # Documentation: https://github.com/Action1Corp/PSAction1/ 5 | # Use Action1 Roadmap system (https://roadmap.action1.com/) to submit feedback or enhancement requests. 6 | 7 | # WARNING: Carefully study the provided scripts and components before using them. Test in your non-production lab first. 8 | 9 | # LIMITATION OF LIABILITY. IN NO EVENT SHALL ACTION1 OR ITS SUPPLIERS, OR THEIR RESPECTIVE 10 | # OFFICERS, DIRECTORS, EMPLOYEES, OR AGENTS BE LIABLE WITH RESPECT TO THE WEBSITE OR 11 | # THE COMPONENTS OR THE SERVICES UNDER ANY CONTRACT, NEGLIGENCE, TORT, STRICT 12 | # LIABILITY OR OTHER LEGAL OR EQUITABLE THEORY (I)FOR ANY AMOUNT IN THE AGGREGATE IN 13 | # EXCESS OF THE GREATER OF FEES PAID BY YOU THEREFOR OR $100; (II) FOR ANY INDIRECT, 14 | # INCIDENTAL, PUNITIVE, OR CONSEQUENTIAL DAMAGES OF ANY KIND WHATSOEVER; (III) FOR 15 | # DATA LOSS OR COST OF PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; OR (IV) FOR ANY 16 | # MATTER BEYOND ACTION1’S REASONABLE CONTROL. SOME STATES DO NOT ALLOW THE 17 | # EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THE ABOVE 18 | # LIMITATIONS AND EXCLUSIONS MAY NOT APPLY TO YOU. 19 | 20 | @{ 21 | 22 | # Script module or binary module file associated with this manifest. 23 | RootModule = 'PSAction1.psm1' 24 | 25 | # Version number of this module. 26 | ModuleVersion = '1.4.6' 27 | 28 | # Supported PSEditions 29 | # CompatiblePSEditions = @() 30 | 31 | # ID used to uniquely identify this module 32 | GUID = 'e5ede30e-11cd-442c-87f8-478d2ef0a4c0' 33 | 34 | # Author of this module 35 | Author = 'Gene Moody' 36 | 37 | # Company or vendor of this module 38 | CompanyName = 'Action1 Corporation' 39 | 40 | # Copyright statement for this module 41 | Copyright = '(c) 2025 Action1 Corporation. All rights reserved.' 42 | 43 | # Description of the functionality provided by this module 44 | Description = 'API Interface for Action1' 45 | 46 | # Minimum version of the Windows PowerShell engine required by this module 47 | PowerShellVersion = '5.1' 48 | 49 | # Name of the Windows PowerShell host required by this module 50 | # PowerShellHostName = '' 51 | 52 | # Minimum version of the Windows PowerShell host required by this module 53 | # PowerShellHostVersion = '' 54 | 55 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 56 | # DotNetFrameworkVersion = '' 57 | 58 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 59 | # CLRVersion = '' 60 | 61 | # Processor architecture (None, X86, Amd64) required by this module 62 | # ProcessorArchitecture = '' 63 | 64 | # Modules that must be imported into the global environment prior to importing this module 65 | # RequiredModules = @() 66 | 67 | # Assemblies that must be loaded prior to importing this module 68 | # RequiredAssemblies = @() 69 | 70 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 71 | # ScriptsToProcess = @() 72 | 73 | # Type files (.ps1xml) to be loaded when importing this module 74 | # TypesToProcess = @() 75 | 76 | # Format files (.ps1xml) to be loaded when importing this module 77 | # FormatsToProcess = @() 78 | 79 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 80 | # NestedModules = @() 81 | 82 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 83 | FunctionsToExport = 'Set-Action1Credentials', 84 | 'Set-Action1DefaultOrg', 85 | 'Set-Action1Locale', 86 | 'Set-Action1Region', 87 | 'Set-Action1Debug', 88 | 'New-Action1', 89 | 'Get-Action1', 90 | 'Update-Action1', 91 | 'Set-Action1Interactive', 92 | 'Start-Action1Requery', 93 | 'Start-Action1PackageUpload' 94 | 95 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 96 | CmdletsToExport = '*' 97 | 98 | # Variables to export from this module 99 | VariablesToExport = '*' 100 | 101 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 102 | AliasesToExport = '*' 103 | 104 | # DSC resources to export from this module 105 | # DscResourcesToExport = @() 106 | 107 | # List of all modules packaged with this module 108 | # ModuleList = @() 109 | 110 | # List of all files packaged with this module 111 | # FileList = @() 112 | 113 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 114 | PrivateData = @{ 115 | 116 | PSData = @{ 117 | 118 | # Tags applied to this module. These help with module discovery in online galleries. 119 | Tags = @('Action1') 120 | 121 | # A URL to the license for this module. 122 | # LicenseUri = '' 123 | 124 | # A URL to the main website for this project. 125 | ProjectUri = 'https://github.com/Action1Corp/PSAction1' 126 | 127 | # A URL to an icon representing this module. 128 | # IconUri = '' 129 | 130 | # ReleaseNotes of this module 131 | # ReleaseNotes = '' 132 | 133 | } # End of PSData hashtable 134 | 135 | } # End of PrivateData hashtable 136 | 137 | # HelpInfo URI of this module 138 | # HelpInfoURI = '' 139 | 140 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 141 | # DefaultCommandPrefix = '' 142 | 143 | } 144 | 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![_Company_Logo](https://www.action1.com/wp-content/uploads/2022/02/action1-logo.svg) 2 | **Patch Management That Just Works** 3 | 4 | [**First 200 endpoints are free, fully featured, forever.**](https://www.action1.com/free) 5 | *** 6 | # PSAction1 - PowerShell interface to the Action1 API 7 | 8 | :stop_sign: **IMPORTANT, READ CAREFULLY!** _The module provided and outlined here along with examples are for an convenience. Though care has been taken to provide proper function and no adverse effects, each environment can be different. Carefully read, understand, and test before using in production, executing it on a test environment first. It is the responsibility of you, the user, to ensure all use function behaves as expected and creates no adverse conditions. Your use implies you have read and accept these conditions._ 9 | 10 | ## Install from PowerShell Gallery 11 | Once installed from the PowerShell gallery, this module will remain resident and does not have to be explicitly imported on each execution. 12 | ```PowerShell 13 | 14 | PS C:\> Install-Module PSAction1 15 | 16 | ``` 17 | 18 | ## Install manually 19 | 20 | If you would prefer to review the code prior to use you can download the module and put it manually in your `$PSModulePath`. 21 | Download it from here for the latest builds, or the [PowerShell Gallery](https://www.PowerShellgallery.com/packages/PSAction1) for the lastest stable release. 22 | Then import it into your script's session, this will need to be done on each execution of your script, so it is advised to make this the first line of the script before any other code. 23 | 24 | :stop_sign: **Important:** _Code downloaded here will be in active development, for maximum stability you should use the module from the PowerShell gallery. You should only use the latest build from Git if you are instructed to do so by support, curious, troubleshooting a specific issue, or just the curious sort of person._ 25 | ```PowerShell 26 | 27 | PS C:\> Import-Module PSAction1 28 | 29 | ``` 30 | 31 | ## Getting started 32 | 33 | Before you begin, you will need to understand the basics of how to authenticate to the Action1 API. 34 | The getting started guide can be found here. 35 | [https://www.action1.com/api-documentation](https://www.action1.com/api-documentation/api-credentials/) 36 | 37 | Once you have followed the instructions to obtain an API key, you should have an "APIKey" (Client ID) value and "Secret" (Client Secret) value. 38 | 39 | 40 | **You are now ready to get started, let's GO!** :tada: 41 | 42 | 43 | ## Using this module 44 | 45 | The first order of operation is to set up authentication, if you do not supply these values beforehand, the script will error telling you what required value is missing. Alteratively if you would rather walk through it step by step, you can set the option **Set-Action1Interactive $true**. Authentication sessions have a timeout, but the module accounts for that. 46 | 47 | When PSAction1 stores the bearer token for you, it checks before use, and if necessary will renew it on demand. This does introduce a very small delay when this happens much like first use, but the impact is minimal and likely not even noticed if not explained. When debug is on, you will see this in process. So once authenticated, there is no furher authentication required for the duration of the session regardless of length. 48 | 49 | :stop_sign: **Important:** _These are example values and DO NOT belong to a live instance, substitute the values with those obtained from the instructions above._ 50 | 51 | ```PowerShell 52 | PS C:\> Set-Action1Region NorthAmerica # Choices are currently NorthAmerica and Europe, more coming soon. 53 | PS C:\> Set-Action1Credentials -APIKey api-key-example_e0983b7c-45e8-4c82-9f98-b63bdc4dcb33@action1.com -Secret 652b47a18e212e695e9fbfaa 54 | 55 | ``` 56 | Next you will have to set an organization context. 57 | Each organization will have a unique ID, you can locate it in the url when you log into Action1. 58 | By default there is only one organization. If you are a Managed Service Provider (MSP) or an enterprise with multiple entities, you can create multiple organizations to separate their data from each other. 59 | 60 | https[]()://app.action1.com/console/dashboard?org=**88c8b425-871e-4ff6-9afc-00df8592c6db** <- This is your Org_ID 61 | 62 | Like the APIKey and Secret, this value is remembered for the duration of the session, as well if not specified beforehand, you will be prompted when needed. If you wish to do something in the context of another organization, you need to sets the context before performing additional actions. 63 | 64 | :stop_sign: **Important:** _You can only operate in the context of one organization at a time as all functions relate to a specific organization._ 65 | 66 | ```PowerShell 67 | 68 | PS C:\> Set-Action1DefaultOrg -Org_ID 88c8b425-871e-4ff6-9afc-00df8592c6db 69 | 70 | ``` 71 | 72 | ### You are all set up, let's do something useful. 73 | 74 | There are five main commands: 75 | - **Get-Action1** 76 | - Retrieves data only makes no changes to actual instance. 77 | - **New-Action1** 78 | - Creates items and returns the new object. 79 | - **Set-Action1[KeyWord]** 80 | - Sets values in module only, does not interact with server data directly. 81 | - **Update-Action1** 82 | - Used to modify or delete items. 83 | - **Start-Action1Requery** 84 | - Used to request the system do a refresh of data. 85 | 86 | Let's start by querying endpoints. 87 | 88 | ```PowerShell 89 | 90 | PS C:\> Get-Action1 -Query Endpoints | select -First 1 91 | 92 | id : ef17c844-5b7c-4b32-9724-f2716b596639 93 | type : Endpoint 94 | self : https://app.action1.com/api/3.0/endpoints/managed/88c8b425-871e-4ff6-9afc-00df8592c6db/ef17c844-5b7c-4b32-9724-f2716b596639/general 95 | status : Connected 96 | last_seen : 2023-12-09_01-44-11 97 | name : A1DEV 98 | address : 192.168.0.135 99 | OS : Windows 11 (23H2) 100 | platform : Windows_64 101 | agent_version : 5.179.579.1 102 | agent_install_date : 2023-11-08_19-11-15 103 | subscription_status : Active 104 | user : A1DEV\gmoody 105 | comment : None 106 | device_name : 107 | MAC : 08:00:27:60:A8:9D 108 | serial : 0 109 | reboot_required : No 110 | online_status : UNDEFINED 111 | AD_organization_unit : 112 | AD_security_groups : {} 113 | CPU_name : Intel(R) Core(TM) i7-1065G7 CPU @ 1.30GHz 114 | CPU_size : 1x1.5 GHz, 4/4 Cores 115 | disk : 80Gb Generic 116 | manufacturer : innotek GmbH 117 | NIC : Intel(R) PRO/1000 MT Desktop Adapter 118 | video : VirtualBox Graphics Adapter (WDDM), , 0Gb 119 | WiFi : 120 | RAM : 0Gb Unknown 121 | last_boot_time : 2023-12-08_22-12-38 122 | update_status : UNDEFINED 123 | vulnerability_status : UNDEFINED 124 | 125 | ``` 126 | :left_speech_bubble: **Note:** _All endpoints have custom attributes that can be set via direct syntax per ID/Attribute._ 127 | 128 | ```PowerShell 129 | 130 | PS C:\> Update-Action1 Modify CustomAttribute -Id 'ef17c844-5b7c-4b32-9724-f2716b596639' -AttributeName "Custom Attribute 1" -AttributeValue "test this" 131 | 132 | ``` 133 | 134 | 135 | ### The list of additional query expressions: 136 | 137 | - Automations 138 | - AdvancedSettings 139 | - Apps 140 | - EndpointGroupMembers 141 | - EndpointGroups 142 | - Me 143 | - Endpoint 144 | - EndpointApps 145 | - Endpoints 146 | - MissingUpdates 147 | - Organizations 148 | - Packages 149 | - Policy 150 | - Policies 151 | - PolicyResults 152 | - ReportData 153 | - ReportExport 154 | - Reports 155 | - Scripts 156 | - AgentDepoyment 157 | - Vulnerabilities 158 | - RawURI 159 | - Settings 160 | 161 | :left_speech_bubble: **Note:** _Notice here that some queries are plural some are singular, all that are plural return a collection of items of that type. 162 | 163 | The singular ones target an object by its **-Id** property , if the object is not found, the return will be NULL, and you will receive an error message indicating the specified ID could not be found. Also, notice here I did not specify **-Query**, as it is the first bound param, it can be implied._ 164 | 165 | This returns the same object as above, directly without having to pull all objects and search, which is multifold more efficient. 166 | 167 | ```PowerShell 168 | 169 | PS C:\> Get-Action1 Endpoint -Id ef17c844-5b7c-4b32-9724-f2716b596639 170 | 171 | ``` 172 | 173 | Let's do more than just look, let's change something! 174 | 175 | - I start by querying the groups, to find one with the name matching what we are looking for. 176 | - Next I just verified the group object visually, this is not required and is just for demonstration. 177 | - Next I made a clone of that group object, this would duplicate the group into an editable template to push back for creation. 178 | - And again I just verify for demonstration. 179 | - I rename that object, and then create a new group based on that data. 180 | - The object is returned with the id and details of the newly created group. 181 | 182 | :left_speech_bubble: **Note:** _Not all objects support creation and or cloning, the module will inform you in these cases._ 183 | 184 | ```PowerShell 185 | PS C:\> $group = Get-Action1 EndpointGroups | ?{$_.name -eq 'Service'} 186 | PS C:\> $group 187 | 188 | id : Service_1696554367754 189 | type : EndpointGroup 190 | self : https://app.action1.com/api/3.0/endpoints/groups/88c8b425-871e-4ff6-9afc-00df8592c6db/Service_1696554367754 191 | name : Service 192 | description : 193 | include_filter : {@{field_name=OS; field_value=Windows 10; mode=include}, @{field_name=name; field_value=A1DEV; mode=include}} 194 | exclude_filter : {} 195 | contents : https://app.action1.com/api/3.0/endpoints/groups/88c8b425-871e-4ff6-9afc-00df8592c6db/Service_1696554367754/contents 196 | uptime_alerts : @{offline_alerts_enabled=no; offline_alerts_delay=10; online_alerts_enabled=no; user_ids_for_notification=System.Object[]} 197 | 198 | PS C:\> $clone = Get-Action1 Settings -for EndpointGroup -Clone $group.id 199 | PS C:\> $clone | Format-List 200 | 201 | name : Service 202 | description : 203 | include_filter : {@{field_name=OS; field_value=Windows 10; mode=include}, @{field_name=name; field_value=A1DEV; mode=include}} 204 | exclude_filter : {} 205 | 206 | PS C:\> $clone.name = "Some New Name" 207 | PS C:\> $clone | Format-List 208 | 209 | name : Some New Name 210 | description : 211 | include_filter : {@{field_name=OS; field_value=Windows 10; mode=include}, @{field_name=name; field_value=A1DEV; mode=include}} 212 | exclude_filter : {} 213 | 214 | PS C:\> New-Action1 EndpointGroup -Data $clone 215 | 216 | id : Some_New_Name_1702095463270 217 | type : EndpointGroup 218 | self : https://app.action1.com/api/3.0/endpoints/groups/88c8b425-871e-4ff6-9afc-00df8592c6db/Some_New_Name_1702095463270 219 | name : Some New Name 220 | description : 221 | include_filter : {@{field_name=OS; field_value=Windows 10; mode=include}, @{field_name=name; field_value=A1DEV; mode=include}} 222 | exclude_filter : {} 223 | contents : https://app.action1.com/api/3.0/endpoints/groups/88c8b425-871e-4ff6-9afc-00df8592c6db/Some_New_Name_1702095463270/contents 224 | 225 | ``` 226 | 227 | :left_speech_bubble: **Note:** _When using **-Clone** it accepts an Id as a param, so it implies **-Id**_ 228 | 229 | The syntax for modifying should come naturally when you know how to query and create but we do it with Update-Action1. 230 | 231 | ```PowerShell 232 | PS C:\> $group = Get-Action1 EndpointGroups | ?{$_.name -eq 'Some New Name'} 233 | PS C:\> $clone = Get-Action1 Settings -for EndpointGroup -Clone $group.id 234 | PS C:\> $clone.name = "Some Other Name" 235 | PS C:\> Update-Action1 Modify -Type EndpointGroup -Id $group.id -Data $clone 236 | 237 | id : Some_New_Name_1702095463270 238 | type : EndpointGroup 239 | self : https://app.action1.com/api/3.0/endpoints/groups/88c8b425-871e-4ff6-9afc-00df8592c6db/Some_New_Name_1702100718378 240 | name : Some Other Name 241 | description : 242 | include_filter : {@{field_name=OS; field_value=Windows 10; mode=include}} 243 | exclude_filter : {} 244 | contents : https://app.action1.com/api/3.0/endpoints/groups/88c8b425-871e-4ff6-9afc-00df8592c6db/Some_New_Name_1702100718378/contents 245 | 246 | ``` 247 | 248 | Cloning is useful when you have an object that is mostly what you want and want to tweak for another purpose, but you _can_ start from scratch as well. 249 | Both in Clones and New Settings, there are helper methods to add things like include/exclude filters. 250 | 251 | ```PowerShell 252 | PS C:\> $NewGroup = Get-Action1 Settings -For EndpointGroup 253 | PS C:\> $NewGroup | Format-List 254 | 255 | name : 256 | description : 257 | include_filter : {} 258 | exclude_filter : {} 259 | 260 | C:\> $NewGroup.Splat("MyNewGroup","The group I just Created") 261 | 262 | name description include_filter exclude_filter 263 | ---- ----------- -------------- -------------- 264 | MyNewGroup The group I just Created {} {} 265 | 266 | PS C:\> $NewGroup.AddIncludeFilter('name','A1DEV','include') 267 | PS C:\> New-Action1 EndpointGroup -Data $NewGroup 268 | 269 | id : MyNewGroup_1702147189271 270 | type : EndpointGroup 271 | self : https://app.action1.com/api/3.0/endpoints/groups/88c8b425-871e-4ff6-9afc-00df8592c6db/MyNewGroup_1702147189271 272 | name : MyNewGroup 273 | description : The group I just Created 274 | include_filter : {@{field_value=A1DEV; field_name=name; mode=include}} 275 | exclude_filter : {} 276 | contents : https://app.action1.com/api/3.0/endpoints/groups/88c8b425-871e-4ff6-9afc-00df8592c6db/MyNewGroup_1702147189271/contents 277 | 278 | ``` 279 | 280 | These helper methods are usually to manage group actions where more than one value is set at once, handle object collections where multiple values must be set on one object and specific case/structure must be enforced, or perform bulk actions. 281 | 282 | Examples of all three being as follows, using an Automation clone as an example. 283 | In this case we specify just the ID of the Endpoint or EndpointGroup as "Endpoint/EndpointGroup" is implied by the method name. 284 | The method ensures that the case sensitive attributes that are implied here, as well as the JSONs formating, are created properly using ID alone as a param. 285 | Clear methods require no params as they imply an absolute action. 286 | 287 | :left_speech_bubble: **Note:** _When using Delete...() methods, the identifier used will be either the ID or the Name of the objet to be removed. This will vary in cases such as in EndpointGroups where 'Filters' are added by name, in Automations 'Endpoints/EndpointGroups' are added by ID._ 288 | 289 | ```PowerShell 290 | PS C:\> $clone = get-Action1 Settings -For Automation -Clone PolicyStore_Do_this_thing_1699034505782 291 | PS C:\> $clone.ClearEndpoints() 292 | PS C:\> $clone.AddEndpoint('ef17c844-5b7c-4b32-9724-f2716b596639') 293 | PS C:\> $clone.AddEndpointGroup('Service_1696554367754') 294 | PS C:\> $clone 295 | 296 | name : Policy Store Do this thing 297 | settings : DISABLED 298 | retry_minutes : 1440 299 | endpoints : {@{id=ef17c844-5b7c-4b32-9724-f2716b596639; type=Endpoint}, @{id=Service_1696554367754; type=EndpointGroup}} 300 | actions : {@{name=Run Command; template_id=run_script; params=; id=Run_Command_0d499d60-7a73-11ee-a574-3509a7afa959}} 301 | 302 | PS C:\> $clone.DeleteEndpointGroup('Service_1696554367754') 303 | PS C:\> $clone 304 | 305 | name : Policy Store Do this thing 306 | settings : DISABLED 307 | retry_minutes : 1440 308 | endpoints : {@{id=ef17c844-5b7c-4b32-9724-f2716b596639; type=Endpoint}} 309 | actions : {@{name=Run Command; template_id=run_script; params=; id=Run_Command_0d499d60-7a73-11ee-a574-3509a7afa959}} 310 | 311 | ``` 312 | 313 | :left_speech_bubble: **Note:** _It is also both important and comforting to note here, that all of these these actions are being performed on an in memory object client side. None of these changes are actually committed to the server, until the object is passed as the **-Data** param to an execution of an **Update-Action1** or **New-Action1**. Changes made here can be made, reviewed, or discarded without commitment, with no adverse effects. So please do review and get familiar with how these helper methods work before committing them to the server. A good primer in their function will be to create an object in the Action1 console, and then pull that object into PSAction1. Look at how the data comes structured in the system, methods will format data following that pattern._ 314 | 315 | Then we could can delete an object, so let's target the clone we just made and pushed up. 316 | Delete operations prompt for confirmation by default, **-Force** overrides that behavior. 317 | 318 | :stop_sign: **Important:** _Deleting an object is irreversible. Use extreme scrutiny and caution when deleting, **ESPECIALLY** if utilizing the **-Force** option!_ 319 | 320 | ```PowerShell 321 | PS C:\> Update-Action1 Delete -Type Group -Id MyNewGroup_1702147189271 -Force 322 | ``` 323 | ### Deploying patches and software packages 324 | 325 | As of version 1.3.8, you can now deploy both patches and software packages through PSAction1! Like many of the other actions, it starts by getting a settings template for the operation, adding relevant information about what you would like to deploy, and then creating a new object in Action1 to kick it off. In this case the type of object created is a policy instance. This is a special type of automation that runs once on demand and does not leave a template in the automations section inside Action1, but it can still be found in the history of any endpoint that it was assigned to. 326 | 327 | Let’s look at an example of issuing a remediation to an endpoint for a particular vulnerability. When patching a vulnerability, it is identified by its CVS id, so we start by getting a Remediation settings template, adding one or more CVE’s to be addressed to it, assigning it to one or more endpoint groups, and then push it back to the server. 328 | 329 | :left_speech_bubble: **Note:** _It is not only likely, it is common, that a single patch will address multiple CVEs in one install. PSAction1 will intelligently address this by detecting that the patch for any given CVE is already added to the queue if you attempt to add additional CVEs from the same patch. This allows you to add all CVEs you wish to address, and the resulting patch list will resolve itself. However, note as well, that all CVEs addressed by that patch will be covered, not **just** the one you added._ 330 | 331 | ```Powershell 332 | PS C:\> $push = Get-Action1 Settings -For Remediation 333 | PS C:\> $push.AddCVE('CVE-2022-3775') 334 | PS C:\> $push.AddEndpointGroup('Test_1720748341834') 335 | PS C:\> New-Action1 Remediation -Data $push 336 | ``` 337 | Software packages get deployed in much the same way, they just require a package id, the script will automatically select the correct version as being the latest available for the package requested. Like remediation, the queue can have one or more packages added, and will prevent you from adding the same package twice. Also, there are helper methods to add endpoints and endpoint groups. So, we create a template object, add a software package, add endpoints and then create a new policy instance object in Action1. Like a remediation this special type of automation will show in the endpoint automation history, but not the Automation section in the Action1 console. 338 | 339 | ```Powershell 340 | PS C:\> $data = Get-Action1 Settings -For DeploySoftware 341 | PS C:\> $data.AddEndpoint('bfbd1da2-d746-44dc-9c87-89382bbd4c53') 342 | PS C:\> $data.AddPackage('Martin_P_ikryl_WinSCP_1632062504985_builtin') #ID of package from Get-Action1 Packages 343 | PS C:\> New-Action1 DeploySoftware -Data $data 344 | ``` 345 | 346 | ### Reporting 347 | 348 | You can pull report data as well through PSAction1, reports an be retrieved as objects for property manipulation, such as ... 349 | 350 | ```PowerShell 351 | PS C:\> Get-Action1 ReportData -Id 'installed_software_1635264799139' 352 | 353 | id : ZDesigner%2520Windows%2520Printer%2520Driver%2520Version type : ReportRow self : https://app.action1.com/api/3.0/reportdata/df137c59-f12a-03c6-7b7e-63701cb6eba3/installed_software_1635264799139/data/ZDesigner%2520Windows%2520Printer%2520Driver%2520Version 354 | fields : @{Name=ZDesigner Windows Printer Driver Version; Details=2} 355 | drilldown_field : Details 356 | drilldown : https://app.action1.com/api/3.0/reportdata/df137c59-f12a-03c6-7b7e-63701cb6eba3/installed_software_1635264799139/data/ZDesigner%2520Windows%2520Printer%2520Driver%2520Version/drilldown 357 | 358 | id : Zebra%2520Font%2520Downloader 359 | type : ReportRow 360 | self : https://app.action1.com/api/3.0/reportdata/df137c59-f12a-03c6-7b7e-63701cb6eba3/installed_software_1635264799139/data/Zebra%2520Font%2520Downloader 361 | fields : @{Name=Zebra Font Downloader; Details=1} 362 | drilldown_field : Details 363 | drilldown : https://app.action1.com/api/3.0/reportdata/df137c59-f12a-03c6-7b7e-63701cb6eba3/installed_software_1635264799139/data/Zebra%2520Font%2520Downloader/drilldown 364 | 365 | ... 366 | ``` 367 | This retrieves an object collection with the id and other details of each of your report objects. Mostly this is useful for determining the id of a particular report you would like to retrieve data for. 368 | 369 | Then you can use that id to pull the actual data in CSV format for integration with or consumption by other systems. 370 | 371 | ```PowerShell 372 | PS C:\> Get-Action1 ReportExport -Id 'installed_software_1635264799139' 373 | 374 | Name,Details 375 | ZDesigner Windows Printer Driver Version,2 376 | Zebra Font Downloader,1 377 | 378 | ... 379 | ``` 380 | 381 | Last but not least, is that because report data is polled, there is a chance when you check at any instant all data will not be up to the minute current. 382 | 383 | ```PowerShell 384 | PS C:\> Start-Action1Requery -Type InstalledSoftware 385 | PS C:\> Start-Action1Requery -Type InstalledSoftware -Endpoint_Id 'ef17c844-5b7c-4b32-9724-f2716b596639' 386 | ``` 387 | 388 | These statements are non-blocking, meaning they initiate a re-query of the data, but the re-query is not instantaneous and can vary depending on your particular deployment. Therefore an immediate attempt to export data again may or may not contain the complete information set from this request. After a reasonable period however it should improve the accuracy of the reported data for all endpoints that are reachable. In the case of **ReportData** and **InstalledSoftware**, these re-query actions can be made as granular as the endpoint, however in the case of **InstaledUpdates** it is only system wide. 389 | 390 | :left_speech_bubble: **Note:** _When this request it made it will be honored the next time an endpoint is visible, it has no affect on offline endpoints until they reconnect.._ 391 | 392 | ### Extending / testing / playground 393 | 394 | This interface is not exhaustive, it used the most commonly requested features of the API, but the API is far larger and feature rich than represented here. That said, the PSAction1 module can still assist. You can use the authentication mechanism to run custom URIs for the purpose of rapidly exploring the API or extending it for more function. To do this the PSAction1 module contains a RawURI method in Get-Action1. 395 | 396 | ```PowerShell 397 | 398 | PS C:\> Get-Action1 RawURI -URI https://app.action1.com/api/3.0/policies/schedules/88c8b425-871e-4ff6-9afc-00df8592c6db/PolicyStore_Do_this_thing_1699034505782 399 | 400 | ``` 401 | 402 | ### Troubleshooting 403 | 404 | At any time you can enable and disable debug to get more information about what is occurring "under the hood", and what is being exchanged with the server. This is especially useful when looking at JSON POST/PATCH data going to the server. 405 | 406 | ```PowerShell 407 | PS C:\> Set-Action1Debug $true 408 | Action1 Debug: Debugging enabled. 409 | PS C:\> Set-Action1Debug $false 410 | ``` 411 | 412 | ### And you can always reach out to myself or the community directly on our [Discord](https://discord.com/channels/841428478669881356/841428479266258946) server or our [Reddit](https://www.reddit.com/r/Action1/) sub. 413 | 414 | ## WARNING: Carefully study the provided scripts and components before using them. Test in your non-production lab first. 415 | 416 | LIMITATION OF LIABILITY. IN NO EVENT SHALL ACTION1 OR ITS SUPPLIERS, OR THEIR RESPECTIVE OFFICERS, DIRECTORS, EMPLOYEES, OR AGENTS BE LIABLE WITH RESPECT TO THE WEBSITE OR THE COMPONENTS OR THE SERVICES UNDER ANY CONTRACT, NEGLIGENCE, TORT, STRICT LIABILITY OR OTHER LEGAL OR EQUITABLE THEORY (I)FOR ANY AMOUNT IN THE AGGREGATE IN EXCESS OF THE GREATER OF FEES PAID BY YOU THEREFOR OR $100; (II) FOR ANY INDIRECT, INCIDENTAL, PUNITIVE, OR CONSEQUENTIAL DAMAGES OF ANY KIND WHATSOEVER; (III) FOR DATA LOSS OR COST OF PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; OR (IV) FOR ANY MATTER BEYOND ’S REASONABLE CONTROL. SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THE ABOVE LIMITATIONS AND EXCLUSIONS MAY NOT APPLY TO YOU. 417 | -------------------------------------------------------------------------------- /PSAction1.psm1: -------------------------------------------------------------------------------- 1 | # Name: PSAction1 2 | # Description: Powershell module for working with the Aciton1 API. 3 | # Copyright (C) 2024 Action1 Corporation 4 | # Documentation: https://github.com/Action1Corp/PSAction1/ 5 | # Use Action1 Roadmap system (https://roadmap.action1.com/) to submit feedback or enhancement requests. 6 | 7 | # WARNING: Carefully study the provided scripts and components before using them. Test in your non-production lab first. 8 | 9 | # LIMITATION OF LIABILITY. IN NO EVENT SHALL ACTION1 OR ITS SUPPLIERS, OR THEIR RESPECTIVE 10 | # OFFICERS, DIRECTORS, EMPLOYEES, OR AGENTS BE LIABLE WITH RESPECT TO THE WEBSITE OR 11 | # THE COMPONENTS OR THE SERVICES UNDER ANY CONTRACT, NEGLIGENCE, TORT, STRICT 12 | # LIABILITY OR OTHER LEGAL OR EQUITABLE THEORY (I)FOR ANY AMOUNT IN THE AGGREGATE IN 13 | # EXCESS OF THE GREATER OF FEES PAID BY YOU THEREFOR OR $100; (II) FOR ANY INDIRECT, 14 | # INCIDENTAL, PUNITIVE, OR CONSEQUENTIAL DAMAGES OF ANY KIND WHATSOEVER; (III) FOR 15 | # DATA LOSS OR COST OF PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; OR (IV) FOR ANY 16 | # MATTER BEYOND ACTION1’S REASONABLE CONTROL. SOME STATES DO NOT ALLOW THE 17 | # EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THE ABOVE 18 | # LIMITATIONS AND EXCLUSIONS MAY NOT APPLY TO YOU. 19 | 20 | $Script:Action1_APIKey 21 | $Script:Action1_Secret 22 | $Script:Action1_Token 23 | $script:Action1_Hosts = [ordered]@{'North America' = 'https://app.action1.com/api/3.0'; Europe = 'https://app.eu.action1.com/api/3.0'; Australia = 'https://app.au.action1.com/api/3.0' } 24 | $Script:Action1_BaseURI = '' 25 | $Script:Action1_Default_Org 26 | $Script:Action1_DebugEnabled = $false 27 | $Script:Action1_Interactive = $false 28 | $Script:Action1_CVE_Lookup = @{} 29 | 30 | $URILookUp = @{ 31 | G_AdvancedSettings = { param($Org_ID) "/setting_templates/$Org_ID" } 32 | G_AgentDepoyment = { param($Org_ID) "/endpoints/discovery/$Org_ID" } 33 | G_Apps = { param($Org_ID) "/apps/$Org_ID/data" } 34 | G_AutomationInstances = { param($Org_ID, $Object_ID) "/automations/instances/$Org_ID`?limit=9999&from=0&endpoint_id=$Object_ID" } 35 | G_Automations = { param($Org_ID) "/policies/schedules/$Org_ID" } 36 | G_Endpoint = { param($Org_ID, $Object_ID) "/endpoints/managed/$Org_ID/$Object_ID" } 37 | G_Endpoints = { param($Org_ID) "/endpoints/managed/$Org_ID`?limit=9999" } 38 | G_EndpointApps = { param($Org_ID, $Object_ID) "/apps/$Org_ID/data/$Object_ID" } 39 | G_EndpointGroupMembers = { param($Org_ID, $Object_ID)"/endpoints/groups/$Org_ID/$Object_ID/contents" } 40 | G_EndpointGroups = { param($Org_ID) "/endpoints/groups/$Org_ID" } 41 | G_Logs = { param($Org_ID) "/logs/$Org_ID" } 42 | G_Me = { "/Me" } 43 | G_MissingUpdates = { param($Org_ID) "/updates/$Org_ID`?limit=9999" } 44 | G_Organizations = { "/organizations" } 45 | G_Packages = { "/packages/all?limit=9999" } 46 | G_PackageVersions = { param($Object_ID) "/software-repository/all/$Object_ID`?fields=versions" } 47 | G_Policy = { param($Org_ID, $Object_ID) "/policies/instances/$Org_ID/$Object_ID" } 48 | G_Policies = { param($Org_ID) "/policies/instances/$Org_ID" } 49 | G_PolicyResults = { param($Org_ID, $Object_ID) "/policies/instances/$Org_ID/$Object_ID/endpoint_results" } 50 | G_ReportData = { param($Org_ID, $Object_ID)"/reportdata/$Org_ID/$Object_ID/data" } 51 | G_ReportExport = { param($Org_ID, $Object_ID)"/reportdata/$Org_ID/$Object_ID/export" } 52 | G_Reports = { "/reports/all" } 53 | G_Scripts = { "/scripts/all" } 54 | G_Vulnerabilities = { param($Org_ID) "/Vulnerabilities/$Org_ID`?limit=9999" } 55 | N_Automation = { param($Org_ID) "/policies/schedules/$Org_ID" } 56 | N_EndpointGroup = { param($Org_ID) "/endpoints/groups/$Org_ID" } 57 | N_Organization = { "/organizations" } 58 | N_Remediation = { param($Org_ID) "/policies/instances/$Org_ID" } 59 | N_DeferredRemediation = { param($Org_ID) "/policies/schedules/$Org_ID" } 60 | N_DeploySoftware = { param($Org_ID) "/policies/instances/$Org_ID" } 61 | R_ReportData = { param($Org_ID, $Object_ID) "/reportdata/$Org_ID/$Object_ID/requery" } 62 | R_InstalledSoftware = { param($Org_ID, $Object_ID) "/apps/$Org_ID/requery/$Object_ID" } 63 | R_InstalledUpdates = { param($Org_ID) "/updates/installed/$Org_ID/requery" } 64 | U_Endpoint = { param($Org_ID, $Object_ID) "/endpoints/managed/$Org_ID/$Object_ID" } 65 | U_GroupModify = { param($Org_ID, $Object_ID) "/endpoints/groups/$Org_ID/$Object_ID" } 66 | U_GroupMembers = { param($Org_ID, $Object_ID) "/endpoints/groups/$Org_ID/$Object_ID/contents" } 67 | U_Automation = { param($Org_ID, $Object_ID) "/policies/schedules/$Org_ID/$Object_ID" } 68 | } 69 | 70 | #----------------------------------JSON object templates--------------------------------------- 71 | 72 | $RemediationTemplate = @" 73 | { 74 | "name": "", 75 | "retry_minutes": "1440", 76 | "endpoints": [ 77 | { 78 | "id": "ALL", 79 | "type": "EndpointGroup" 80 | } 81 | ], 82 | "actions": [ 83 | { 84 | "name": "Deploy Update", 85 | "template_id": "deploy_update", 86 | "params": { 87 | "display_summary": "", 88 | "packages": [ 89 | { 90 | "default": "default" 91 | } 92 | ], 93 | "update_approval": "manual", 94 | "automatic_approval_delay_days": 7, 95 | "scope": "Specified", 96 | "reboot_options": { 97 | "auto_reboot": "yes", 98 | "show_message": "yes", 99 | "message_text": "Your computer requires maintenance and will be rebooted. Please save all work and reboot now to avoid losing any data.", 100 | "timeout": 240 101 | } 102 | } 103 | } 104 | ] 105 | } 106 | "@ 107 | 108 | $PackageDeployTemplate = @" 109 | { 110 | "name": "", 111 | "retry_minutes": "1440", 112 | "endpoints": [ 113 | { 114 | "id": "ALL", 115 | "type": "EndpointGroup" 116 | } 117 | ], 118 | "actions": [ 119 | { 120 | "name": "Deploy Software", 121 | "template_id": "deploy_package", 122 | "params": { 123 | "display_summary": "", 124 | "packages": [ 125 | { 126 | "default": "default" 127 | } 128 | ], 129 | "reboot_options": { 130 | "auto_reboot": "no" 131 | } 132 | } 133 | } 134 | ] 135 | } 136 | "@ 137 | #----------------------------------JSON object templates--------------------------------------- 138 | 139 | function CheckToken() { 140 | if (($null -ne $Script:Action1_Token) -and ($Script:Action1_Token.expires_at -ge $(Get-Date))) { 141 | Debug-Host "Current token is valid." 142 | return $true 143 | } 144 | else { 145 | Debug-Host "Token not set or expired, fetching new." 146 | if (FetchToken -ne $null ) { 147 | Debug-Host "Token refresh successful." 148 | return $true 149 | } 150 | else { 151 | Write-Error "Token could not be refreshed, check for errors in output." 152 | return $false 153 | } 154 | } 155 | } 156 | 157 | function CheckRoot { 158 | if ($Script:Action1_BaseURI -eq '') { 159 | if ($Script:Action1_Interactive) { 160 | while ($Script:Action1_BaseURI -eq '') { 161 | 0..($Action1_Hosts.Count - 1) | ` 162 | ForEach-Object { 163 | Write-Host "$($_) : $($($Action1_Hosts.Keys -Split '`n')[$_])" }; 164 | $Script:Action1_BaseURI = $($Action1_Hosts.Values -Split '`n')[[int]::Parse($(Read-Host -Prompt 'Select your data center region.'))] 165 | 166 | } 167 | return $true 168 | } 169 | else { 170 | Write-Error "Region not set, call Set-Action1Region prior to making any calls to the API." 171 | exit 172 | } 173 | } 174 | return $true 175 | } 176 | function CheckOrg { 177 | if ($null -eq $Script:Action1_Default_Org) { 178 | if ($Script:Action1_Interactive) { 179 | while ($null -eq $Script:Action1_Default_Org) { Set-Action1DefaultOrg } 180 | } 181 | else { 182 | Write-Error "Default Org not set, call Set-Action1DefaultOrg prior to making any calls to the API." 183 | exit 184 | } 185 | } 186 | return $Script:Action1_Default_Org 187 | } 188 | function FetchToken { 189 | if (CheckRoot) { 190 | if ([string]::IsNullOrEmpty($Script:Action1_APIKey) -or [string]::IsNullOrEmpty($Script:Action1_Secret)) { 191 | if ($Script:Action1_Interactive) { 192 | Set-Action1Credentials 193 | } 194 | else { 195 | Write-Error "Authentication details are not set, call Set-Action1Credentials prior to making any calls to the API." 196 | exit 197 | } 198 | } 199 | try { 200 | $Token = (ConvertFrom-Json -InputObject (Invoke-WebRequest -Uri "$Script:Action1_BaseURI/oauth2/token" -Method POST -UseBasicParsing -Body @{client_id = $Script:Action1_APIKey; client_secret = $Script:Action1_Secret }).Content ) 201 | $Token | Add-Member -MemberType NoteProperty -Name "expires_at" -Value $(Get-Date).AddSeconds(([int]$Token.expires_in - 5)) #Expire token 5 seconds early to avoid race condition timeouts. 202 | $Script:Action1_Token = $Token 203 | return $Token 204 | } 205 | catch [System.Net.WebException] { 206 | Write-Error "Error fetching auth token: $($_)." 207 | Write-Error $Token 208 | return $null 209 | } 210 | } 211 | } 212 | 213 | function BuildArgs { 214 | param ( 215 | [String]$In, 216 | [String]$Add 217 | ) 218 | if ([string]::IsNullOrEmpty($In)) { return $Add }else { return "$In&$Add" } 219 | } 220 | 221 | function DoGet { 222 | param ( 223 | [Parameter(Mandatory)] 224 | [String]$Path, 225 | [Parameter(Mandatory)] 226 | [String]$Label, 227 | [String]$AddArgs, 228 | [switch]$Raw 229 | ) 230 | try { 231 | if ($AddArgs) { $Path += "?{0}" -f $AddArgs } 232 | Debug-Host "GET request to $Path : Raw flag is $Raw" 233 | if ($Raw) { 234 | return (Invoke-WebRequest -Uri $Path -Method GET -UseBasicParsing -Headers @{Authorization = "Bearer $(($Script:Action1_Token).access_token)"; 'Content-Type' = 'application/json; charset=utf-8' }).Content 235 | } 236 | else { 237 | return (ConvertFrom-Json -InputObject (Invoke-WebRequest -Uri $Path -Method GET -UseBasicParsing -Headers @{Authorization = "Bearer $(($Script:Action1_Token).access_token)"; 'Content-Type' = 'application/json; charset=utf-8' }).Content ) 238 | } 239 | } 240 | catch [System.Net.WebException] { 241 | Write-Error "Error fetching $($Label): $($_)." 242 | return $null 243 | } 244 | } 245 | 246 | function Start-Action1PackageUpload { 247 | param( 248 | [Parameter(Mandatory)] 249 | [String]$Package_ID, 250 | [Parameter(Mandatory)] 251 | [String]$Version_ID, 252 | [Parameter(Mandatory)] 253 | [String]$Filename, 254 | [Parameter(Mandatory)] 255 | [ValidateSet( 256 | 'Windows_32', 257 | 'Windows_64' 258 | )] 259 | [String]$Platform, 260 | [int32]$BufferSize = 24Mb 261 | ) 262 | $uri = "$Script:Action1_BaseURI/software-repository/all/$Package_ID/versions/$Version_ID/upload?platform=$Platform" 263 | Debug-Host "Base URI is $uri" 264 | $UploadTarget = "" 265 | Debug-Host "Uploading file: '$Filename'" 266 | Debug-Host "Writing in chunks of $BufferSize bytes." 267 | $FileData = [System.IO.File]::Open($Filename, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) 268 | if ($FileData.Length -lt $BufferSize) { $BufferSize = $FileData.Length; Debug-Host "File is smaller than BufferSize, adjusting to $($FileData.Length)" } 269 | $Buffer = New-Object byte[] $BufferSize 270 | $Place = 0 271 | 272 | $HeaderBase = @{ 273 | 'accept' = '*/*' 274 | 'X-Upload-Content-Type' = 'application/octet-stream' 275 | } 276 | try { 277 | $Headers = $HeaderBase.Clone() 278 | $Headers.Add('X-Upload-Content-Length', $($FileData.Length)) 279 | $Headers.Add('Content-Type', 'application/json') 280 | if (CheckToken) { $Headers.Add('Authorization', "Bearer $(($Script:Action1_Token).access_token)"); Invoke-WebRequest -Uri $uri -Method Post -UseBasicParsing -Headers $Headers -ErrorAction SilentlyContinue } 281 | } 282 | catch { $UploadTarget = $_.Exception.Response.Headers['X-Upload-Location'] } 283 | Debug-Host "Upload URI is $UploadTarget" 284 | while (($Read = $FileData.Read($Buffer, 0, $Buffer.Length)) -gt 0) { 285 | $Headers = $HeaderBase.Clone() 286 | $Headers.Add('Content-Range', "bytes $($Place)-$($($Place + $Read-1))/$($FileData.Length)") 287 | $Headers.Add('Content-Length', "$($Read)") 288 | $Headers.Add('Content-Type', 'application/octet-stream') 289 | $Place += $Read 290 | try { if (CheckToken) { $Headers.Add('Authorization', "Bearer $(($Script:Action1_Token).access_token)"); $response = Invoke-WebRequest -Method Put -UseBasicParsing -Uri $UploadTarget -Body $Buffer -Headers $Headers -ErrorAction SilentlyContinue } } 291 | catch { Debug-Host "Last Status: $($_.Exception.Response.StatusCode)" } 292 | if (($FileData.Length - $Place) -lt $BufferSize) { $buffer = New-Object byte[] ($FileData.Length - $place) } 293 | Debug-Host "Upload $([math]::Round((($Place / $FileData.Length)*100),1))% Complete." 294 | if ($Buffer.Length -eq 0) { Debug-Host "Final Status:$($response.StatusCode)" }else { Debug-Host "Bytes Written: $($Buffer.Length)" } 295 | } 296 | $FileData.Close() 297 | } 298 | 299 | 300 | function Debug-Host { 301 | param( 302 | [Parameter(Mandatory)] 303 | [string]$Message 304 | ) 305 | if ($Script:Action1_DebugEnabled) { Write-Host "Action1 Debug: $Message" -ForegroundColor Blue } 306 | } 307 | 308 | function PushData { 309 | param ( 310 | [Parameter(Mandatory)] 311 | [ValidateSet( 312 | 'PATCH', 313 | 'POST', 314 | 'DELETE' 315 | )] 316 | [string]$Method, 317 | [Parameter(Mandatory)] 318 | [String]$Path, 319 | [Parameter(Mandatory)] 320 | [String]$Label, 321 | [object]$Body 322 | ) 323 | try { 324 | Debug-Host "$Method request to $Path." 325 | if ($data) { Debug-Host "Data to be sent:`n $(ConvertTo-Json -InputObject $Body -Depth 10)" } 326 | return (ConvertFrom-Json -InputObject (Invoke-WebRequest -Uri $Path -Method $Method -UseBasicParsing -Body (ConvertTo-Json -InputObject $Body -Depth 10) -Headers @{Authorization = "Bearer $(($Script:Action1_Token).access_token)"; 'Content-Type' = 'application/json; charset=utf-8' }).Content) 327 | } 328 | catch [System.Net.WebException] { 329 | Write-Error "Error processing $($Label): $($_)" 330 | return $null 331 | } 332 | } 333 | 334 | function Set-Action1Credentials { 335 | param ( 336 | [Parameter(Mandatory)] 337 | [ValidateNotNullOrEmpty()] 338 | [string]$APIKey, 339 | [Parameter(Mandatory)] 340 | [ValidateNotNullOrEmpty()] 341 | [string]$Secret 342 | ) 343 | $Script:Action1_APIKey = $APIKey 344 | $Script:Action1_Secret = $Secret 345 | } 346 | 347 | function Set-Action1Debug { 348 | param( 349 | [Parameter(Mandatory)] 350 | [boolean]$Enabled 351 | ) 352 | $Script:Action1_DebugEnabled = $Enabled 353 | if ($Enabled) { Debug-Host "Debugging enabled." } 354 | } 355 | 356 | function Set-Action1DefaultOrg { 357 | param ( 358 | [Parameter(Mandatory)] 359 | [ValidateNotNullOrEmpty()] 360 | [string]$Org_ID 361 | ) 362 | $Script:Action1_Default_Org = $Org_ID 363 | } 364 | 365 | function Set-Action1Locale { 366 | param ( 367 | [Parameter(Mandatory)] 368 | [ValidateSet('NorthAmerica', 'Europe')] 369 | [String]$Region 370 | ) 371 | Write-Host "Locale set, Note:Set-Action1Locale is being depreciated, please modify all scripts to use Set-Action1Region instead." -ForegroundColor Red 372 | Set-Action1Region -Region $Region 373 | } 374 | 375 | function Set-Action1Region { 376 | param ( 377 | [Parameter(Mandatory)] 378 | [ValidateSet('NorthAmerica', 'Europe', 'Australia')] 379 | [String]$Region 380 | ) 381 | switch ($Region) { 382 | NorthAmerica { $Script:Action1_BaseURI = "https://app.action1.com/api/3.0" } 383 | Europe { $Script:Action1_BaseURI = "https://app.eu.action1.com/api/3.0" } 384 | Australia { $Script:Action1_BaseURI = 'https://app.au.action1.com/api/3.0' } 385 | } 386 | } 387 | 388 | function Set-Action1Interactive { 389 | param( 390 | [Parameter(Mandatory)] 391 | [boolean]$Enabled 392 | ) 393 | if ($Enabled) { Debug-Host "Interactive mode enabled, you will be prompted for variables that are required but not set." } 394 | $Script:Action1_Interactive = $Enabled 395 | } 396 | 397 | function Get-Action1 { 398 | param ( 399 | [Parameter(Mandatory)] 400 | [ValidateSet( 401 | 'AutomationInstances', 402 | 'Automations', 403 | 'AdvancedSettings', 404 | 'Apps', 405 | 'CutomAttribute', 406 | 'EndpointGroupMembers', 407 | 'EndpointGroups', 408 | 'Me', 409 | 'Endpoint', 410 | 'EndpointApps', 411 | 'Endpoints', 412 | 'Logs', 413 | 'MissingUpdates', 414 | 'Organizations', 415 | 'Packages', 416 | 'PackageVersions', 417 | 'Policy', 418 | 'Policies', 419 | 'PolicyResults', 420 | 'ReportData', 421 | 'ReportExport', 422 | 'Reports', 423 | 'Scripts', 424 | 'AgentDepoyment', 425 | 'Vulnerabilities', 426 | 'RawURI', 427 | 'Settings' 428 | )] 429 | [String]$Query, 430 | [string]$Id, 431 | #[int]$Limit, 432 | #[int]$From, 433 | [string]$URI, 434 | [ValidateSet( 435 | 'Automation', 436 | 'Endpoint', 437 | 'EndpointGroup', 438 | 'Organization', 439 | 'GroupAddEndpoint', 440 | 'GroupDeleteEndpoint', 441 | 'GroupFilter', 442 | 'Remediation', 443 | 'DeferredRemediation', 444 | 'DeploySoftware' 445 | )] 446 | [string]$For, 447 | [string]$Clone 448 | ) 449 | #Short out processing path if URI literal is specified. 450 | if ($Query -eq 'RawURI') { if (!$URI) { Write-Error "Error -URI value required when Query is type RawURI.`n"; return $null }else { if (CheckToken) { return DoGet -Path $URI -Label $Query } } } 451 | # Retrieve settings objects for post/patch actions. 452 | if ($Query -eq 'Settings') { 453 | if (!$For) { 454 | Write-Error "Error: -For value must be specified when Query type is 'Settings'.`n"; return $null 455 | } 456 | else { 457 | if ($Clone) { 458 | if ($Query -ne 'Settings') { Write-Error "Clone flag only allowed for query type 'Setings.'`n"; return $null } 459 | switch ($For) { 460 | 'EndpointGroup' { 461 | $Pull = Get-Action1 EndpointGroups | Where-Object { $_.id -eq ($Clone) } 462 | if (!$Pull) { 463 | Write-Error "No $For found matching id $clone.`n"; return $null 464 | } 465 | else { 466 | $sbAddIncludeFilter = { param( [string]$field_name, [string]$field_value) $this.include_filter += New-Object psobject -Property @{field_name = $field_name; field_value = $field_value; mode = 'include' } } 467 | $sbDeleteIncludeFilter = { param([string]$field_name) $this.include_filter = @($this.include_filter | Where-Object { !($_.field_name -eq $field_name) }) } 468 | $sbClearIncludeFilter = { $this.include_filter = @() } 469 | $sbAddExcludeFilter = { param( [string]$field_name, [string]$field_value) $this.exclude_filter += New-Object psobject -Property @{field_name = $field_name; field_value = $field_value; mode = 'include' } } 470 | $sbDeleteExcludeFilter = { param([string]$field_name) $this.exclude_filter = @($this.exclude_filter | Where-Object { !($_.field_name -eq $field_name) }) } 471 | $sbClearExcludeFilter = { $this.exclude_filter = @() } 472 | @('id', 'type', 'self', 'contents', 'uptime_alerts') | ForEach-Object { $Pull.PSObject.Members.Remove($_) } 473 | $Pull | Add-Member -MemberType ScriptMethod -Name "AddIncludeFilter" -Value $sbAddIncludeFilter 474 | $Pull | Add-Member -MemberType ScriptMethod -Name "DeleteIncludeFilter" -Value $sbDeleteIncludeFilter 475 | $Pull | Add-Member -MemberType ScriptMethod -Name "ClearIncludeFilter" -Value $sbClearIncludeFilter 476 | $Pull | Add-Member -MemberType ScriptMethod -Name "AddExcludeFilter" -Value $sbAddExcludeFilter 477 | $Pull | Add-Member -MemberType ScriptMethod -Name "DeleteExcludeFilter" -Value $sbDeleteExcludeFilter 478 | $Pull | Add-Member -MemberType ScriptMethod -Name "ClearExcludeFilter" -Value $sbClearExcludeFilter 479 | return $Pull 480 | } 481 | } 482 | 'Automation' { 483 | $Pull = Get-Action1 Automations | Where-Object { $_.id -eq ($Clone) } 484 | if (!$Pull) { 485 | Write-Error "No $For found matching id $clone." 486 | return $null 487 | } 488 | else { 489 | $sbAddEndpoint = { param([string]$Id) $this.endpoints += New-Object psobject -Property @{id = $Id; type = 'Endpoint' } } 490 | $sbAddEndpointGroup = { param([string]$Id) $this.endpoints += New-Object psobject -Property @{id = $Id; type = 'EndpointGroup' } } 491 | $sbDeleteEndpoint = { param([string]$Id) $this.endpoints = @($this.endpoints | Where-Object { !($_.type -eq 'Endpoint' -and $_.id -eq $Id) }) } 492 | $sbDeleteEndpointGroup = { param([string]$Id) $this.endpoints = @($this.endpoints | Where-Object { !($_.type -eq 'EndpointGroup' -and $_.id -eq $Id) }) } 493 | $sbClearEndpoints = { $this.endpoints = @() } 494 | $sbDeferExecution = { $this.settings = 'DISABLED' } 495 | 496 | @('id', 'type', 'self', 'last_run', 'next_run', 'system', 'randomize_start') | ForEach-Object { $Pull.PSObject.Members.Remove($_) } 497 | $CleanEndpoints = @() 498 | $Pull.endpoints | ForEach-Object { $CleanEndpoints += New-Object psobject -Property @{id = $_.id; type = $_.type } } 499 | $Pull.endpoints = $CleanEndpoints 500 | $Pull | Add-Member -MemberType ScriptMethod -Name "AddEndpoint" -Value $sbAddEndpoint 501 | $Pull | Add-Member -MemberType ScriptMethod -Name "AddEndpointGroup" -Value $sbAddEndpointGroup 502 | $Pull | Add-Member -MemberType ScriptMethod -Name "DeleteEndpoint" -Value $sbDeleteEndpoint 503 | $Pull | Add-Member -MemberType ScriptMethod -Name "DeleteEndpointGroup" -Value $sbDeleteEndpointGroup 504 | $Pull | Add-Member -MemberType ScriptMethod -Name "ClearEndpoints" -Value $sbClearEndpoints 505 | $Pull | Add-Member -MemberType ScriptMethod -Name "DeferExecution" -Value $sbDeferExecution 506 | return $Pull 507 | } 508 | } 509 | default { Write-Error "Invalild request to clone type $For." ; return $null } 510 | } 511 | } 512 | else { 513 | switch ($For) { 514 | #Case out specific mods for any one base type. 515 | 'EndpointGroup' { 516 | $sbAddIncludeFilter = { param( [string]$field_name, [string]$field_value) $this.include_filter += New-Object psobject -Property @{field_name = $field_name; field_value = $field_value; mode = 'include' } } 517 | $sbDeleteIncludeFilter = { param([string]$field_name) $this.include_filter = @($this.include_filter | Where-Object { !($_.field_name -eq $field_name) }) } 518 | $sbSetIncludeLogic = { param([string]$value) $this.include_filter_logic = $value } 519 | $sbClearIncludeFilter = { $this.include_filter = @() } 520 | $sbAddExcludeFilter = { param([string]$field_name, [string]$field_value) $this.exclude_filter += New-Object psobject -Property @{field_name = $field_name; field_value = $field_value; mode = 'include' } } 521 | $sbDeleteExcludeFilter = { param([string]$field_name) $this.exclude_filter = @($this.exclude_filter | Where-Object { !($_.field_name -eq $field_name) }) } 522 | $sbSetExcludeLogic = { param([string]$value) $this.include_filter_logic = $value } 523 | $sbClearExcludeFilter = { $this.exclude_filter = @() } 524 | 525 | $ret = New-Object psobject -Property @{name = 'Default Group Name'; description = 'Default Description'; include_filter_logic = ''; include_filter = @() ; exclude_filter = @() } 526 | 527 | $ret | Add-Member -MemberType ScriptMethod -Name "AddIncludeFilter" -Value $sbAddIncludeFilter 528 | $ret | Add-Member -MemberType ScriptMethod -Name "DeleteIncludeFilter" -Value $sbDeleteIncludeFilter 529 | $ret | Add-Member -MemberType ScriptMethod -Name "ClearIncludeFilter" -Value $sbClearIncludeFilter 530 | $ret | Add-Member -MemberType ScriptMethod -Name "SetIncludeLogic" -Value $sbSetIncludeLogic 531 | $ret | Add-Member -MemberType ScriptMethod -Name "AddExcludeFilter" -Value $sbAddExcludeFilter 532 | $ret | Add-Member -MemberType ScriptMethod -Name "DeleteExcludeFilter" -Value $sbDeleteExcludeFilter 533 | $ret | Add-Member -MemberType ScriptMethod -Name "ClearExcludeFilter" -Value $sbClearExcludeFilter 534 | $ret | Add-Member -MemberType ScriptMethod -Name "SetExcludeLogic" -Value $sbSetExcludeLogic 535 | return $ret 536 | } 537 | { $_ -in @('Remediation', 'DeferredRemediation') } { 538 | 539 | $deploy = ConvertFrom-Json $RemediationTemplate 540 | $deploy.name = "E$tempxternal $For template $((Get-Date).ToString('yyyyMMddhhmmss'))" 541 | $deploy.actions[0].params.display_summary = "$For via external API call." 542 | $sbRefreshCVEList = { 543 | $Script:Action1_CVE_Lookup = @{} 544 | Debug-Host "Refreshing CVE list at $(Get-Date)" 545 | Get-Action1 Vulnerabilities | ForEach-Object{$Script:Action1_CVE_Lookup[$_.cve_id]=$_} 546 | } 547 | $sbAddCVE = { 548 | param([string]$CVE_ID) 549 | $vul = (($Script:Action1_CVE_Lookup[$CVE_ID]).software).available_updates 550 | if ($null -eq $vul) { 551 | Write-Host "No patch for $CVE_ID found in Action1." -ForegroundColor Red 552 | } 553 | else { 554 | foreach ($item in $vul) { 555 | $upd = $item.package_id 556 | $ver = $item.version 557 | $name = $item.name 558 | if (!($null -eq $this.actions.params.packages[0].$upd)) { 559 | Debug-Host "$upd has already been added to this template.`nThis happens when an update addresses more than one CVE in a single package." 560 | } 561 | else { 562 | Debug-Host "Adding $upd to the package list for $CVE_ID." 563 | if ($null -eq $this.actions.params.packages[0].'default') { 564 | $this.actions.params.packages += New-Object PSCustomObject -Property @{$upd = $ver } 565 | } 566 | else { 567 | $this.actions.params.packages[0] = New-Object PSCustomObject -Property @{$upd = $ver } 568 | } 569 | } 570 | } 571 | } 572 | } 573 | $sbAddEndpointGroup = { param([string]$Id) if ($this.endpoints[0].id -eq 'All') { $this.endpoints[0] = New-Object psobject -Property @{id = $Id; type = 'EndpointGroup' } }else { $this.endpoints += New-Object psobject -Property @{id = $Id; type = 'EndpointGroup' } } } 574 | 575 | $deploy | Add-Member -MemberType ScriptMethod -Name "AddCVE" -Value $sbAddCVE 576 | $deploy | Add-Member -MemberType ScriptMethod -Name "AddEndpointGroup" -Value $sbAddEndpointGroup 577 | $deploy | Add-Member -MemberType ScriptMethod -Name "RefreshCVEList" -Value $sbRefreshCVEList 578 | if ($_ -eq 'Deferredremediation') { $deploy | Add-Member -MemberType NoteProperty -Name "settings" -Value 'DISABLED' } 579 | #$deploy.settings = "ENABLED ONCE AT:$((Get-Date).ToUniversalTime().AddMinutes(10).ToString("HH-mm-ss")) DATE:$((Get-Date).ToUniversalTime().ToString("yyyy-MM-dd"))"} 580 | $deploy.RefreshCVEList() 581 | return $deploy 582 | } 583 | 'DeploySoftware' { 584 | $deploy = ConvertFrom-Json $PackageDeployTemplate 585 | $deploy.name = "External $For template $((Get-Date).ToString('yyyyMMddhhmmss'))" 586 | $deploy.actions[0].params.display_summary = "$For via external API call." 587 | 588 | $sbAddEndpoint = { param([string]$Id) if ('All' -eq $this.endpoints[0].id) { $this.ClearEndpoints() }; $this.endpoints += New-Object psobject -Property @{id = $Id; type = 'Endpoint' } } 589 | $sbAddEndpointGroup = { param([string]$Id) if ('All' -eq $this.endpoints[0].id) { $this.ClearEndpoints() }; $this.endpoints += New-Object psobject -Property @{id = $Id; type = 'EndpointGroup' } } 590 | $sbDeleteEndpoint = { param([string]$Id) $this.endpoints = @($this.endpoints | Where-Object { !($_.type -eq 'Endpoint' -and $_.id -eq $Id) }) } 591 | $sbDeleteEndpointGroup = { param([string]$Id) $this.endpoints = @($this.endpoints | Where-Object { !($_.type -eq 'EndpointGroup' -and $_.id -eq $Id) }) } 592 | $sbClearEndpoints = { $this.endpoints = @() } 593 | $sbAddPackage = { 594 | param([string]$Package_ID) 595 | $pack = Get-Action1 Packages | Where-Object { $_.id -eq $Package_ID } 596 | $name = $pack.name 597 | if ($null -eq $pack) { 598 | Write-Host "Unable to locate package $Package_ID." -ForegroundColor Red 599 | } 600 | else { 601 | if (!($null -eq $this.actions.params.packages[0].$pack)) { 602 | Debug-Host "$name has already been added to this template." 603 | } 604 | else { 605 | $version = $(Get-Action1 RawURI -URI "$Script:Action1_BaseURI/packages/all/$Package_ID/versions").version 606 | Debug-Host "Adding $name version $Version to the package list." 607 | if ($null -eq $this.actions.params.packages[0].'default') { 608 | $this.actions.params.packages += New-Object PSCustomObject -Property @{$Package_ID = $version } 609 | } 610 | else { 611 | $this.actions.params.packages[0] = New-Object PSCustomObject -Property @{$Package_ID = $version } 612 | } 613 | } 614 | } 615 | } 616 | $deploy | Add-Member -MemberType ScriptMethod -Name "AddEndpoint" -Value $sbAddEndpoint 617 | $deploy | Add-Member -MemberType ScriptMethod -Name "AddEndpointGroup" -Value $sbAddEndpointGroup 618 | $deploy | Add-Member -MemberType ScriptMethod -Name "DeleteEndpoint" -Value $sbDeleteEndpoint 619 | $deploy | Add-Member -MemberType ScriptMethod -Name "DeleteEndpointGroup" -Value $sbDeleteEndpointGroup 620 | $deploy | Add-Member -MemberType ScriptMethod -Name "ClearEndpoints" -Value $sbClearEndpoints 621 | $deploy | Add-Member -MemberType ScriptMethod -Name "AddPackage" -Value $sbAddPackage 622 | return $deploy 623 | } 624 | default { Write-Error "Invalild request for template type $For." ; return $null } 625 | } 626 | } 627 | } 628 | } 629 | # Note things that do not get procesed post API call, and should be delivered unaltered. 630 | $Rawlist = @('ReportExport', 'Logs') 631 | 632 | if (CheckToken) { 633 | $AddArgs = "" 634 | $sbPolicyResultsDetail = { 635 | $Page = DoGet -Path $this.details -Label "PolicyResultsDetails" 636 | $Page.items | Write-Output 637 | While (![string]::IsNullOrEmpty($Page.next_page)) { 638 | $Page = DoGet -Path $Page.next_page -Label "PolicyResultsDetails" 639 | $Page.items | Write-Output 640 | } 641 | } 642 | $sbCustomFieldGet = { param([string]$name)($this.custom | Where-Object { $_.name -eq $name }).value } 643 | 644 | if ($Limit -gt 0) { $AddArgs = BuildArgs -In $AddArgs -Add "limit=$Limit" } 645 | if ($From -gt 0) { $AddArgs = BuildArgs -In $AddArgs -Add "from=$From" } 646 | #Add more URI arguments here?.. 647 | if (!$URILookUp["G_$Query"].ToString().Contains("`$Org_ID")) { 648 | if (!$URILookUp["G_$Query"].ToString().Contains("`$Object_ID")) { 649 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["G_$Query"]) 650 | } 651 | else { 652 | if ($Id) { 653 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["G_$Query"] -Object_ID $Id) 654 | } 655 | else { 656 | Write-Error 'This options requires that you specify an Object_ID.' 657 | } 658 | } 659 | } 660 | else { 661 | if ($Id) { 662 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["G_$Query"] -Org_ID $(CheckOrg) -Object_ID $Id) 663 | } 664 | else { 665 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["G_$Query"] -Org_ID $(CheckOrg)) 666 | } 667 | } 668 | if ($Rawlist.Contains($Query)) { $Page = DoGet -Path $Path -Label $Query -AddArgs $AddArgs -Raw } else { $Page = DoGet -Path $Path -Label $Query -AddArgs $AddArgs } 669 | if ($Page.items) { 670 | switch -Wildcard ($Query) { 671 | 'PolicyResults' { 672 | $page.Items | ForEach-Object { 673 | $_ | Add-Member -MemberType ScriptMethod -Name "GetDetails" -Value $sbPolicyResultsDetail 674 | Write-Output $_ 675 | } 676 | } 677 | 'Endpoint*' { 678 | $page.Items | ForEach-Object { 679 | $_ | Add-Member -MemberType ScriptMethod -Name "GetCustomAttribute" -Value $sbCustomFieldGet 680 | Write-Output $_ 681 | } 682 | } 683 | default { $Page.Items | Write-Output } 684 | } 685 | While (![string]::IsNullOrEmpty($Page.next_page)) { 686 | Debug-Host "[$Query] Next page..." 687 | if ($Rawlist.Contains($Query)) { $Page = DoGet -Path $Page.next_page -Label $Query -Raw } else { $Page = DoGet -Path $Page.next_page -Label $Query } 688 | switch -Wildcard ($Query) { 689 | 'PolicyResults' { 690 | $page.Items | ForEach-Object { 691 | $_ | Add-Member -MemberType ScriptMethod -Name "GetDetails" -Value sbPolicyResultsDetail 692 | Write-Output $_ 693 | } 694 | } 695 | 'Endpoint*' { 696 | $page.Items | ForEach-Object { 697 | $_ | Add-Member -MemberType ScriptMethod -Name "GetCustomAttribute" -Value $sbCustomFieldGet 698 | Write-Output $_ 699 | } 700 | } 701 | default { $Page.Items | Write-Output } 702 | } 703 | } 704 | } 705 | else { 706 | switch -Wildcard ($Query) { 707 | 'Endpoint*' { 708 | $Page | Add-Member -MemberType ScriptMethod -Name "GetCustomAttribute" -Value $sbCustomFieldGet 709 | Write-Output $Page 710 | 711 | } 712 | default { Write-Output $Page } 713 | } 714 | } 715 | } 716 | } 717 | 718 | function New-Action1 { 719 | param( 720 | [Parameter(Mandatory)] 721 | [ValidateSet( 722 | 'EndpointGroup', 723 | 'Organization', 724 | 'Automation', 725 | 'Remediation', 726 | 'DeferredRemediation', 727 | 'DeploySoftware', 728 | 'RawURI' 729 | )] 730 | [string]$Item, 731 | [string]$URI, 732 | [Parameter(Mandatory)] 733 | [object]$Data 734 | ) 735 | Debug-Host "Creating new $Item." 736 | #Short out processing path if URI literal is specified. 737 | if ($Item -eq 'RawURI') { if (!$URI) { Write-Error "Error -URI value required when Action is type RawURI.`n"; return $null }else { if (CheckToken) { return PushData -Path $URI -Method POST -Body $Data -Label 'RawRequest'} } } 738 | if (CheckToken) { 739 | try { 740 | if (!$URILookUp["N_$Item"].ToString().Contains("`$Org_ID")) { 741 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["N_$Item"]) 742 | } 743 | else { 744 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["N_$Item"] -Org_ID $(CheckOrg)) 745 | } 746 | return PushData -Method POST -Path $Path -Label $Item -Body $Data 747 | } 748 | catch { 749 | Write-Error "Error adding $Item`: $($_)." 750 | return $null 751 | } 752 | } 753 | } 754 | 755 | function Update-Action1 { 756 | param( 757 | [Parameter(Mandatory)] 758 | [ValidateSet( 759 | 'Modify', 760 | 'ModifyMembers', 761 | 'Delete' 762 | )] 763 | [String]$Action, 764 | [Parameter(Mandatory)] 765 | [ValidateSet( 766 | 'EndpointGroup', 767 | 'Endpoint', 768 | 'Automation', 769 | 'CustomAttribute', 770 | 'RawURI' 771 | )] 772 | [string]$Type, 773 | [object]$Data, 774 | [string]$Id, 775 | [string]$AttributeName, 776 | [string]$AttributeValue, 777 | [string]$URI, 778 | [switch]$Force 779 | ) 780 | Debug-Host "Trying update for $Action => $Type." 781 | #Short out processing path if URI literal is specified. 782 | if ($Type -eq 'RawURI') { if (!$URI) { Write-Error "Error -URI value required when Action is type RawURI.`n"; return $null }else { if (CheckToken) { return PushData -Path $URI -Method PATCH -Body $Data -Label 'RawRequest'} } } 783 | if (CheckToken) { 784 | switch ($Action) { 785 | 'ModifyMembers' { 786 | switch ($Type) { 787 | 'EndpointGroup' { 788 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["U_GroupMembers"] -Org_ID $(CheckOrg) -Object_ID $id) 789 | return PushData -Method POST -Path $Path -Body $Data -Label "$Action=>$Type" 790 | } 791 | default { Write-Error "Invalid request of $Type for query $Action." ; return $null } 792 | } 793 | } 794 | 'Modify' { 795 | if (!$Id) { Write-Error "When perfoming $Action=>$Type, the value for -Id must be specified to know what object to act on."; return $null } 796 | switch ($Type) { 797 | 'Automation' { 798 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["U_Automation"] -Org_ID $(CheckOrg) -Object_Id $Id) 799 | return PushData -Method PATCH -Path $Path -Body $Data -Label "$Action=>$Type" 800 | } 801 | 'CustomAttribute' { 802 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["U_Endpoint"] -Org_ID $(CheckOrg) -Object_Id $Id) 803 | $Data = New-Object psobject -Property @{"custom:$AttributeName" = $AttributeValue } 804 | return PushData -Method PATCH -Path $Path -Body $Data -Label "$Action=>$Type" 805 | } 806 | 'Endpoint' { 807 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["U_Endpoint"] -Org_ID $(CheckOrg) -Object_Id $Id) 808 | $Data.PSObject.Members | ForEach-Object { if (@('name', 'comment') -notcontains $_.Name) { $Data.PSObject.Members.Remove($_.Name) } } 809 | return PushData -Method PATCH -Path $Path -Body $Data -Label "$Action=>$Type" 810 | } 811 | 'EndpointGroup' { 812 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["U_GroupModify"] -Org_ID $(CheckOrg) -Object_Id $Id) 813 | return PushData -Method PATCH -Path $Path -Body $Data -Label "$Action=>$Type" 814 | } 815 | default { Write-Error "Invalid request of $Type for query $Action." ; return $null } 816 | } 817 | } 818 | 'Delete' { 819 | Debug-Host "Force delete enabled:$Force." 820 | switch ($Type) { 821 | 'EndpointGroup' { 822 | if ($force -or ((Read-Host "Are you sure you want to $Action $Type [$id]?`n[Y]es to confirm, any other key to cancel.") -eq 'Y')) { 823 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["U_GroupModify"] -Org_ID $(CheckOrg) -Object_Id $Id) 824 | return PushData -Method DELETE -Path $Path -Label "$Action=>$Type" 825 | } 826 | } 827 | 'Endpoint' { 828 | if ($force -or ((Read-Host "Are you sure you want to $Action $Type [$id]?`n[Y]es to confirm, any other key to cancel.") -eq 'Y')) { 829 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["U_Endpoint"] -Org_ID $(CheckOrg) -Object_Id $Id) 830 | return PushData -Method DELETE -Path $Path -Label "$Action=>$Type" 831 | } 832 | } 833 | 'Automation' { 834 | if ($force -or ((Read-Host "Are you sure you want to $Action $Type [$id]?`n[Y]es to confirm, any other key to cancel.") -eq 'Y')) { 835 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["U_Automation"] -Org_ID $(CheckOrg) -Object_Id $Id) 836 | return PushData -Method DELETE -Path $Path -Label "$Action=>$Type" 837 | } 838 | } 839 | default { Write-Error "Invalid request of $Type for query $Action." ; return $null } 840 | } 841 | } 842 | default { Write-Error "Invalid request of $Type for query $Action." ; return $null } 843 | } 844 | } 845 | } 846 | 847 | function Start-Action1Requery { 848 | param( 849 | [Parameter(Mandatory)] 850 | [ValidateSet( 851 | 'ReportData', 852 | 'InstalledSoftware', 853 | 'InstalledUpdates' 854 | )] 855 | [string]$Type, 856 | [string]$Endpoint_Id 857 | ) 858 | if (CheckToken) { 859 | if (!$URILookUp["R_$Type"].ToString().Contains("`$Org_ID")) { 860 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["R_$Type"]) 861 | } 862 | else { 863 | if ($Endpoint_Id) { 864 | if ($URILookUp["R_$Type"].ToString().Contains("`$Object_ID")) { 865 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["R_$Type"] -Org_ID $(CheckOrg) -Object_ID $Endpoint_Id) 866 | } 867 | else { 868 | Write-Error "Endpoint_Id was specified but this action is not endpoint specific, can continue, defaulting to system wide." 869 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["R_$Type"] -Org_ID $(CheckOrg)) 870 | } 871 | } 872 | else { 873 | $Path = "$Script:Action1_BaseURI{0}" -f (& $URILookUp["R_$Type"] -Org_ID $(CheckOrg)) 874 | } 875 | } 876 | return PushData -Method POST -Path $Path.TrimEnd('/') -Label "Requery=>$Type" 877 | } 878 | } 879 | 880 | --------------------------------------------------------------------------------