Error Querying API Information Using the Rapid7Nexpose PowerShell Module

I’ve managed to connect to our InsightVM instance, however, I am unable to query anything due to the following error. I am using the PowerShell module that is posted here: https://github.com/My-Random-Thoughts/Rapid7Nexpose. Any ideas as to why I’m getting the error below? My PowerShell version is 5.1.18362.1171 and I’m on Win10 1909 Enterprise. Thank you!

Error formatting a string: Index (zero based) must be greater than or equal to zero and less than the size of the argument list…
You cannot call a method on a null-valued expression.
At line:183 char:13

  •         Throw "`n$errMsg1`n$errMsg2`n$errMsg3"
    
  •         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    • CategoryInfo : OperationStopped: (
      Error formatti…ued expression.:String) [], RuntimeException
    • FullyQualifiedErrorId :
      Error formatting a string: Index (zero based) must be greater than or equal to zero and less than the size of the argument list.
      .
      You cannot call a method on a null-valued expression.

Not sure what method you are calling, but my knowledge of just directly hitting the InsightVM API from PowerShell there are arrays of filters used. This error tells me that the author is trying to split a string into one of these arrays. Can you provide your PowerShell sting that calls this (desensitized if necessary) knowing your arguments and what cmdlet is being called will aid in troubleshooting

Here is an example of a Asset Search:

$Search = [pscustomobject]@{
match = 'all'
  filters = @(
     [pscustomobject]@{
        field = 'risk-score'
        operator = 'is-greater-than'
        value = 5000
     },
     [pscustomobject]@{
        field = 'operating-system'
        operator = 'contains'
        value = 'windows'
     },
     [pscustomobject]@{
        field = 'service-name'
        operator = 'contains'
        value = 'ssh'
     }
  )
}
$Results = Invoke-RestMethod -Uri "$Rapid7BaseUrl/assets/search?size=500" -Body $($Search | ConvertTo-Json) -Method Post -ContentType 'Application/JSON' -Headers $Rapid7Headers
3 Likes

Sure! I’m using the Get-NexposeAsset function. Also, according to the directions on Github page, one example was calling Get-NexposeSite. I ran this in Verbose mode. That is supposed to return all sites, however, it returns the same error:

       **VERBOSE: Executing API method "GET" against "/api/3/sites?size=100"**
    Error formatting a string: Index (zero based) must be greater than or equal to zero and less than the size of the argument list..
    You cannot call a method on a null-valued expression.
    At line:183 char:13
    +             Throw "`n$errMsg1`n$errMsg2`n$errMsg3"
    +             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : OperationStopped: (
    Error formatti...ued expression.:String) [], RuntimeException
        + FullyQualifiedErrorId : 
        Error formatting a string: Index (zero based) must be greater than or equal to zero and less than the size of the argument list. 
       .
    You cannot call a method on a null-valued expression.
1 Like

The author has a bug in the error handling section of the code.
I’d post a message on the Github page addressing this.
This error is masking what you actual error is.
Starting on line 172 of master/private/Invoke-NexposeQuery.ps1

Catch {
    $errMsg1 = $_.Exception.Message
    Try {
        $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
        $reader.BaseStream.Position = 0
        $reader.DiscardBufferedData()
        $errMsg2 = $reader.ReadToEnd()
    }
    Catch {
        $errMsg3 = $_.Exception.Message
    }
    Throw "`n$errMsg1`n$errMsg2`n$errMsg3"
}

Also, a simple version of this to check if your credentials, server, and port are correct:

$Rapid7_User = ''
$Rapid7_Password = ''
$Rapid7_Server = ''
$Rapid7_Port = '3780'
$Rapid7_BaseUrl = "https://$($Rapid7_Server):$($Rapid7_Port)/api/3"
$Rapid7_Headers = @{Authorization = "Basic $([System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("$($Rapid7_User):$($Rapid7_Password)")))"}

(Invoke-RestMethod -Uri "$Rapid7_BaseUrl/sites?size=100" -Headers $Rapid7_Headers).resources
2 Likes

