I recently saw a post (maybe on Reddit) where someone was asking for a script that would download all the files in a SharePoint site. I found myself with a few spare minutes and an open ChatGPT window, so I took a swing at it. I think the results turned out pretty well. I do want to give a shout out to the Salaudeen Rajack at SharePoint Diary. We did get some help from this script of his.
I tried to write it as well as I could, so I wrote it as a function inside of a module file. Because of that you have to save the script as a PSM1 file (as opposed to a regular old PS1 file). Then use Import-Module to import that PSM1 file, which adds my function, Download-SharePointFiles to your PowerShell host. Then run Download-SharePointFiles with the correct parameters. It looks like this:
That gives you output that looks like this:
I hope you find it helpful. Let me know in the comments if there’s any functionality you’d like me to add.
The PnP team recently announced they were making changes to how the PnP.PowerShell and M365 CLI authenticate. The short story is that if you want to use the PnP.PowerShell module or the M365 CLI after September 9th, 2024, you’ll need to create your own Application Registration, also known as an App Reg. I know, I know, that sounds confusing and scary. Fortunately there’s nothing to it. I’ve got all the steps for you to follow along. You’ll be ready to go in no time.
If you’re reading this, I assume you already have the PnP.PowerShell module installed. Good, because you’ll need that. The steps I’m going to show you will probably work with any version 2.0.0 or later, but I recommend updating to at least 2.9.0, which is the latest version as of this blog post. It’s got some extra sauce in it to make this go more smoothly.
After you’ve got the PnP.PowerShell module installed, open up PowerShell. This module has supported using custom app regs for a while, so all of the plumbing is already there. We need to run the Register-PnPAzureADApp cmdlet (also aliases as Register-EntraIDApp, they’re the same thing) to create our own App Reg. Example #1 from the help is what I use. Here’s what it looks like:
The name of the App Reg we’re creating is “PnP.PowerShell.” My tenant is 1kgvf, but of course you’ll use your own. It’s going to create a certificate for us (we won’t need it) and store it in the CurrentUser Certificate Store. And finally, since I’m using MFA, like every good user does, I use the –Interactive switch to do browser based authentication. You’ll have to log in as a Global Admin, or a user the Global Admin has given permission to create App Regs to.
After I log in Azure gets to work doing what it does. Since I didn’t pass any scopes to Register-PnPAzureADApp it uses its default set. You’ll get prompted to authenticate a second time then asked to consent to them.
We’ll talk more about that in a bit.
Once it’s done, you’ll get a screen like this:
Here is where you’ll get the Client ID (also called AppID and AzureAppID, it’s all the same) you’ll need when you connect. In my example that’s 001ed5a0-be10-4bc3-a40c-a1cad0c987b7. You can also get that ID number by going into the Azure Portal and looking at the Enterprise applications.
Find your App Reg and click it.
Then you can copy it to your clipboard.
Now that you have your ClientID, let’s use that to connect to Microsoft 365. In my case I would use this connect statement:
Of course you’ll use your own tenant name and the ClientID that you created. The one I created isn’t visible to your tenant.
You’ll get prompted for a username and password, and hopefully some MFA. Then you’ll be connected to M365. Here’s how it all looks:
As you can see, I’m connected and a quick function check looks like everything is working fine. Success!!! Well, sort of…
We talked before about scopes, and how the Register-AzureADApp cmdlet used its default scopes since I didn’t specify any. Those scopes covered SharePoint, and Users, but not much else. For instance, if I try to get a list of the Teams in my tenant, I’m met with an authentication screen. After I authenticate (with a Global Admin) I get a page wanting more permission:
Now it wants access to Read all groups, and more User permissions. I clicked Consent and then Accept. It returned my Teams to me.
This adds some complication. If the user you normally use PnP.PowerShell with is a GA, then you’re golden. Every time you stumble onto something you can’t do, it’ll prompt you and you’ll consent. If you have a separate GA user, or someone else is doing the GA stuff for you, you’ll have to go into the Azure Portal and add the additional Scopes in there. That sounds like something that would fit really well in another blog post.
There’s another gotcha. By default, when Register-PnPAzureADApp creates the App Reg, it only gives the user that created it permission to use it. If anyone else needs to use it you’ll need to go into the Azure Portal, open the App Reg, go to the Users and Groups blade, and add the additional users. That won’t give them any additional permissions anywhere in M365, it simply gives them permission to use this App Reg when using PnP.PowerShell.
I mentioned at the top that version 2.9.0 had some extra sauce to help with this. It can get a bit tedious adding the –ClientID SomeUglyGUID part every time you connect. This is particularly painful if you’ve got scripts and the like with connect statements that don’t have the -ClientID parameter. In 2.9.0 and later Connect-PnPOnline supports an environment variable ENTRAID_APP_ID. If no –ClientID is specified, and that variable has a value, Connect-PnPOnline will use that. Here’s what it looks like:
That variable value will go away when you close PowerShell. If you put the $env:ENTRAID_APP_ID = "001ed5a0-be10-4bc3-a40c-a1cad0c987b7" line in your PowerShell profile it will get populated every time you open PowerShell. If you’re a local admin on your machine you can also set a persistent environment variable in an admin prompt with this line:
My buddies at Syskit and I were chatting recently and we realized we hadn’t worked together in far, far too long. We decided that needed to be remedied right away! So last week I wrote a blog post for them called Using AI to write PowerShell scripts. It’s even more fun than it sounds, I promise. In it I cover my process for using AI to write even cooler PowerShell scripts than I would be able to on my own. You can even use the free version of ChatGPT to write better PowerShell.
During the last Ask Sympraxis our friend Kasper Larsen relayed a question he had gotten recently, “Is it possible to run the PnP.PowerShell module if I’m not allowed to install it or PowerShell 7?” The question brought tears of sadness to my eyes. Then, my indomitable spirit kicked in, “We’ll help this person!”, it said. And here we are.
The short answer to, “Can you run PnP.PowerShell if you can’t install anything” is a resounding, “Yes! Heck yes you can!” The answer to “how” comes in two parts. The first is to download the PowerShell 7 Zip file and run pwsh.exe out of there without installing it. The second part is to install the PnP.PowerShell module in the CurrentUser scope, so that it doesn’t try to write anything to a protected directory. After that, run PnP.PowerShell cmdlets to your heart’s content.
Here’s what it looks like:
There’s proof, the PowerShell way, that the user I’m logged in as isn’t an admin. First, I download the PowerShell 7 zip file and extract it to a folder in my Downloads folder.
Then I CD to the directory and run pwsh.exe, like this:
You can see from the $PSVersionTable that we’re running PowerShell 7. Now I install the PnP.PowerShell module to my user with the line Install-Module pnp.powershell –Scope CurrentUser.
After I run the install I use Connect-PnPOnline like I normally would. At the bottom I highlighted where the module is installed, your personal Documents directory.
One very important note, is that you (or anyone) won’t be able to connect if the PnP.PowerShell application registration hasn’t been approved in your tenant. This blog post, “How to Register the PnP.PowerShell App Registration if You’re not a Tenant Admin” covers it a bit. That App Registration is necessary in 99% of the use cases. You can connect and do a few SharePoint things without it, but that list is pretty short.
I’m not sure how often this will come up, but hopefully this blog post is at least interesting.
If you’ve been around the block with SharePoint or Microsoft 365 administration, you know that handling client credentials can sometimes feel like juggling with fire. When I start a new engagement with a client I generally get credentials to access their tenant. Of course they immediately go into our password management tool. I also do a lot of PowerShell scripting for my clients, so I save them to the Windows Credential store too, so that I can connect with Connect-PnPOnline without having to enter them each time. And while that’s not a lot of work, I thought I could streamline it. That’s why I’ve put together a PowerShell command, Add-ClientCredential, that makes it a little easier.
What Does Add-ClientCredential do?
In a nutshell, this PowerShell command is designed to streamline the process of adding client credentials in a SharePoint or Microsoft 365 environment. It stores your credential for https://tenant.sharepoint.com and https://tenant-admin.sharepoint.com. If you don’t specify any credentials when you connect with Connect-PnPOnline it will look for them in the Windows Credential Store. If you have one assigned for the root of the tenant, https://tenant.sharepoint.com, it will also use that for other sites in the tenant, like https://tenant.sharepoint.com/sites/ToddisTheBest, if a credential is not saved for that specific site.
Keep in mind that will show up in plain text in PowerShell’s Get-History, or the Transcript file, if you have that running.
I built in some smarts so that if there is already a credential stored for “Contoso” it will let you know and ask you if you want to overwrite it.
If you want to get super fancy you can add the –TestCredential switch which will test the credentials you gave it by logging in with them. All of this is available if you run help Add-ClientCredential –Examples.
In the background this function usesAdd-PnPStoredCredential to store the credentials for you. It adds them for the root of the tenant, and the –admin URL.
How to Get Started
To get your hands on this little beauty, head over to my GitHub repository. You can download addclientcreds.psm1 itself, or clone the whole repo. Use Import-Module to import it into your PowerShell host and you’re ready to go.
Wrapping Up
addclientcreds.psm1 is my attempt to put a little more simplicity and sanity into the world of SharePoint and Microsoft 365 administration. I hope you find it as useful as I do. As always, I welcome your feedback and questions. Drop a comment below or shoot me a message on Twitter @ToddKlindt.
Hello, SharePoint and PowerShell enthusiasts! Todd Klindt here, and I’ve got something exciting to share with you today. I recently had the opportunity to present a developer-focused demo on the Microsoft 365 & Power Platform Community channel. The topic? Dynamic parameter validation in PowerShell.
In this 13-minute demo, I walk you through the process of creating your own PowerShell cmdlet. This cmdlet allows you to pull and tab through information from a site, list, text file, Azure, Graph, etc. within the PowerShell environment.
I utilize the power of ValidateSet, ValidateScript, and ArgumentCompleter. To show off, the demo concludes with an interesting twist. I used ChatGPT to write the same code.
All the code I used in the demo is available on my GitHub repo. You can find it at PoshPnP and PnP PowerShell.
I hope you find this demo useful in your PowerShell journey. Remember, when you’re writing PowerShell, try to be the tool maker, not the tool user. Happy coding!
Today, I want to share a handy PowerShell function I developed recently to enhance readability of ShareGate log files.
If you are using ShareGate for SharePoint migrations or management tasks, you might be familiar with the extensive Excel logs produced by ShareGate. While these logs are rich in details, they can sometimes be too rich in details. They often require some formatting for better readability or to highlight the necessary details. When doing a lot of migrations I found myself doing the same steps over and over again to these logs. Being lazy, I thought, “Someone should automate this!”. I sat down with my buddy PowerShell and this is what we came up with.
This function, Format-ShareGateLogFile, tackles this by opening a ShareGate log file in Excel format and applying a few changes. It does this by using Doug Finke’sexcellent ImportExcel PowerShell Module. It adds a table to the first worksheet, formats the first column as “Date-Time”, calculates the duration of the log file, and formats the duration as “[h]:mm:ss” in the last row. Finally, it saves the changes and closes the Excel file.
To use this function, you’ll have to download Logfiles.psm1 from my Github Repo. Then use Import-Module to import it into your PowerShell session.
After that’s done you can run Format-ShareGateLogFile. You will need to provide the path to the Excel file to format as an argument to the Path parameter, which is mandatory. For instance,
This will format the Excel file located at “C:\path\to\ShareGateLogFile.xlsx” for readability.
The function also accepts two optional switches: Open and HideColumns. If you use the Open switch, the function will open the formatted Excel file automatically after it has finished formatting. The HideColumns switch will hide specified columns (E-U, W-AR, AT-BA) in the Excel file. For instance,
This will format the Excel file, hide the specified columns, and open the Excel file automatically after it has been formatted.
One neat feature of the function is that it can accept pipeline input for the `Path` parameter. This means you can pipe in a series of file paths to the function and it will format each file in turn. For instance,
This will get all the Excel (.xlsx) files in the specified folder, and for each one, it will be formatted for readability, with specified columns hidden, and the Excel file opened automatically after it has been formatted.
This function has saved me heaps of time while working with ShareGate log files, and I hope it does the same for you. Happy scripting!
Hey there, PowerShell enthusiasts and fellow M365 admins! I’ve been tinkering around with a little something I think you’ll find helpful, especially if you’ve been playing around with OpenAI’s ChatGPT. I have been and let me tell you, this has been a wild ride.
Meet Format-ChatGPTConversation, it’s a function I wrote that takes your ChatGPT conversation history and formats it into a more digestible output. If you’ve been in the trenches with ChatGPT, you know that it can be tough to share your conversations with other people. You have to take a bunch of screenshots or just copy out individual parts. Neither of those are any fun. That’s where this function comes into play.
Now, you might be thinking, “That’s great, but where do I find these ChatGPT conversations?” Good news! You can get the conversations.json file right from the OpenAI chat interface. Just head over to https://chat.openai.com/, click on the three dots by your name in the lower left corner, and navigate to “Settings”. Once you’re in there, click the “Data controls” tab, and voila! There’s an “Export” button waiting for you. Clicking that will give you a zip file, and nestled inside it is your conversations.json file.
So what does Format-ChatGPTConversation actually do? Well, it processes this JSON file, extracting relevant conversation details like the title, ID, create time, author, and content. Any system messages or messages without an author are left out. We don’t need them muddying up our beautiful output, do we?
Let’s talk about how to use this bad boy. You can pipe it to the console, save the formatted output to a text file, pass multiple JSON files through the pipeline, or even group the output by conversation title. Here are a few examples:
All of those examples are included in the Examples in the function. You can get to them with help Format-ChatGPTConversation -Examples after you’ve downloaded and imported it.
Ain’t that a sight for sore eyes? It’s like finding a pearl in an oyster. The world of PowerShell and ChatGPT is your oyster, my friends. Go forth and explore.
How do you get this marvel of modern technology? Go to my PowerShell repo on Github and download it. Then use Import-Module to import it into your PowerShell session.
As always, I’d love to hear how you’re using this function. Are you finding it helpful? Have you made any tweaks or improvements? Let’s have a conversation about our ChatGPT conversations. It’s like Inception, but with less Leonardo DiCaprio and more PowerShell.
Have you ever updated some properties for a user in SharePoint Online, waited what seemed like a reasonable length of time, then did a search for that user? Have you ever done that and found your old values still there? Gah! Me too! Did you then use Mikael Svenson’s brilliant script to fix that? Yeah, me too. A few times.
I’m also a tinkerer, and I love me some PowerShell. So I cracked open VS Code, put on some aggressive music, loaded up Mikael’s code, and looked around a bit. I found a couple of things that I’d do differently. Not that there’s anything wrong with his code, it’s good stuff. But I wanted to so some stuff like turn his code into a function, so I could add it to an existing module I have. I wanted to move the JSON file from the root of the web to the Shared Documents library, innocent little changes like that. And his code had been written several years ago. PowerShell has matured since then, as coders we’ve matured, so I cleaned a few other little things up while I was in there. It was fun, and it kept me off the streets for a while.
After I got done I reached out to Mikael to see if he was okay with me sharing it with all of you. Mikael, being the stand up guy that he is, was totally good with it. Then I reached out to Paul Bullock about getting it published to the PnP Script Samples. I’ve been meaning to start submitting some stuff to that, but I didn’t have anything I thought was worthy. Paul graciously accepted my script, so now you can all enjoy it too.
While you’re there, check out all the other great script samples. There’s some gold in those hills.
As an M365 Admin it’s not tough to see that the future is hidden somewhere in the Microsoft Graph. And this particular M365 Admin would like to get there using his favorite tool, PowerShell. Microsoft released the Graph SDK, but it didn’t really scratch the itch for me. I found it confusing and really tough to use. But, as much as I ignored it, the Graph didn’t go away. With increased frequency there would be some little tidbit of information I’d want and the answer would be, “It’s in the Graph.” Grrrrr.
It uses PnP.PowerShell, so you’ll need that module installed, and you’ll need to connect with Connect-PnPOnline. From there you can pass it any Graph endpoint, v1.0 or beta. You can find out what the endpoints are at the Graph Explorer. Right now this only supports Gets. You’ll also have to manually put in any parameters or filters. I’ve added help and examples. I hope to expand them both in the future.
The output is a stand PSCustomObject, so you can send its output down the pipeline however you’d like. Select-Object, Where-Object, Sort-Object, the world is your oyster.
Let me know if this is useful and what you’re doing with it.