Personal Insights in Tech

Category: PowerShell

All posts relating to PowerShell scripts developed on.

Using PnP.PowerShell to retrieve SharePoint Online List Data

The Problem

Working (at this time) with Power BI Report Server, my team and I occasionally encounter hurdles with getting data to automatically refresh from certain sources. In this case, we were unable to directly connect and refresh data from our organizations SharePoint Online site list. In this post I detail how I use the PnP.PowerShell module to retrieve SharePoint Online site list data.

The Solution

After a few developmental trials, I crafted the following PowerShell Script (sample) which utilizes the local logged-in User credentials and PnP.PowerShell module to easily retrieve data from SharePoint Online site lists. As long as the local user has access to the site list in question, and the module is installed on the machine, developers can simply paste this script into PowerShell ISE on their own machine, and configure to their own site list(s) and data column needs.

The script upon execution briefly pops up with a dialog passing local credentials to the SharePoint site in question, and then shows a progress bar as data is exported and transformed, closing when finished:

Progress bar of data export progress from SharePoint Online site list data.

This script can thus be run on command to refresh the data as often as needed, or saved to a specified location and set up in Task Scheduler to run automatically:

Automatically run script each business day at 8:30AM.
Settings in Task Scheduler to run the specified .ps1 PowerShell Script.

Future Plans

My team and I plan to pursue changing this operation to occur through development of an SQL Server Integration Services (SSIS) package, but at this time there are security concerns with utilizing Visual Studio 2019 / .NET 5 (out of support), and Visual Studio 2022 has not yet added development features for SSIS – an irksome problem for myself and many other developers since October 2021; more details on this here.

Conclusion

This post has covered in detail how I use the PnP.PowerShell module to retrieve SharePoint Online site list data. To see other PowerShell scripts I have developed, see my other PowerShell blog posts, or my PowerShell Operations GitHub repository. Thanks for reading!

SCRIPT:

# Script:        Get SharePoint Online List Data
# Developer:     James Scurlock

#
# Description:   Ran from Developer machine in PowerShell ISE, uses local credentials to access a SharePoint Online Site,
#                and retrieve data in a given List for export to .csv/.xlsx. Intention is for this data to be regularly pulled to refresh
#                a Power BI Report based upon said data.
#
# Last Modified: 7/6/2022
#--------------------------------------------------
# PRE-REQUISITE(S): 
#--------------------------------------------------
#    1. Have Access to the SharePoint Site List in question
#    2. Install-Module -Name PnP.PowerShell
#--------------------------------------------------
# DEVELOPER NOTE(S):
#--------------------------------------------------
#    PnP PowerShell Documentation: https://pnp.github.io/powershell/cmdlets/Connect-PnPOnline.html#interactive-login-for-multi-factor-authentication
#--------------------------------------------------
# CONFIGURATION PARAMETER(S):
#--------------------------------------------------
$siteURL = "https://{yourDomain}.sharepoint.com/sites/{nameOfSite}"
$listName = "{nameOfList}"
$exportFileLocation = "\\fileServer\ITGroup\DBTeam\SharePointExportedData\"
$CSVPath = $exportFileLocation + "Exported Data.csv"
#$ExcelPath = $exportFileLocation + "Exported Data.xlsx"

