In my article about how to decrypt SharePoint Online documents with PowerShell, I explained how to use the Unlock-SPOSensitivityLabelEncryptedFile cmdlet to decrypt protected SharePoint files by removing the sensitivity labels protecting the files. The example script uses cmdlets from the SharePoint PnP module to return a set of files from a folder in a document library for processing, and the unlock cmdlet then removes protection from any file with a sensitivity label.
The script works, but it’s not as flexible as I would like. For instance, because PnP can’t distinguish files with labels, every document in the folder is processed whether it is labelled or not. This does no harm, but it’s not something that you might want to do in the case of something like a tenant-to-tenant migration where thousands of protected documents might need to be processed.
Update May 10, 2021: The latest version of the SharePoint Online PowerShell module contains the Get-FileSensitivityLabelInfo cmdlet. This can be run to return the label status of a file, including if the label assigned to the file encrypts the file.The existence of this cmdlet removes some of the need to use the Graph to find and remove labels from protected files, but the Graph is still the fastest way to get the job done.
Using the Sites Microsoft Graph API
Which brings me to an updated version of the script (available from GitHub), which uses the Sites API from the Microsoft Graph to navigate through SharePoint Online and find labelled documents to process. Apart from being able to search for documents with sensitivity labels, a Graph API is usually the fastest way to deal with large numbers of objects.
Because we’re making Graph calls from PowerShell, we need to create a registered app in Azure AD to use as the entry point to the Graph (the same steps as outlined in this post are used). The app needs to be able to read site data, so I assigned it Sites.Read.All and Sites.ReadWrite.All permissions (Figure 1).
Figure 1: Setting API permissions for the Graph app
Finding Protected Documents
The script accepts two parameters: the name of the site to search (not the URL) and an optional folder. If multiple matching sites are found, the user is asked to choose which one to search (Figure 2).
Figure 2: Choosing a SharePoint Online site to investigate for protected documents
Once a target site is confirmed, the script figures out if a folder is specified and if that folder exists in the chosen site. In Graph terms, we’re now dealing with drive objects. The default drive is the root folder of a document library and each folder is a different drive. To find folders, we need to find the child objects in the root, identify the right folder, find its drive identifier, and use that to find the files in the folder. All good, clean Graph fun.
The Drive API returns a maximum of 200 items at a time, so some Nextlink processing is needed to fetch the complete set of files in a folder. Each file is examined to figure out if it has a sensitivity label with protection, and if so, the display name of the label. After processing all the files, we tell the user what we’ve found and ask permission to go ahead and decrypt the files (Figure 3). If the user chooses not to proceed, the script writes details of the protected files out to a CSV file.
Figure 3: Reporting the protected files found in a folder in a SharePoint Online document library
Decrypting Files
Files are decrypted by calling the Unlock-SPOSensitivityLabelEncryptedFile cmdlet. There’s no native Graph API call to decrypt SharePoint documents. In any case, we’re running a PowerShell script so it’s easy to call the cmdlet.
An Example to Build On
The script is an example of what’s possible with a combination of PowerShell and Graph API calls. I’m sure that the code and the functionality can be improved (feel free to suggest changes and improvements via GitHub). I’m just happy to demonstrate how things work and how including the Graph enables some extra flexibility.
Read the Office 365 for IT Pros eBook to find much more information about how sensitivity labels work – and many PowerShell examples too!
20 Replies to “How to Decrypt Protected SharePoint Files Using PowerShell and the Graph API”
I’ve tried running this on a site that I know for sure has lots of encrypted files but just get the following response “No encrypted files found in Site-Name – exiting”. Any ideas?
That’s because the script doesn’t go into subfolders. The code is written to illustrate a principle, not to handle every condition you might like it to. I would never have any time if I tried to do that… So I don’t…
Loading...
Love this script – shame that Microsoft haven’t got a good native way to decrypt content for tenant to tenant moves. I am running into an issue though with a test environment; the list of protected content is returned but for each item but when attempting to decrypt, see the error “Unlock-SPOSensitivityLabelEncryptedFile : Invalid URI: The format of the URI could not be determined.”. Any ideas?
Have you been able to run the cmdlet interactively with PowerShell? There’s something obviously up with the URI that’s been fed into it. So I would look at the URI and figure out what PowerShell is trying to process by starting with an attempt to remove a label from a single document.
This is a great article, just what I was looking for for our decryption toolbox! – Would you know of an article showing how to use PowerShell to apply a label to selected documents/folders in SPO? Cheers Danny
There’s an API coming. It’s not yet available. I hope to have some information about it before the end of July. However, to set expectations, the API will be throttled and is not designed for high-scale application of labels to documents.
Hi, Thanks for your article !. I tried to found the attribute “sensitivitylabel” from Microsoft Graph but this attribute does not appear in the results even if the file is encrypted. I tested in v1.0 and beta. Do you know why ? For the moment I had to retrieve all the files with graph and verify them with Get-FileSensitivityLabelInfo. Thanks !
Hi,
thanks for your answer. I reproduce your code and it’s work with your endpoint.
I understand now why it does not work on my side, I use a different endpoint to search all my items in one time (recursive search) and this endpoint does not have the sensitivity Label attribute (https://graph.microsoft.com/v1.0/sites/$($Siteid)/drives/$($libraryID)/list/items).
Tony, I have a question regarding the script above. For me it’s not running the way it should, and I refer at line 52 of code, the reason seems to be the site ID value, that instead of being only something like “12345678-12ab-12ab-34ab-123456abcdef”, it is tenant.sharepoint.com,12345678-12ab-12ab-34ab-123456abcdef,1234abcd-df36-49ba-a147-1234abc93582 (the ID’s are not real ID’s, just random numbers. Can you please help me select just the SiteID that is needed in the script line 75
I ran the script and everything went as planned. The $Site variable is filled by executing a Graph request to search for the site passed in $SearchSite. Maybe that variable isn’t populated? When the request runs, it populates the Value property in $Site with details of matching sites. Each site looks like this:
createdDateTime : 2021-10-30T11:21:58Z
description : All about PR
id : office365itpros.sharepoint.com,9ca62cd9-a20e-49c8-b116-c2596a64ddc5,ef300dac-c802-4d79-bc99-07
43c1b8ba62
lastModifiedDateTime : 2021-10-16T23:15:28Z
name : PublicRelations
webUrl : https://office365itpros.sharepoint.com/sites/PublicRelations
displayName : Public Relations
root :
siteCollection : @{hostname=office365itpros.sharepoint.com}
If ($Site.Value.Count -eq 0) { # Nothing found
Write-Host “No matching sites found – exiting”; break }
If ($Site.Value.Count -eq 1) { # Only one site found – go ahead
$SiteId = $Site.Value.Id
$SiteName = $Site.Value.DisplayName
Write-Host “Found site to process:” $SiteName }
Tony, I used Graph-explorer, and if I use the variable “$Site, which for you (above) is id : office365itpros.sharepoint.com,9ca62cd9-a20e-49c8-b116-c2596a64ddc5,ef300dac-c802-4d79-bc99-07
43c1b8ba62, the next variable $SiteId = $Site.Value.ID is the whole string, not just the ID of the site in this case 9ca62cd9-a20e-49c8-b116-c2596a64ddc5. If I use this hardcoded ID it works, and I get a list of files in Graph Explorer.
Bottom line is that we are trying to get a list of all PDF’s in SharePoint that were encrypted with sensitivity labels, and my knowledge in graph is limited. Using just PowerShell I am limited because of the throttling. Please let me know if you have any suggestions.
Loading...
I really can’t help too much because I don’t have access to your data. Without that access, it’s impossible for me to debug anything.
{"id":null,"mode":"button","open_style":"in_modal","currency_code":"EUR","currency_symbol":"\u20ac","currency_type":"decimal","blank_flag_url":"https:\/\/office365itpros.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/blank.gif","flag_sprite_url":"https:\/\/office365itpros.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/flags.png","default_amount":100,"top_media_type":"featured_image","featured_image_url":"https:\/\/office365itpros.com\/wp-content\/uploads\/2022\/11\/cover-141x200.jpg","featured_embed":"","header_media":null,"file_download_attachment_data":null,"recurring_options_enabled":true,"recurring_options":{"never":{"selected":true,"after_output":"One time only"},"weekly":{"selected":false,"after_output":"Every week"},"monthly":{"selected":false,"after_output":"Every month"},"yearly":{"selected":false,"after_output":"Every year"}},"strings":{"current_user_email":"","current_user_name":"","link_text":"Virtual Tip Jar","complete_payment_button_error_text":"Check info and try again","payment_verb":"Pay","payment_request_label":"Office 365 for IT Pros","form_has_an_error":"Please check and fix the errors above","general_server_error":"Something isn't working right at the moment. Please try again.","form_title":"Office 365 for IT Pros","form_subtitle":null,"currency_search_text":"Country or Currency here","other_payment_option":"Other payment option","manage_payments_button_text":"Manage your payments","thank_you_message":"Thank you for supporting the work of Office 365 for IT Pros!","payment_confirmation_title":"Office 365 for IT Pros","receipt_title":"Your Receipt","print_receipt":"Print Receipt","email_receipt":"Email Receipt","email_receipt_sending":"Sending receipt...","email_receipt_success":"Email receipt successfully sent","email_receipt_failed":"Email receipt failed to send. Please try again.","receipt_payee":"Paid to","receipt_statement_descriptor":"This will show up on your statement as","receipt_date":"Date","receipt_transaction_id":"Transaction ID","receipt_transaction_amount":"Amount","refund_payer":"Refund from","login":"Log in to manage your payments","manage_payments":"Manage Payments","transactions_title":"Your Transactions","transaction_title":"Transaction Receipt","transaction_period":"Plan Period","arrangements_title":"Your Plans","arrangement_title":"Manage Plan","arrangement_details":"Plan Details","arrangement_id_title":"Plan ID","arrangement_payment_method_title":"Payment Method","arrangement_amount_title":"Plan Amount","arrangement_renewal_title":"Next renewal date","arrangement_action_cancel":"Cancel Plan","arrangement_action_cant_cancel":"Cancelling is currently not available.","arrangement_action_cancel_double":"Are you sure you'd like to cancel?","arrangement_cancelling":"Cancelling Plan...","arrangement_cancelled":"Plan Cancelled","arrangement_failed_to_cancel":"Failed to cancel plan","back_to_plans":"\u2190 Back to Plans","update_payment_method_verb":"Update","sca_auth_description":"Your have a pending renewal payment which requires authorization.","sca_auth_verb":"Authorize renewal payment","sca_authing_verb":"Authorizing payment","sca_authed_verb":"Payment successfully authorized!","sca_auth_failed":"Unable to authorize! Please try again.","login_button_text":"Log in","login_form_has_an_error":"Please check and fix the errors above","uppercase_search":"Search","lowercase_search":"search","uppercase_page":"Page","lowercase_page":"page","uppercase_items":"Items","lowercase_items":"items","uppercase_per":"Per","lowercase_per":"per","uppercase_of":"Of","lowercase_of":"of","back":"Back to plans","zip_code_placeholder":"Zip\/Postal Code","download_file_button_text":"Download File","input_field_instructions":{"tip_amount":{"placeholder_text":"How much would you like to tip?","initial":{"instruction_type":"normal","instruction_message":"How much would you like to tip? Choose any currency."},"empty":{"instruction_type":"error","instruction_message":"How much would you like to tip? Choose any currency."},"invalid_curency":{"instruction_type":"error","instruction_message":"Please choose a valid currency."}},"recurring":{"placeholder_text":"Recurring","initial":{"instruction_type":"normal","instruction_message":"How often would you like to give this?"},"success":{"instruction_type":"success","instruction_message":"How often would you like to give this?"},"empty":{"instruction_type":"error","instruction_message":"How often would you like to give this?"}},"name":{"placeholder_text":"Name on Credit Card","initial":{"instruction_type":"normal","instruction_message":"Enter the name on your card."},"success":{"instruction_type":"success","instruction_message":"Enter the name on your card."},"empty":{"instruction_type":"error","instruction_message":"Please enter the name on your card."}},"privacy_policy":{"terms_title":"Terms and conditions","terms_body":null,"terms_show_text":"View Terms","terms_hide_text":"Hide Terms","initial":{"instruction_type":"normal","instruction_message":"I agree to the terms."},"unchecked":{"instruction_type":"error","instruction_message":"Please agree to the terms."},"checked":{"instruction_type":"success","instruction_message":"I agree to the terms."}},"email":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email address"},"success":{"instruction_type":"success","instruction_message":"Enter your email address"},"blank":{"instruction_type":"error","instruction_message":"Enter your email address"},"not_an_email_address":{"instruction_type":"error","instruction_message":"Make sure you have entered a valid email address"}},"note_with_tip":{"placeholder_text":"Your note here...","initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"empty":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"not_empty_initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"saving":{"instruction_type":"normal","instruction_message":"Saving note..."},"success":{"instruction_type":"success","instruction_message":"Note successfully saved!"},"error":{"instruction_type":"error","instruction_message":"Unable to save note note at this time. Please try again."}},"email_for_login_code":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email to log in."},"success":{"instruction_type":"success","instruction_message":"Enter your email to log in."},"blank":{"instruction_type":"error","instruction_message":"Enter your email to log in."},"empty":{"instruction_type":"error","instruction_message":"Enter your email to log in."}},"login_code":{"initial":{"instruction_type":"normal","instruction_message":"Check your email and enter the login code."},"success":{"instruction_type":"success","instruction_message":"Check your email and enter the login code."},"blank":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."},"empty":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."}},"stripe_all_in_one":{"initial":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"empty":{"instruction_type":"error","instruction_message":"Enter your credit card details here."},"success":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"invalid_number":{"instruction_type":"error","instruction_message":"The card number is not a valid credit card number."},"invalid_expiry_month":{"instruction_type":"error","instruction_message":"The card's expiration month is invalid."},"invalid_expiry_year":{"instruction_type":"error","instruction_message":"The card's expiration year is invalid."},"invalid_cvc":{"instruction_type":"error","instruction_message":"The card's security code is invalid."},"incorrect_number":{"instruction_type":"error","instruction_message":"The card number is incorrect."},"incomplete_number":{"instruction_type":"error","instruction_message":"The card number is incomplete."},"incomplete_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incomplete."},"incomplete_expiry":{"instruction_type":"error","instruction_message":"The card's expiration date is incomplete."},"incomplete_zip":{"instruction_type":"error","instruction_message":"The card's zip code is incomplete."},"expired_card":{"instruction_type":"error","instruction_message":"The card has expired."},"incorrect_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incorrect."},"incorrect_zip":{"instruction_type":"error","instruction_message":"The card's zip code failed validation."},"invalid_expiry_year_past":{"instruction_type":"error","instruction_message":"The card's expiration year is in the past"},"card_declined":{"instruction_type":"error","instruction_message":"The card was declined."},"missing":{"instruction_type":"error","instruction_message":"There is no card on a customer that is being charged."},"processing_error":{"instruction_type":"error","instruction_message":"An error occurred while processing the card."},"invalid_request_error":{"instruction_type":"error","instruction_message":"Unable to process this payment, please try again or use alternative method."},"invalid_sofort_country":{"instruction_type":"error","instruction_message":"The billing country is not accepted by SOFORT. Please try another country."}}}},"fetched_oembed_html":false}
I’ve tried running this on a site that I know for sure has lots of encrypted files but just get the following response “No encrypted files found in Site-Name – exiting”. Any ideas?
No idea. I have no knowledge of what conditions you’re running with. For instance, do you have site admin permissions?
GA
These files were originally labelled with AIP scanner does that make any difference?
Running into the same issue for nested content. Files in the root are discovered but files in subfolders are not.
That’s because the script doesn’t go into subfolders. The code is written to illustrate a principle, not to handle every condition you might like it to. I would never have any time if I tried to do that… So I don’t…
Love this script – shame that Microsoft haven’t got a good native way to decrypt content for tenant to tenant moves. I am running into an issue though with a test environment; the list of protected content is returned but for each item but when attempting to decrypt, see the error “Unlock-SPOSensitivityLabelEncryptedFile : Invalid URI: The format of the URI could not be determined.”. Any ideas?
Have you been able to run the cmdlet interactively with PowerShell? There’s something obviously up with the URI that’s been fed into it. So I would look at the URI and figure out what PowerShell is trying to process by starting with an attempt to remove a label from a single document.
This is a great article, just what I was looking for for our decryption toolbox! – Would you know of an article showing how to use PowerShell to apply a label to selected documents/folders in SPO? Cheers Danny
There’s an API coming. It’s not yet available. I hope to have some information about it before the end of July. However, to set expectations, the API will be throttled and is not designed for high-scale application of labels to documents.
Hi, Thanks for your article !. I tried to found the attribute “sensitivitylabel” from Microsoft Graph but this attribute does not appear in the results even if the file is encrypted. I tested in v1.0 and beta. Do you know why ? For the moment I had to retrieve all the files with graph and verify them with Get-FileSensitivityLabelInfo. Thanks !
I just tested the script again and this code retrieves all files in the target folder
If (!$SearchFolder) { # Search the root folder of the site
$Uri = “https://graph.microsoft.com/v1.0/sites/$($Siteid)/lists/Documents/Drive/root/children?`$select=sensitivitylabel,weburl,name” }
Else { # Search the nominated folder
$Uri = “https://graph.microsoft.com/v1.0/sites/$($Siteid)/lists/Documents/Drive/Items/$($DriveId)/children?`$select=sensitivitylabel,weburl,name
$Files = (Invoke-RestMethod -Uri $URI -Headers $Headers -Method Get -ContentType “application/json”)
And then this code filters for files with a sensitivity label assigned with protection:
ForEach ($File in $Files.Value) {
If ($File.SensitivityLabel.ProtectionEnabled -eq $True) {
$FileName = $BaseUrl + $File.Name
$ReportLine = [PSCustomObject] @{
File = $File.Name
FileURL = $FileName
Label = $File.SensitivityLabel.DisplayName
LabelGuid = $File.SensitivityLabel.Id }
$Report.Add($ReportLine) } #End If
} # End For
It all works. I get something like this:
File : How to Hide Documents from Delve.docx
FileURL : https://office365itpros.sharepoint.com/sites/BlogsAndProjects/Shared%20Documents/Blog%20Posts/How to
Hide Documents from Delve.docx
Label : Secret
LabelGuid : 81955691-b8e8-4a81-b7b4-ab32b130bff5
Hi,
thanks for your answer. I reproduce your code and it’s work with your endpoint.
I understand now why it does not work on my side, I use a different endpoint to search all my items in one time (recursive search) and this endpoint does not have the sensitivity Label attribute (https://graph.microsoft.com/v1.0/sites/$($Siteid)/drives/$($libraryID)/list/items).
Tony, I have a question regarding the script above. For me it’s not running the way it should, and I refer at line 52 of code, the reason seems to be the site ID value, that instead of being only something like “12345678-12ab-12ab-34ab-123456abcdef”, it is tenant.sharepoint.com,12345678-12ab-12ab-34ab-123456abcdef,1234abcd-df36-49ba-a147-1234abc93582 (the ID’s are not real ID’s, just random numbers. Can you please help me select just the SiteID that is needed in the script line 75
I ran the script and everything went as planned. The $Site variable is filled by executing a Graph request to search for the site passed in $SearchSite. Maybe that variable isn’t populated? When the request runs, it populates the Value property in $Site with details of matching sites. Each site looks like this:
createdDateTime : 2021-10-30T11:21:58Z
description : All about PR
id : office365itpros.sharepoint.com,9ca62cd9-a20e-49c8-b116-c2596a64ddc5,ef300dac-c802-4d79-bc99-07
43c1b8ba62
lastModifiedDateTime : 2021-10-16T23:15:28Z
name : PublicRelations
webUrl : https://office365itpros.sharepoint.com/sites/PublicRelations
displayName : Public Relations
root :
siteCollection : @{hostname=office365itpros.sharepoint.com}
$URI = “https://graph.microsoft.com/v1.0/sites?search=’$($SearchSite)'”
[array]$Site = (Invoke-RestMethod -Uri $URI -Headers $Headers -Method Get -ContentType “application/json”)
If ($Site.Value.Count -eq 0) { # Nothing found
Write-Host “No matching sites found – exiting”; break }
If ($Site.Value.Count -eq 1) { # Only one site found – go ahead
$SiteId = $Site.Value.Id
$SiteName = $Site.Value.DisplayName
Write-Host “Found site to process:” $SiteName }
Tony, I used Graph-explorer, and if I use the variable “$Site, which for you (above) is id : office365itpros.sharepoint.com,9ca62cd9-a20e-49c8-b116-c2596a64ddc5,ef300dac-c802-4d79-bc99-07
43c1b8ba62, the next variable $SiteId = $Site.Value.ID is the whole string, not just the ID of the site in this case 9ca62cd9-a20e-49c8-b116-c2596a64ddc5. If I use this hardcoded ID it works, and I get a list of files in Graph Explorer.
Bottom line is that we are trying to get a list of all PDF’s in SharePoint that were encrypted with sensitivity labels, and my knowledge in graph is limited. Using just PowerShell I am limited because of the throttling. Please let me know if you have any suggestions.
I really can’t help too much because I don’t have access to your data. Without that access, it’s impossible for me to debug anything.