Thank you, @brandon_mcclure. I’ll let the author know. I’m really just trying to look into automating a way to import {Last Logged On Users} as an {Owner} tag in InsightVM if there is a way to do that. Possibly even {Location} based on IP scope-lookup in DHCP. That one might be trickier though.

According to that cred checker script, my creds work just fine. I have to omit the port in my code because everything goes through our load balancer. The LB proxies everything and masks the port, so if you attempt to connect using the DNS name and port, it throws an error. I fixed this is some of the code within the Library, and got it to work. Below is an example of what returns after using the cred check you provided (making sure I desensitized the output and using Verbose):

VERBOSE: GET https://DNSNAME/api/3/sites?size=100 with 0-byte payload
VERBOSE: received -1-byte response of content type application/json;charset=UTF-8


assets          : 0
description     : Monthly Discovery Scan for the BYOD networks
id              : 84
importance      : normal
links           : {@{href=https://DNSNAME/api/3/sites/84; rel=self}, @{href=https://DNSNAME/api/3/sites/84/alerts; rel=Alerts}, 
                  @{href=https://DNSNAME/api/3/sites/84/scan_engine; rel=Scan Engine}, @{href=https://DNSNAME/api/3/sites/84/scan_schedules; rel=Schedules}...}
name            : BYOD Wireless Discovery Scan
riskScore       : 0.0
scanEngine      : 12
scanTemplate    : discovery
type            : static
vulnerabilities : @{critical=0; moderate=0; severe=0; total=0}

assets          : 10913
id              : 66
importance      : normal
lastScanTime    : 2021-02-19T06:44:51.949Z
links           : {@{href=https://DNSNAME/api/3/sites/66; rel=self}, @{href=https://DNSNAME/api/3/sites/66/alerts; rel=Alerts}, 
                  @{href=https://DNSNAME/api/3/sites/66/scan_engine; rel=Scan Engine}, @{href=https://DNSNAME/api/3/sites/66/scan_schedules; rel=Schedules}...}
name            : Discovery Site
riskScore       : 182934384
scanEngine      : 12
scanTemplate    : discovery
type            : static
vulnerabilities : @{critical=125644; moderate=31318; severe=435232; total=592194}

assets          : 4
description     : This will scan R7-Scan1-R7-Scan4 for any vulns using the local scanner built into InsightVM.
id              : 75
importance      : normal
lastScanTime    : 2021-02-17T21:09:23.333Z
links           : {@{href=https://DNSNAME/api/3/sites/75; rel=self}, @{href=https://DNSNAME/api/3/sites/75/alerts; rel=Alerts}, 
                  @{href=https://DNSNAME/api/3/sites/75/scan_engine; rel=Scan Engine}, @{href=https://DNSNAME/api/3/sites/75/scan_schedules; rel=Schedules}...}
name            : Rapid 7 Scanners Site
riskScore       : 4710.0
scanEngine      : 3
scanTemplate    : full-audit-without-web-spider
type            : static
vulnerabilities : @{critical=1; moderate=4; severe=3; total=8}

assets          : 3170
id              : 3
importance      : normal
lastScanTime    : 2021-02-19T21:44:56.653Z
links           : {@{href=https://DNSNAME/api/3/sites/3; rel=self}, @{href=https://DNSNAME/api/3/sites/3/alerts; rel=Alerts}, 
                  @{href=https://DNSNAME/api/3/sites/3/scan_engine; rel=Scan Engine}, @{href=https://DNSNAME/api/3/sites/3/scan_schedules; rel=Schedules}...}
name            : Rapid7 Insight Agents
riskScore       : 282816000
scanEngine      : 3
scanTemplate    : full-audit-without-web-spider
type            : agent
vulnerabilities : @{critical=196503; moderate=53786; severe=693745; total=944034}

assets          : 1
id              : 83
importance      : normal
lastScanTime    : 2020-12-02T22:20:37.559Z
links           : {@{href=https://DNSNAME/api/3/sites/83; rel=self}, @{href=https://DNSNAME/api/3/sites/83/alerts; rel=Alerts}, 
                  @{href=https://DNSNAME/api/3/sites/83/scan_engine; rel=Scan Engine}, @{href=https://DNSNAME/api/3/sites/83/scan_schedules; rel=Schedules}...}
name            : Synology Devices
riskScore       : 28601.0
scanEngine      : 12
scanTemplate    : hipaa-audit
type            : static
vulnerabilities : @{critical=16; moderate=9; severe=25; total=50}

assets          : 0
description     : These are IT owned UPS devices.
id              : 79
importance      : normal
links           : {@{href=https://DNSNAME/api/3/sites/79; rel=self}, @{href=https://DNSNAME/api/3/sites/79/alerts; rel=Alerts}, 
                  @{href=https://DNSNAME/api/3/sites/79/scan_engine; rel=Scan Engine}, @{href=https://DNSNAME/api/3/sites/79/scan_schedules; rel=Schedules}...}
name            : UPS
riskScore       : 0.0
scanEngine      : 12
scanTemplate    : discovery
type            : static
vulnerabilities : @{critical=0; moderate=0; severe=0; total=0}
1 Like

This will apply the Owner Tag of “Me” to the first 500 Windows OS Assets you have:

$Rapid7_Owner = 'Me'
$Rapid7_Body = [pscustomobject]@{
    name = $Rapid7_Owner
    type = 'owner'
}
$Rapid7_NewTag = Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags" -Body ($Rapid7_Body | ConvertTo-Json) -Headers $Rapid7_Headers -Method Post -ContentType 'application/json'

$Rapid7_AssetSearch = [pscustomobject]@{
  match = 'all'
  filters = @(
     [pscustomobject]@{
        field = 'operating-system'
        operator = 'contains'
        value = 'windows'
     }
  )
}
$Rapid7_AssetResults = (Invoke-RestMethod -Uri "$Rapid7_BaseUrl/assets/search?size=500" -Body $($Rapid7_AssetSearch | ConvertTo-Json) -Method Post -ContentType 'Application/JSON' -Headers $Rapid7_Headers).resources
$Rapid7_AssetResults | ForEach-Object {
    Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags/$($Rapid7_NewTag.id)/assets/$($_.id)" -Headers $Rapid7_Headers -Method Put
}

hypothetically, I didn’t actually run it but I did pull it out of other code where I did dynamically populate the owner tag from a REST call pulling the data from another system

2 Likes

@brandon_mcclure , thank you very much. I’m a little late replying back to your post. I have (somewhat) successfully created this integration using PowerShell, however, I am having issues with exception handling during the tag creation. More specifically after a tag has already been created based on the username (owner) tag, I am unsure how to append a prior tag to an asset within the script. I am using a foreach-object loop and I query AD based on hostname, and I only pull in the username and append that to a variable within the loop. After implementing this, at some point I receive a (400) bad request and it screws up the import process. Sometimes appending the original user to future assets until a new asset returns a new user. It will then create the new user tag, and will continue to work properly until the same user is also tied to another machine - then it will continue to append that user (owner) to future assets until a different (new) user is queried. Using Try/Catch, is there a way I can have the script use a GET request to obtain a tag (created prior) if a PUT/POST does not work within the script?

(Invoke-RestMethod -Uri "$Rapid7_BaseUrl/assets/$($_.id)/tags" -Headers $Rapid7_Headers).resources

@brandon_mcclure. Thanks, however, I still get a 400 error from that as well. Here is what I have created so far. I’m not great at this part of scripting (still learning exception handling in PowerShell).

$Rapid7_Owner = $User_name
$Rapid7_Body = [pscustomobject]@{
    name = $Rapid7_Owner
    type = 'owner'
}

try {
#Create Tag
$Rapid7_Tag = Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags" -Body ($Rapid7_Body | ConvertTo-Json) -Headers $Rapid7_Headers -Method Post -ContentType 'application/json'
Write-Host "In Try"

}

catch {
#Query Tag (If already created)
$Rapid7_Tag = $null
$Rapid7_Tag = (Invoke-RestMethod -Uri "$Rapid7_BaseUrl/assets/$($_.id)/tags" -Headers $Rapid7_Headers).resources
Write-Host "In Catch"

}

finally {

#Append Tag to Asset
Write-Host "Trying Put"
Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags/$($Rapid7_Tag.id)/assets/$($_.id)" -Headers $Rapid7_Headers -Method Put


}

in your line
$Rapid7_Tag = Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags" -Body ($Rapid7_Body | ConvertTo-Json) -Headers $Rapid7_Headers -Method Post -ContentType 'application/json'

you need to append -ErrorAction Stop to make it a terminating error that can be caught in the catch
$Rapid7_Tag = Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags" -Body ($Rapid7_Body | ConvertTo-Json) -Headers $Rapid7_Headers -Method Post -ContentType 'application/json' -ErrorAction Stop

Then inside the catch $_ gets remapped to the error message, so if you are going to use $_ in a try-catch block it is always best to remap it to a named variable. You also are missing the logic to get your AssetID, that is what the search did. But if you knew it you could just define it.

try this after changing the $Rapid7_AssetID to one you know

$Rapid7_Owner = $User_name
$Rapid7_AssetID = 1
$Rapid7_Body = [pscustomobject]@{
    name = $Rapid7_Owner
    type = 'owner'
}

try {
    #Create Tag
    $Rapid7_Tag = Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags" -Body ($Rapid7_Body | ConvertTo-Json) -Headers $Rapid7_Headers -Method Post -ContentType 'application/json' -ErrorAction Stop
    Write-Host "In Try"
} catch {
    #Query Tag (If already created)
    $Rapid7_Tag = $null
    $Rapid7_Tag = (Invoke-RestMethod -Uri "$Rapid7_BaseUrl/assets/$Rapid7_AssetID/tags" -Headers $Rapid7_Headers).resources
    Write-Host "In Catch"
} finally {
    #Append Tag to Asset
    Write-Host "Trying Put"
    Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags/$($Rapid7_Tag.id)/assets/$Rapid7_AssetID" -Headers $Rapid7_Headers -Method Put
}

That helps a great deal! Thanks! I ran into an issue with querying custom tags. I’m unable to match $User_Name based on $tag.name. When I run the full query to get all of the tags, they only show ones we’ve created in InsightVM, not the custom ones imported based on username. Is there a way to pull these specifically? Below is the GET I ran to pull all tags, as well as the try/catch used.

$QueryTags = (Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags" -Headers $Rapid7_Headers).resources

try {
#Create Tag
$Rapid7_Tag = Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags" -Body ($Rapid7_Body | ConvertTo-Json) -Headers $Rapid7_Headers -Method Post -ContentType 'application/json' -ErrorAction Stop
Write-Host "In Try"
Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags/$($Rapid7_Tag.id)/assets/$($Rapid7_ID)" -Headers $Rapid7_Headers -Method Put -ErrorAction Stop

}

catch {
#Query Tag (If already created)

    foreach ($tag in $QueryTags) {


        if (($tag.name) -like $User_Name)

        {

        Write-Host "Match!"
        $Rapid7_TagID = $tag.id
        Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags/$($Rapid7_TagID)/assets/$($Rapid7_ID)" -Headers $Rapid7_Headers -Method Put

        } 

    }


Write-Host "In Catch"

}

finally {




}

check your paging

$Rapid7_Tags.page
number size totalResources totalPages
------ ---- -------------- ----------
     0   10            386         39

You can pull by page

$Rapid7_Tags = Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags?page=1" -Headers $Rapid7_Headers

$Rapid7_Tags.page

number size totalResources totalPages
------ ---- -------------- ----------
     1   10            386         39

Or Pull all in one page

$Rapid7_Tags = Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags?size=500" -Headers $Rapid7_Headers

$Rapid7_Tags.page

number size totalResources totalPages
------ ---- -------------- ----------
     0  500            386          1

That’s exactly what I needed. Thank you. /tags?size= is what I was missing. I’m running into one other problem. For some reason even if my sizing for asset search is well over the amount of assets we have in our environment (i.e. 100000), I still do not retrieve all of my assets. I’m searching using the following criteria:

$Rapid7_AssetSearch = [pscustomobject]@{
match = 'all'
  filters = @(
      [pscustomobject]@{
      field = 'operating-system'
      operator = 'contains'
      value = 'windows'
     },
      [pscustomobject]@{
     field = 'host-name'
     operator = 'is-not'
     value = ''
     }
 )
}

I assume this filters out blank host name entries? Is there a way to go a step further to filter out assets that have the Insight Agent? I tried incorporating site-id based off of what the API documentation showed was “searchable”. Is there a way to only include assets within the Rapid7 Agent Site? I’ll paste the attempted filter below. I realize my syntax is probably incorrect. The API documentation says it needs to be in a string array format. Any idea how to build that out?

   [pscustomobject]@{
          field = 'site-id'
          operator = 'in'
          value = 3
         }
[pscustomobject]@{
          field = 'site-id'
          operator = 'in'
          value = @('1','2','3')
         }

Wow, that was a simple fix! Any interest in posting the full script when I get it working as intended? (Masking sensitive data, of course.)

As another side project, I’m also looking into integrating another similar script with our SCCM database - pulling “Most Active Users” in as “owner(s).

Go for it, I’m sure others will find it useful.

DISCLAIMER: This is not an efficient script by any means, but it gets the job done. We created a separate InsightVM user just for API access. You can set this script up to run daily and tag new Windows machines without an owner. We have AD Audit+ in our environment, so everything is ported to the “Description” field within each AD computer. The first part of the description is “username”, so we only pull that info for each computer. If the computer does not have an AD user associated with it, it will be tagged as “No AD User”. In a future iteration of this script, we hope to have this integrated with SCCM so we can get (maybe) more accurate username tagging. Our company plays musical assets, so at times it is difficult to know who is the true owner of specific assets.

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls -bor
[Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12

#################################################
#Pass Creds through API
#################################################
$Rapid7_User = 'USERNAME'
$Rapid7_Password = 'PASSWORD OR READ-HOST'
$Rapid7_Server = 'INSIGHT ON-PREM SERVER'
$Rapid7_BaseUrl = "https://$($Rapid7_Server):PORTIFNECESSARY/api/3"
$Rapid7_Headers = @{Authorization = "Basic $([System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("$($Rapid7_User):$($Rapid7_Password)")))"}


#################################################
#Function to Remove ALL Owner Tags (commented out)
#################################################

<# Removing All Owner Tags
function DeleteOwnerTags {

$QueryTags = (Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags?size=4000" -Headers $Rapid7_Headers).resources

foreach ($tag in $QueryTags)

{
    if ($tag.type -contains "owner")

    {

    Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags/$($tag.id)" -Headers $Rapid7_Headers -Method Delete -Verbose -ErrorAction Stop

    }


}#end of foreach

}#end of function DeleteTags

DeleteOwnerTags
#>



#################################################
#Asset Search Filter for API 
#################################################

$Rapid7_AssetSearch = [pscustomobject]@{
match = 'all'
  filters = @(
      [pscustomobject]@{
      field = 'operating-system'
      operator = 'contains'
      value = 'windows'
     },
     [pscustomobject]@{
     field = 'owner-tag'
     operator = 'is-not-applied'
      }

 )
}
######################################################################
#Grab Hostname from R7 Database using API Query Based on Above Search
######################################################################

$GetPages = (Invoke-RestMethod -Uri "$Rapid7_BaseUrl/assets/search?page=1" -Body $($Rapid7_AssetSearch | ConvertTo-Json) -Method Post -ContentType 'Application/JSON' -Headers $Rapid7_Headers)
$TotalAssetPages = $GetPages.page | Select totalPages -ExpandProperty totalPages
#$AssetPage=$null


#####################################################################
# Loop through all of the objects within every page in the API
#####################################################################

for ($AssetPage=0; $AssetPage -le $TotalAssetPages; $AssetPage++)

{ #START OF FOR LOOP

$Rapid7_AssetResults = (Invoke-RestMethod -Uri "$Rapid7_BaseUrl/assets/search?page=$AssetPage" -Body $($Rapid7_AssetSearch | ConvertTo-Json) -Method Post -ContentType 'Application/JSON' -Headers $Rapid7_Headers).resources
Write-Host "Now Processing page $AssetPage of $TotalAssetPages... Continuing on"


    $Rapid7_AssetResults | ForEach-Object {

        try
            {

            if ($($_.hostname) -match "FQDN")
            {
            $Rapid7_Hostname = ($($_.hostname))
            $Rapid7_Hostname = $Rapid7_Hostname.Replace(".FQDN","")
            $Rapid7_AssetID = ($($_.id))
            $User_Name = $((Get-ADComputer -Identity $Rapid7_Hostname -Properties * | Select Description -ExpandProperty Description).Split('|')[0]) -replace '`' -replace ""
            $User_Name = $User_Name -replace "'" -replace ""
            $User_Name = $User_Name.ToLower()
            Write-Host "FQDN $Rapid7_Hostname Found and Trimmed!"
            $User_Name
            }

            if ($($_.hostname) -notmatch "FQDN")
            {
            $Rapid7_Hostname = ($($_.hostname))
            $Rapid7_AssetID = ($($_.id))
            $User_Name = $((Get-ADComputer -Identity $Rapid7_Hostname -Properties * | Select Description -ExpandProperty Description).Split('|')[0]) -replace '`' -replace ""
            $User_Name = $User_Name -replace "'" -replace ""
            $User_Name = $User_Name.ToLower()
            Write-Host "FQDN $Rapid7_Hostname Not Found"
            $User_Name
            }

            }

        catch
            {

            Write-Host "Unable to query AD using this hostname $Rapid7_Hostname"
            $User_Name = "No AD User"
            }

        finally

            {

            $QueryTags = (Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags?page=1" -Headers $Rapid7_Headers)
            $TotalTagPages = $QueryTags.page | Select totalPages -ExpandProperty totalPages

            ###################################################
            #API format to query/get/append tag info
            ###################################################

                $Rapid7_Body = [pscustomobject]@{
                name = ($($User_Name))
                type = 'owner'

                }
              
              

                try {#Create Tag
                    $Rapid7_Tag = Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags" -Body ($Rapid7_Body | ConvertTo-Json) -Headers $Rapid7_Headers -Method Post -ContentType 'application/json' -ErrorAction Stop
                    Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags/$($Rapid7_Tag.id)/assets/$($Rapid7_AssetID)" -Headers $Rapid7_Headers -Method Put -Verbose -ErrorAction Stop

                    }#End of Finally 1 - Try

                catch {#Query Tag (If already created)

                ##########################################################
                #For Loop used to iterate through all Tag Pages
                ##########################################################

                for ($TagPage=0; $TagPage -le $TotalTagPages; $TagPage++)
                {

                        $QueryTags = (Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags?page=$TagPage" -Headers $Rapid7_Headers).resources
                        Write-Host "Now searching through tag page $TagPage of $TotalTagPages for $User_Name"

                     foreach ($tag in $QueryTags) {


                            if ($User_Name -match ($($tag.name)))

                                {

                                Write-Host "Match!" -ForegroundColor Magenta
                                

                                try     {

                                        Invoke-RestMethod -Uri "$Rapid7_BaseUrl/tags/$($tag.id)/assets/$($Rapid7_AssetID)" -Headers $Rapid7_Headers -Method Put -Verbose -ErrorAction Stop
                                        Write-Host "Appending known user tag to asset $Rapid7_AssetID" -ForegroundColor Green
                                        
                                        
                                        }#End of Try (Nested within Foreach-If)

                                catch   {

                                        Write-Host "The tag $($tag.name) with ID $($tag.id) has already been created, and the asset $($Rapid7_Hostname) has already been tagged with this particular tag. Continuing on..."

                                        }#End of Catch (Nested within Foreach-If)

                                }#End of If (Nested within Foreach) 

                      }#End of Nested Foreach #2 - Inner Block

                 }#End of For #2 Inner Block - Tag page search iteration
                
              }#End of Finally1 - Catch

          

}#finally 1 outer block                           
                                                                       
}#Foreach-Object Loop #1 outer block

}#End of For Loop #1 Outer Block - Asset page search iteration
2 Likes

I got all excited for a bit then , but I guess this only works if the Description field is populated with the username using an external tool (AD Audit+) .

We spend far too long trying to track down who is using what desktop/laptop and really need to find a solution like this. Maybe when you suss the SCCM querying I’ll have another look. :wink:

Great work and perseverance on getting this working though!