# EDIT on Line 62 as needed for fields to be renamed upon Export.
$propertiesToExport = @("ID","Title","field_1","field_2","field_3"); 
#--------------------------------------------------
# BEGIN SCRIPT
#--------------------------------------------------
try 
{
	# Check if Module is installed on current machine
    if (Get-Module -ListAvailable -Name PnP.PowerShell) {
        $dataForExport = @()
        cls
    
        # Connect-PnPOnline: https://pnp.github.io/powershell/cmdlets/Connect-PnPOnline.html
        # INFO - The following prompts for credentials, but Fails with either 403 Forbidden or non-matching name/password in Microsoft account system. Will not work if Multi-Factor Authentication (MFA) is enabled.
        #    Connect-PnPOnline -Url $SiteURL -Credentials (Get-Credential) # 
        # INFO - Non-Admin: The following returns "The sign-in name or password does not match one in the Microsoft account system - assuming also not working due to Multi-Factor Authentication.
        #        Admin: AADSTS53000: Device is not in required device state: compliant. Conditional Access policy requires a compliant device, and the device is not compliant. The user must enroll their device with an approved MDM provider like Intune.
        #    $creds = (New-Object System.Management.Automation.PSCredential "<<UserName>>",(ConvertTo-SecureString "<<Password>>" -AsPlainText -Force))
        #    Connect-PnPOnline -Url $siteURL -Credentials $creds
        # Ex. 9 - Creates a brief popup window that checks for login cookies. Can use -ForceAuthentication to reset and force a new login.
        #         NOTE: This is required for systems utilizing Multi-Factor Authentication.
        Connect-PnPOnline -Url $siteURL -UseWebLogin 

        $counter = 0
   
        #$ListItems = Get-PnPListItem -List $ListName -Fields $SelectedFields -PageSize 2000 # PageSize: The number of items to retrieve per page request
        #$ListItems = (Get-PnPListItem -List $ListName -Fields $SelectedFields).FieldValues
        $listItemIDTitleGUID = Get-PnPListItem -List $listName #  = Id, Title, GUID

        foreach ($item in $listItemIDTitleGUID)
        {
            $counter++
            Write-Progress -PercentComplete ($counter / $($listItemIDTitleGUID.Count)  * 100) -Activity "Exporting List Items..." -Status  "Exporting Item $counter of $($listItemIDTitleGUID.Count)"

            # Get all list items specificed properties by ID using PnP cmdlet - https://pnp.github.io/powershell/cmdlets/Get-PnPListItem.html
            $listItems = (Get-PnPListItem -List $listName -Id $item.Id -Fields $propertiesToExport).FieldValues
    
            # Add the Name / Value pairing of each property for export to the export data array. Adding and removing some properties by name to specify the actual field designation upon Export.
            $listItems | foreach {
                $itemForExport = New-Object PSObject
                foreach ($field in $propertiesToExport) {
                    if ($field -eq "field_1") {
                        $itemForExport | Add-Member -MemberType NoteProperty -name "field_1" -value $_[$field]
                        $itemForExport | Add-Member -MemberType NoteProperty -Name "ProjectSponsor" -Value $itemForExport.field_1 -Force
                        $itemForExport.PSObject.Properties.Remove("field_1")
                    } else {
                        if ($field -eq "field_2") {
                            $itemForExport | Add-Member -MemberType NoteProperty -name "field_2" -value $_[$field]
                            $itemForExport | Add-Member -MemberType NoteProperty -Name "InitiativeKey" -Value $itemForExport.field_2 -Force
                            $itemForExport.PSObject.Properties.Remove("field_2")
                        } else {
                            if ($field -eq "field_3") {
                                $itemForExport | Add-Member -MemberType NoteProperty -name "field_3" -value $_[$field]
                                $itemForExport | Add-Member -MemberType NoteProperty -Name "AreaOfFocus" -Value $itemForExport.field_3 -Force
                                $itemForExport.PSObject.Properties.Remove("field_3")
                            } else {
									$itemForExport | Add-Member -MemberType NoteProperty -name $field -value $_[$field]
								
							}
						}
					}
				}
		
                # Add the object with the above updated properties to the export Array
                $dataForExport += $itemForExport
            }
        }

        #Export the result Array to csv/xlsx files
        $dataForExport | Export-CSV $CSVPath -NoTypeInformation
			#$dataForExport | Export-Excel -Path $ExcelPath

        # Disconnect the current connection and clear token cache.
        Disconnect-PnPOnline
    } else {
        cls
        Write-Host "In order to execute this script, the Cmdlet Module {PnP.PowerShell} must be installed. Please open an Admin shell and run the following command: Install-Module -Name PnP.PowerShell"
    }
} catch {
    Write-Host $_.Exception.Message
    $Error.Clear()
}
#--------------------------------------------------
# END SCRIPT
#--------------------------------------------------

Using PowerShell to retrieve Azure DevOps Work Item Data

This PowerShell Script and any future enhancements can be found in my Public GitHub repository – Azure DevOps Work Item Data Extraction.ps1

Sample of Script to retrieve Azure DevOps Work Item data.

Working in a Microsoft centric ecosystem, my team at this time utilizes Azure DevOps. For a Reporting project the other day, I had a request where alongside creating a dashboard for data scraped from a 3rd party tool’s API, business users wanted to incorporate data from a particular Azure DevOps Board, to review how quickly certain related Work Item types were being resolved over time.

The below PowerShell script documents how this task was acheived, outputing the desired data as a .json file for modeling in Power BI, and is relatively simple overall – in my opinion.

We utilize Work Item Query Language (WIQL) to get the fields we care about through the DevOps REST API call.

Steps:

  1. Navigate to the Project of interest in your Azure Devops. Make note of your {organization}, {project}, and {team} in DevOps. These strings will need to be added to the below script as variables.
  2. In the top-right, select the User Settings icon and go to Personal access tokens. Assuming you have never set one up before, create a new token. For this example, I have given the access token a name, selected my {Organization}, specified access as being Full, and custom defined it to expire the maximum number of days I can specify (1 year). You can customize this to better fit your security and expiration preferences.

    Finding Azure DevOps Personal Access Tokens

    Creating new Personal Access Token

    Settings for Personal Access Token

    Make note of this token ID as once you leave the screen you cannot retrieve it again, and thereby must create an entirely new token.
  1. Paste the token value and other appropriate strings into variables. Paste the copied token value into the {$token} variable, and paste your noted {$organization}, {$project}, and {$team} strings into their appropriate variables. Specify where the exported data should be saved to in the {$exportFileLocation} variable. The example below utilizes my Downloads folder.
  2. Build your data query criteria. In the {$body} variable, build out the criteria that determines what work items you want data for. In my case I was looking for work items with the custom type of “Vulnerability” that had been completed, or, in state of “Done”.

    A quick note on this:

    The WHERE clause is the important part of the query inside the {$body} variable. By REST API design, the return of this data call will only contain the Work Item ID, and the Work Item URL – regardless of what columns you specify in the SELECT clause.
  1. Elect field data columns to export. Customize the {$customObject} and {$exportData} variables to output the fields you wish to export for found work items matching your query. Working inside PowerShell ISE, auto-complete helped me discover a number of available fields, both default and custom-defined in my custom work item type.

And that is pretty much it.

Assuming the user you run this script as has full access to the work items you are trying to query, each work item matching your criteria will be looped through to retrieve details and then exported as a .json file to your specified location.

This PowerShell Script and any future enhancements can be found in my Public GitHub repository – Azure DevOps Work Item Data Extraction.ps1

CHALLENGE

One notable challenge I ran into with this project is that the v6.0 of this DevOps REST API document mentions you should be able to query multiple work items at once and, retrieve the field data for each. This was not something I could get to work, as each call returned empty field data: “fields=;”

Working: $url = “https://dev.azure.com/$organization/$project/_apis/wit/workitems/12345?`$expand=All&api-version=5.0”
Not Working: $url = “https://dev.azure.com/$organization/$project/_apis/wit/workitems?ids=46477,43819&api-version=6.0”

I will update in the future if I figure out how to get this to work, but for now simply iterate through each returned work item matching query criteria, and 1 ID at a time retrieve the field data therein.

# Script:        Get DevOps WorkItem Data
# Last Modified: 4/10/2022 3:30 PM
#
# Note: If copying from Repo, add in your Personal-Access-Token (PAT) and sepcify export file location.
#--------------------------------------------------------------------------------
# 1. Use WIQL to get a queried for list of Work Items matching certain conditions
#--------------------------------------------------------------------------------
$token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

$organization = "AnalyticArchives"
$project = "ProDev"
$team = "ProDev Team"

$exportFileLocation = 'C:\Users\{YourUser}\Downloads'

$codedToken = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($token)"))
$AzureDevOpsAuthenicationHeader = @{Authorization = 'Basic ' + $codedToken}

$uri = "https://dev.azure.com/$organization/$project/$team/_apis/wit/wiql?api-version=6.0"

$body = @{
    #NOTE: Ignore the specified columns, only returns ID and URL by design
    "query" = "Select [System.Id], 
                      [System.Title], 
                      [System.State],
                      [Custom.FirstDetected] 
                      From WorkItems 
                      Where [System.WorkItemType] = 'Vulnerability' 
                      AND [State] = 'Done'"
} | ConvertTo-Json 

$response = Invoke-RestMethod -Method Post -Uri $uri -Headers $AzureDevOpsAuthenicationHeader -Body $body -ContentType 'application/json'

#--------------------------------------------------------------------------------
# 2. Use returned list of Work Item ID's to get field Data
#--------------------------------------------------------------------------------
$exportData = @()
$tempID = 0
foreach ($item in $response.workItems) {
    $tempID = $item.id
    $url = "https://dev.azure.com/$organization/$project/_apis/wit/workitems/$($tempID)?`$expand=All&api-version=5.0"
    $response = Invoke-RestMethod -Uri $url -Headers $AzureDevOpsAuthenicationHeader -Method Get -ContentType application/json 

    foreach ($field in $response) {

        $customObject = new-object PSObject -property @{

            "WorkItemID" = $field.fields.'System.Id'
            "URL" = $field.url
            "WorkItemType" = $field.fields.'System.WorkItemType'
            "Title" = $field.fields.'System.Title'
            "IterationPath" = $field.fields.'System.IterationPath'
            "State" = $field.fields.'System.State' 
            "AssignedTo" = $field.fields.'System.AssignedTo'.displayName
            "CoAssigned" = $field.fields.'Custom.CoAssigned'.displayName
            "FirstDetected" = $field.fields.'Custom.FirstDetected'
            "DueDate" = $field.fields.'Microsoft.VSTS.Scheduling.DueDate'
            "Priority" = $field.fields.'Microsoft.VSTS.Common.Priority'
            "Effort" = $field.fields.'Microsoft.VSTS.Scheduling.Effort'
            "ChangedDate" = $field.fields.'System.ChangedDate'
            "ChangedBy" = $field.fields.'System.ChangedBy'.displayName
            "ParentWorkItem" = $field.fields.'System.Parent'
            "RelatedWorkItems" = $field.relations
        } 
    $exportData += $customObject      
    }
}

$exportData | Select-Object -Property WorkItemID,
    URL,
    WorkItemType,
    Title,
    IterationPath,
    State,
    AssignedTo,
    CoAssigned,
    FirstDetected,
    DueDate,
    Priority,
    Effort,
    ChangedDate, 
    ChangedBy,
    ParentWorkItem,
    RelatedWorkItems

$exportData | ConvertTo-Json | Out-File "$($exportFileLocation)\DEVOPS_EXPORT.json"

Powered by WordPress & Theme by Anders Norén