From 0566515bfce42ba772b86a4d8eac0dd580cc6e11 Mon Sep 17 00:00:00 2001 From: rdonovan92 Date: Tue, 17 Mar 2026 13:41:49 -0500 Subject: [PATCH 1/9] Initial Release, added first batch of functions and pester template for module release --- .github/workflows/pester.yml | 20 +++ .github/workflows/scriptanalyzer.yml | 21 +++ CHANGELOG.md | 8 +- LICENSE | 50 +++---- README.md | 130 ++++++++++++++---- src/UofIBox/UofIBox.psd1 | 129 +++++++++++++++++ src/UofIBox/UofIBox.psm1 | 6 + src/UofIBox/dscresources/.gitkeep | 0 src/UofIBox/functions/private/.gitkeep | 0 src/UofIBox/functions/public/.gitkeep | 0 .../functions/public/Get-BoxFileMetadata.ps1 | 29 ++++ .../public/Get-BoxFolderMetadata.ps1 | 29 ++++ .../functions/public/Invoke-BoxRestCall.ps1 | 97 +++++++++++++ .../public/New-BoxFolderCollaboration.ps1 | 87 ++++++++++++ .../functions/public/New-BoxSession.ps1 | 63 +++++++++ .../functions/public/Receive-BoxFile.ps1 | 36 +++++ .../functions/public/Receive-BoxFolder.ps1 | 75 ++++++++++ .../functions/public/Remove-BoxFile.ps1 | 29 ++++ .../functions/public/Remove-BoxFolder.ps1 | 26 ++++ src/UofIBox/functions/public/Send-BoxFile.ps1 | 54 ++++++++ test/UofIBox.Tests.ps1 | 9 ++ 21 files changed, 842 insertions(+), 56 deletions(-) create mode 100644 .github/workflows/pester.yml create mode 100644 .github/workflows/scriptanalyzer.yml create mode 100644 src/UofIBox/UofIBox.psd1 create mode 100644 src/UofIBox/UofIBox.psm1 create mode 100644 src/UofIBox/dscresources/.gitkeep create mode 100644 src/UofIBox/functions/private/.gitkeep create mode 100644 src/UofIBox/functions/public/.gitkeep create mode 100644 src/UofIBox/functions/public/Get-BoxFileMetadata.ps1 create mode 100644 src/UofIBox/functions/public/Get-BoxFolderMetadata.ps1 create mode 100644 src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 create mode 100644 src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 create mode 100644 src/UofIBox/functions/public/New-BoxSession.ps1 create mode 100644 src/UofIBox/functions/public/Receive-BoxFile.ps1 create mode 100644 src/UofIBox/functions/public/Receive-BoxFolder.ps1 create mode 100644 src/UofIBox/functions/public/Remove-BoxFile.ps1 create mode 100644 src/UofIBox/functions/public/Remove-BoxFolder.ps1 create mode 100644 src/UofIBox/functions/public/Send-BoxFile.ps1 create mode 100644 test/UofIBox.Tests.ps1 diff --git a/.github/workflows/pester.yml b/.github/workflows/pester.yml new file mode 100644 index 0000000..16dfcba --- /dev/null +++ b/.github/workflows/pester.yml @@ -0,0 +1,20 @@ +name: Pester Tests + +on: [pull_request] + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-18.04, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + run: Install-Module -Name 'Pester' -Force -SkipPublisherCheck + shell: pwsh + - name: Run tests + run: Invoke-Pester -Path './test/' -CI + shell: pwsh diff --git a/.github/workflows/scriptanalyzer.yml b/.github/workflows/scriptanalyzer.yml new file mode 100644 index 0000000..da85e6f --- /dev/null +++ b/.github/workflows/scriptanalyzer.yml @@ -0,0 +1,21 @@ +name: ScriptAnalyzer + +on: [pull_request] + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest] + + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + run: Install-Module -Name 'PSScriptAnalyzer' -Force -SkipPublisherCheck + shell: pwsh + - name: ScriptAnalyzer + run: Invoke-ScriptAnalyzer -Path '.\src\' -Recurse -EnableExit + shell: pwsh + diff --git a/CHANGELOG.md b/CHANGELOG.md index eafbb04..eb6da1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - + ## Unreleased ### Added @@ -13,3 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed ### Removed + +## [1.0.0] - 2026-03-17 + +### Added + + - Initial Release, first batch of functions added to the module. diff --git a/LICENSE b/LICENSE index 23fa39a..45c0855 100644 --- a/LICENSE +++ b/LICENSE @@ -1,35 +1,29 @@ -University of Illinois/NCSA Open Source License +Copyright (c) 2020 University of Illinois. All rights reserved. -Copyright (c) 2023 by the Board of Trustees of the University of -Illinois. All rights reserved. -Developed by Cybersecurity and Technology Services -University of Illinois -https://techservices.illinois.edu +Developed by: Cybersecurity Engineering + University of Illinois + https://github.com/techservicesillinois/Secops-Powershell-Box -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation files -(the "Software"), to deal with the Software without restriction, -including without limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -- Redistributions of source code must retain the above copyright notice, +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal with +the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to +do so, subject to the following conditions: +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimers. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the documentation + and/or other materials provided with the distribution. +* Neither the names of Cybersecurity Engineering, University of Illinois, + nor the names of its contributors may be used to endorse or promote products + derived from this Software without specific prior written permission. -- Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimers in the - documentation and/or other materials provided with the distribution. - -- Neither the names of University of Illinois nor the names of its - contributors may be used to endorse or promote products derived from - this Software without specific prior written permission. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +SOFTWARE. diff --git a/README.md b/README.md index 18987b7..d4f1c06 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,119 @@ -## About +![Pester Tests](https://github.com/techservicesillinois/Secops-Powershell-Box/workflows/Pester%20Tests/badge.svg) +![ScriptAnalyzer](https://github.com/techservicesillinois/Secops-Powershell-Box/workflows/ScriptAnalyzer/badge.svg) -This repository is used by Cybersecurity operations teams at the -University of Illinois as a GitHub template. +# What is This? -This resource helps comply with University of Illinois -Cybersecurity standards - including [IT-07][it07], [IT08][it08], -and [IT13][it13]. +This is a PowerShell module for automating interactions with the Box API. It provides a clean, reusable set of functions for managing folders, files, and user access within Box. -[it07]: https://go.illinois.edu/secstd-IT07 -[it08]: https://go.illinois.edu/secstd-IT08 -[it13]: https://go.illinois.edu/secstd-IT13 +The module is designed for automation scenarios such as: -See [Cybersecurity Development on the Illinois Knowledge Base][kbsearch] -for information about our development standards. +Ticket-driven provisioning -[kbsearch]: https://answers.uillinois.edu/illinois/search.php?q=cybersecurity+developer&cat=0 +Scheduled jobs -The remainder of this `README.md` contains example text. +Azure Automation runbooks -## Data Sources +Security and compliance workflows -|Data Store|Data Type|Sensitivity|Notes| -|----------|---------|-----------|-----| +It simplifies Box API usage by handling authentication, REST calls, and common operations behind easy-to-use PowerShell functions. -## Endpoint Connections +# How do I install it? -|Endpoint|Purpose|Stage|Access| -|--------|-------|-----|------| +### Option 1: Clone the Repository +git clone +cd +Import-Module .\BoxAutomation.psm1 -## Product Support +### Option 2: Manual Import -This product is supported by Cybersecurity teams at the -University of Illinois Urbana-Champaign on a best-effort basis. +Download the module files and import directly: -As of the last update to this README, the expected End-of-Life and -End-of-Support dates of this product are . +Import-Module "C:\Path\To\BoxAutomation.psm1" -End-of-Life was decided upon based on these dependencies: +### Prerequisites - - - - +PowerShell 7+ +A Box application configured for Client Credentials Grant + +Box Client ID, Client Secret, and Enterprise ID + +# How does it work? + +The module is built around two core components: + +Authentication + +New-BoxSession authenticates to Box using OAuth Client Credentials and stores the access token for reuse. + +$Credential = Get-Credential +New-BoxSession -Credential $Credential +REST Wrapper + +Invoke-BoxRestCall is a centralized wrapper for all API calls. It handles: + +Authentication headers + +Base URI construction + +JSON conversion + +Error handling + +File downloads + +Key Functions +Folder Management + +New-BoxFolderWithCollaboration – Create folders and assign users/roles + +Get-BoxFolder – Retrieve folder metadata + +Remove-BoxFolder – Delete folders + +File Management + +Upload-BoxFile – Upload files + +Get-BoxFile – Retrieve file metadata + +Remove-BoxFile – Delete files + +Receive-BoxFile – Download files + +Folder Download + +Receive-BoxFolder – Recursively downloads a folder and recreates the structure locally + +Example Workflow +$Credential = Get-Credential +New-BoxSession -Credential $Credential + +New-BoxFolderWithCollaboration ` + -FolderName "SecurityProject" ` + -Login user@company.com ` + -Role editor + +# How do I help? + +Contributions are welcome. You can help by: + +Reporting bugs or issues + +Suggesting new features + +Improving documentation + +Refactoring functions for performance or readability + +If contributing: + +Follow existing function structure and naming conventions + +Use splats instead of backticks + +Ensure all functions use Invoke-BoxRestCall + +Include comment-based help for all functions + +# To Do diff --git a/src/UofIBox/UofIBox.psd1 b/src/UofIBox/UofIBox.psd1 new file mode 100644 index 0000000..00efefb --- /dev/null +++ b/src/UofIBox/UofIBox.psd1 @@ -0,0 +1,129 @@ +# +# Module manifest for module 'UofIBox' +# +# Generated by: Cybersecurity Engineering +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'UofIBox.psm1' + +# Version number of this module. +ModuleVersion = '1.0.0' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '308d74d8-6717-4324-be7d-206fc0adc551' + +# Author of this module +Author = 'Cybersecurity Engineering' + +# Company or vendor of this module +CompanyName = 'The University of Illinois' + +# Copyright statement for this module +Copyright = 'The University of Illinois Board of Trustees' + +# Description of the functionality provided by this module +Description = 'Powershell Wrapper for the Box API' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '7.0' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# 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. +# FunctionsToExport = '*' + +# 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. +# CmdletsToExport = '*' + +# Variables to export from this module +# VariablesToExport = '*' + +# 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. +# AliasesToExport = '*' + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# 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. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + LicenseUri = 'https://github.com/techservicesillinois/Secops-Powershell-Box/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/techservicesillinois/Secops-Powershell-Box' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} diff --git a/src/UofIBox/UofIBox.psm1 b/src/UofIBox/UofIBox.psm1 new file mode 100644 index 0000000..50d2a76 --- /dev/null +++ b/src/UofIBox/UofIBox.psm1 @@ -0,0 +1,6 @@ +[String]$FunctionPath = Join-Path -Path $PSScriptRoot -ChildPath 'Functions' +#All function files are executed while only public functions are exported to the shell. +Get-ChildItem -Path $FunctionPath -Filter "*.ps1" -Recurse | ForEach-Object -Process { + Write-Verbose -Message "Importing $($_.BaseName)" + . $_.FullName | Out-Null +} \ No newline at end of file diff --git a/src/UofIBox/dscresources/.gitkeep b/src/UofIBox/dscresources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/UofIBox/functions/private/.gitkeep b/src/UofIBox/functions/private/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/UofIBox/functions/public/.gitkeep b/src/UofIBox/functions/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/UofIBox/functions/public/Get-BoxFileMetadata.ps1 b/src/UofIBox/functions/public/Get-BoxFileMetadata.ps1 new file mode 100644 index 0000000..2f97a58 --- /dev/null +++ b/src/UofIBox/functions/public/Get-BoxFileMetadata.ps1 @@ -0,0 +1,29 @@ +<# +.SYNOPSIS +Retrieves metadata for a Box file. + +.DESCRIPTION +Returns file information including size, name, and owner. + +.PARAMETER FileId +The ID of the Box file. + +.EXAMPLE +Get-BoxFile -FileId "123456789" +#> + +function Get-BoxFileMetadata { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$FileId + ) + + $Call = @{ + RelativeURI = "files/$FileId" + Method = "GET" + } + + Invoke-BoxRestCall @Call +} \ No newline at end of file diff --git a/src/UofIBox/functions/public/Get-BoxFolderMetadata.ps1 b/src/UofIBox/functions/public/Get-BoxFolderMetadata.ps1 new file mode 100644 index 0000000..2400b84 --- /dev/null +++ b/src/UofIBox/functions/public/Get-BoxFolderMetadata.ps1 @@ -0,0 +1,29 @@ +<# +.SYNOPSIS +Retrieves Box folder metadata. + +.DESCRIPTION +Gets metadata information for a specified Box folder. + +.PARAMETER FolderId +The ID of the Box folder. + +.EXAMPLE +Get-BoxFolder -FolderId "123456789" +#> + +function Get-BoxFolderMetadata { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$FolderId + ) + + $Call = @{ + RelativeURI = "folders/$FolderId" + Method = "GET" + } + + Invoke-BoxRestCall @Call +} \ No newline at end of file diff --git a/src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 b/src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 new file mode 100644 index 0000000..9cca69f --- /dev/null +++ b/src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 @@ -0,0 +1,97 @@ +<# +.SYNOPSIS +Makes a REST API call to the Box API. + +.DESCRIPTION +Wrapper for Invoke-RestMethod that handles authentication, +headers, retries, and base URI handling for the Box API. + +.PARAMETER RelativeURI +Relative URI of the Box endpoint. +Example: "folders/123456" + +.PARAMETER Method +HTTP method (GET, POST, PUT, DELETE) + +.PARAMETER Body +Hashtable body that will be converted to JSON. + +.PARAMETER Upload +Switch indicating the upload API endpoint should be used. + +.EXAMPLE +Invoke-BoxRestCall -RelativeURI "folders/123" -Method GET + +.EXAMPLE +Invoke-BoxRestCall -RelativeURI "folders" -Method POST -Body $Body +#> + +function Invoke-BoxRestCall { + + [CmdletBinding(DefaultParameterSetName='Body')] + param ( + [Parameter(Mandatory)] + [string]$RelativeURI, + + [Parameter(Mandatory)] + [string]$Method, + + [hashtable]$Body, + + [switch]$Upload + ) + + begin { + + if ($null -eq $Script:BoxSession) { + throw "No Box session established. Run New-BoxSession first." + } + + if ($RelativeURI.StartsWith('/')) { + $RelativeURI = $RelativeURI.Substring(1) + } + + if ($Upload) { + $BaseURI = "https://upload.box.com/api/2.0/" + } + else { + $BaseURI = "https://api.box.com/2.0/" + } + } + + process { + + $IVRSplat = @{ + Headers = @{ + Authorization = "Bearer $($Script:BoxSession.AccessToken)" + "Content-Type" = "application/json" + } + Method = $Method + Uri = "$BaseURI$RelativeURI" + } + + if ($Body) { + $IVRSplat.Add("Body", ($Body | ConvertTo-Json -Depth 10)) + } + + Write-Verbose "Calling Box API: $Method $RelativeURI" + + try { + + $Result = Invoke-RestMethod @IVRSplat + $Script:APICallCount++ + + return $Result + } + catch { + + Write-Verbose "Box API call failed, retrying in 3 seconds..." + Start-Sleep -Seconds 3 + + $Result = Invoke-RestMethod @IVRSplat + $Script:APICallCount++ + + return $Result + } + } +} \ No newline at end of file diff --git a/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 b/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 new file mode 100644 index 0000000..a48cc64 --- /dev/null +++ b/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 @@ -0,0 +1,87 @@ +<# +.SYNOPSIS +Creates a Box folder and assigns collaborators. + +.DESCRIPTION +Creates a new folder and assigns users with the specified roles. + +.PARAMETER FolderName +Name of the folder to create. + +.PARAMETER ParentFolderId +Parent folder ID. + +.PARAMETER Collaborators +Array of collaborator objects containing login and role. + +.PARAMETER ReturnFolderLink +Returns a link to the folder. + +.EXAMPLE +New-BoxFolderCollaboration -FolderName "Finance" -Collaborators $Users +#> +function New-BoxFolderCollaboration { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$FolderName, + + [string]$ParentFolderId = "0", + + [Parameter(Mandatory)] + [array]$Collaborators, + + [switch]$ReturnFolderLink + ) + + $FolderBody = @{ + name = $FolderName + parent = @{ + id = $ParentFolderId + } + } + + $CreateFolder = @{ + RelativeURI = "folders" + Method = "POST" + Body = $FolderBody + } + + $FolderResponse = Invoke-BoxRestCall @CreateFolder + $FolderId = $FolderResponse.id + + foreach ($User in $Collaborators) { + + $CollabBody = @{ + item = @{ + type = "folder" + id = $FolderId + } + accessible_by = @{ + type = "user" + login = $User.login + } + role = $User.role + } + + $CollabCall = @{ + RelativeURI = "collaborations" + Method = "POST" + Body = $CollabBody + } + + Invoke-BoxRestCall @CollabCall + } + + if ($ReturnFolderLink) { + + return [PSCustomObject]@{ + FolderName = $FolderName + FolderId = $FolderId + FolderUrl = "https://app.box.com/folder/$FolderId" + } + } + + return $FolderResponse +} \ No newline at end of file diff --git a/src/UofIBox/functions/public/New-BoxSession.ps1 b/src/UofIBox/functions/public/New-BoxSession.ps1 new file mode 100644 index 0000000..0058b2d --- /dev/null +++ b/src/UofIBox/functions/public/New-BoxSession.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS +Creates a Box API session. + +.DESCRIPTION +Authenticates to the Box API using client credentials and stores +the access token for use with other functions in the module. + +.PARAMETER Credential +Credential containing the Box Client ID and Client Secret. + +.PARAMETER EnterpriseId +Box enterprise ID. + +.EXAMPLE +$Credential = Get-Credential +New-BoxSession -Credential $Credential -EnterpriseId "123456" +#> + +function New-BoxSession { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [PSCredential]$Credential, + + [Parameter(Mandatory)] + [string]$EnterpriseId + ) + + $Body = @{ + grant_type = "client_credentials" + client_id = $Credential.UserName + client_secret = $Credential.GetNetworkCredential().Password + box_subject_type = "enterprise" + box_subject_id = $EnterpriseId + } + + $TokenRequest = @{ + Method = "POST" + Uri = "https://api.box.com/oauth2/token" + Body = $Body + ContentType = "application/x-www-form-urlencoded" + } + + Write-Verbose "Authenticating to Box API" + + $Response = Invoke-RestMethod @TokenRequest + + if ($Response.access_token) { + + $Script:BoxSession = [PSCustomObject]@{ + AccessToken = $Response.access_token + Created = Get-Date + ExpiresIn = $Response.expires_in + } + + Write-Verbose "Box session created." + } + else { + throw "$_" + } +} \ No newline at end of file diff --git a/src/UofIBox/functions/public/Receive-BoxFile.ps1 b/src/UofIBox/functions/public/Receive-BoxFile.ps1 new file mode 100644 index 0000000..e2ec050 --- /dev/null +++ b/src/UofIBox/functions/public/Receive-BoxFile.ps1 @@ -0,0 +1,36 @@ +<# +.SYNOPSIS +Downloads a file from Box. + +.DESCRIPTION +Downloads the specified Box file to a local path. + +.PARAMETER FileId +The ID of the Box file. + +.PARAMETER OutputPath +Local file path where the file will be saved. + +.EXAMPLE +Receive-BoxFile -FileId "123456" -OutputPath "C:\Downloads\file.pdf" +#> + +function Receive-BoxFile { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$FileId, + + [Parameter(Mandatory)] + [string]$OutputPath + ) + + $DownloadCall = @{ + Method = "GET" + RelativeURI = "files/$FileId/content" + OutFile = $OutputPath + } + + Invoke-BoxRestCall @DownloadCall +} \ No newline at end of file diff --git a/src/UofIBox/functions/public/Receive-BoxFolder.ps1 b/src/UofIBox/functions/public/Receive-BoxFolder.ps1 new file mode 100644 index 0000000..e7e3204 --- /dev/null +++ b/src/UofIBox/functions/public/Receive-BoxFolder.ps1 @@ -0,0 +1,75 @@ +<# +.SYNOPSIS +Downloads an entire Box folder. + +.DESCRIPTION +Recursively downloads all files and subfolders from a Box folder. + +.PARAMETER FolderId +The ID of the Box folder to download. + +.PARAMETER OutputDirectory +Local directory where files will be downloaded. + +.EXAMPLE +Receive-BoxFolder -FolderId "123456" -OutputDirectory "C:\BoxDownloads" +#> + +function Receive-BoxFolder { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$FolderId, + + [Parameter(Mandatory)] + [string]$OutputDirectory + ) + + # Get folder metadata + $FolderCall = @{ + RelativeURI = "folders/$FolderId" + Method = "GET" + } + + $Folder = Invoke-BoxRestCall @FolderCall + + $LocalFolder = Join-Path $OutputDirectory $Folder.name + + if (-not (Test-Path $LocalFolder)) { + New-Item -ItemType Directory -Path $LocalFolder | Out-Null + } + + # Get folder items + $ItemsCall = @{ + RelativeURI = "folders/$FolderId/items" + Method = "GET" + } + + $Items = Invoke-BoxRestCall @ItemsCall + + foreach ($Item in $Items.entries) { + + if ($Item.type -eq "file") { + + $DownloadPath = Join-Path $LocalFolder $Item.name + + $DownloadCall = @{ + FileId = $Item.id + OutputPath = $DownloadPath + } + + Receive-BoxFile @DownloadCall + } + + if ($Item.type -eq "folder") { + + $SubFolderCall = @{ + FolderId = $Item.id + OutputDirectory = $LocalFolder + } + + Receive-BoxFolder @SubFolderCall + } + } +} \ No newline at end of file diff --git a/src/UofIBox/functions/public/Remove-BoxFile.ps1 b/src/UofIBox/functions/public/Remove-BoxFile.ps1 new file mode 100644 index 0000000..798a6c4 --- /dev/null +++ b/src/UofIBox/functions/public/Remove-BoxFile.ps1 @@ -0,0 +1,29 @@ +<# +.SYNOPSIS +Deletes a Box file. + +.DESCRIPTION +Removes a file from Box permanently. + +.PARAMETER FileId +The ID of the file to delete. + +.EXAMPLE +Remove-BoxFile -FileId "123456789" +#> + +function Remove-BoxFile { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$FileId + ) + + $Call = @{ + RelativeURI = "files/$FileId" + Method = "DELETE" + } + + Invoke-BoxRestCall @Call +} \ No newline at end of file diff --git a/src/UofIBox/functions/public/Remove-BoxFolder.ps1 b/src/UofIBox/functions/public/Remove-BoxFolder.ps1 new file mode 100644 index 0000000..6beaa5f --- /dev/null +++ b/src/UofIBox/functions/public/Remove-BoxFolder.ps1 @@ -0,0 +1,26 @@ +<# +.SYNOPSIS +Deletes a Box folder. + +.DESCRIPTION +Deletes the specified folder and its contents from Box. + +.PARAMETER FolderId +ID of the folder to delete. + +.EXAMPLE +Remove-BoxFolder -FolderId "123456789" +#> + +function Remove-BoxFolder { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$FolderId + ) + + Invoke-BoxRestCall ` + -RelativeURI "folders/$FolderId?recursive=true" ` + -Method DELETE +} \ No newline at end of file diff --git a/src/UofIBox/functions/public/Send-BoxFile.ps1 b/src/UofIBox/functions/public/Send-BoxFile.ps1 new file mode 100644 index 0000000..c41e265 --- /dev/null +++ b/src/UofIBox/functions/public/Send-BoxFile.ps1 @@ -0,0 +1,54 @@ +<# +.SYNOPSIS +Uploads a file to Box. + +.DESCRIPTION +Uploads a local file to a specified Box folder. + +.PARAMETER FilePath +Local path of the file to upload. + +.PARAMETER ParentFolderId +Box folder ID where the file will be uploaded. + +.EXAMPLE +Send-BoxFile -FilePath "C:\Temp\report.pdf" -ParentFolderId "123456" +#> + +function Send-BoxFile { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$FilePath, + + [Parameter(Mandatory)] + [string]$ParentFolderId + ) + + if ($null -eq $Script:BoxSession) { + throw "No Box session established. Run New-BoxSession first." + } + + $Attributes = @{ + name = [System.IO.Path]::GetFileName($FilePath) + parent = @{ + id = $ParentFolderId + } + } | ConvertTo-Json -Compress + + $Headers = @{ + Authorization = "Bearer $($Script:BoxSession.AccessToken)" + } + + $Form = @{ + attributes = $Attributes + file = Get-Item $FilePath + } + + Invoke-RestMethod ` + -Method POST ` + -Uri "$($Script:Settings.UploadBaseURI)files/content" ` + -Headers $Headers ` + -Form $Form +} \ No newline at end of file diff --git a/test/UofIBox.Tests.ps1 b/test/UofIBox.Tests.ps1 new file mode 100644 index 0000000..f821848 --- /dev/null +++ b/test/UofIBox.Tests.ps1 @@ -0,0 +1,9 @@ +[String]$ModuleRoot = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'src\UofIBox' +Import-Module -Name $ModuleRoot + +Describe 'Module Manifest Tests' { + It 'Passes Test-ModuleManifest' { + $ManifestPath = Join-Path -Path "$(Split-Path -Path $PSScriptRoot -Parent)" -ChildPath 'src\UofIBox\UofIBox.psd1' + Test-ModuleManifest -Path $ManifestPath | Should -Not -BeNullOrEmpty + } +} From dcb0115b1d05922a0ffdf0418e05d673d8ab8a1d Mon Sep 17 00:00:00 2001 From: rdonovan92 Date: Tue, 17 Mar 2026 15:56:12 -0500 Subject: [PATCH 2/9] Fixed scriptanalyzer changes --- ...{Get-BoxFileMetadata.ps1 => Get-BoxFileData.ps1} | 2 +- ...-BoxFolderMetadata.ps1 => Get-BoxFolderData.ps1} | 2 +- .../functions/public/New-BoxFolderCollaboration.ps1 | 7 ++++++- src/UofIBox/functions/public/New-BoxSession.ps1 | 3 +++ src/UofIBox/functions/public/Remove-BoxFile.ps1 | 13 +++++++------ src/UofIBox/functions/public/Remove-BoxFolder.ps1 | 4 +++- 6 files changed, 21 insertions(+), 10 deletions(-) rename src/UofIBox/functions/public/{Get-BoxFileMetadata.ps1 => Get-BoxFileData.ps1} (93%) rename src/UofIBox/functions/public/{Get-BoxFolderMetadata.ps1 => Get-BoxFolderData.ps1} (92%) diff --git a/src/UofIBox/functions/public/Get-BoxFileMetadata.ps1 b/src/UofIBox/functions/public/Get-BoxFileData.ps1 similarity index 93% rename from src/UofIBox/functions/public/Get-BoxFileMetadata.ps1 rename to src/UofIBox/functions/public/Get-BoxFileData.ps1 index 2f97a58..a3b0382 100644 --- a/src/UofIBox/functions/public/Get-BoxFileMetadata.ps1 +++ b/src/UofIBox/functions/public/Get-BoxFileData.ps1 @@ -12,7 +12,7 @@ The ID of the Box file. Get-BoxFile -FileId "123456789" #> -function Get-BoxFileMetadata { +function Get-BoxFileData { [CmdletBinding()] param( diff --git a/src/UofIBox/functions/public/Get-BoxFolderMetadata.ps1 b/src/UofIBox/functions/public/Get-BoxFolderData.ps1 similarity index 92% rename from src/UofIBox/functions/public/Get-BoxFolderMetadata.ps1 rename to src/UofIBox/functions/public/Get-BoxFolderData.ps1 index 2400b84..ed26442 100644 --- a/src/UofIBox/functions/public/Get-BoxFolderMetadata.ps1 +++ b/src/UofIBox/functions/public/Get-BoxFolderData.ps1 @@ -12,7 +12,7 @@ The ID of the Box folder. Get-BoxFolder -FolderId "123456789" #> -function Get-BoxFolderMetadata { +function Get-BoxFolderData { [CmdletBinding()] param( diff --git a/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 b/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 index a48cc64..63ceebc 100644 --- a/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 +++ b/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 @@ -22,7 +22,7 @@ New-BoxFolderCollaboration -FolderName "Finance" -Collaborators $Users #> function New-BoxFolderCollaboration { - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$FolderName, @@ -35,6 +35,8 @@ function New-BoxFolderCollaboration { [switch]$ReturnFolderLink ) + if ($PSCmdlet.ShouldProcess("Box", "Create folder '$FolderName'")) { + $FolderBody = @{ name = $FolderName parent = @{ @@ -50,9 +52,11 @@ function New-BoxFolderCollaboration { $FolderResponse = Invoke-BoxRestCall @CreateFolder $FolderId = $FolderResponse.id + } foreach ($User in $Collaborators) { + if ($PSCmdlet.ShouldProcess("Box", "Add collaborator $Target")) { $CollabBody = @{ item = @{ type = "folder" @@ -72,6 +76,7 @@ function New-BoxFolderCollaboration { } Invoke-BoxRestCall @CollabCall + } } if ($ReturnFolderLink) { diff --git a/src/UofIBox/functions/public/New-BoxSession.ps1 b/src/UofIBox/functions/public/New-BoxSession.ps1 index 0058b2d..a3703ff 100644 --- a/src/UofIBox/functions/public/New-BoxSession.ps1 +++ b/src/UofIBox/functions/public/New-BoxSession.ps1 @@ -19,6 +19,9 @@ New-BoxSession -Credential $Credential -EnterpriseId "123456" function New-BoxSession { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + "PSUseShouldProcessForStateChangingFunctions", + "")] [CmdletBinding()] param( [Parameter(Mandatory)] diff --git a/src/UofIBox/functions/public/Remove-BoxFile.ps1 b/src/UofIBox/functions/public/Remove-BoxFile.ps1 index 798a6c4..234d2e0 100644 --- a/src/UofIBox/functions/public/Remove-BoxFile.ps1 +++ b/src/UofIBox/functions/public/Remove-BoxFile.ps1 @@ -14,16 +14,17 @@ Remove-BoxFile -FileId "123456789" function Remove-BoxFile { - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$FileId ) - $Call = @{ - RelativeURI = "files/$FileId" - Method = "DELETE" - } - + if ($PSCmdlet.ShouldProcess("File $FileId", "Delete")) { + $Call = @{ + RelativeURI = "files/$FileId" + Method = "DELETE" + } Invoke-BoxRestCall @Call + } } \ No newline at end of file diff --git a/src/UofIBox/functions/public/Remove-BoxFolder.ps1 b/src/UofIBox/functions/public/Remove-BoxFolder.ps1 index 6beaa5f..12e8d2b 100644 --- a/src/UofIBox/functions/public/Remove-BoxFolder.ps1 +++ b/src/UofIBox/functions/public/Remove-BoxFolder.ps1 @@ -14,13 +14,15 @@ Remove-BoxFolder -FolderId "123456789" function Remove-BoxFolder { - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$FolderId ) + if ($PSCmdlet.ShouldProcess("Folder $FolderId", "Delete")) { Invoke-BoxRestCall ` -RelativeURI "folders/$FolderId?recursive=true" ` -Method DELETE + } } \ No newline at end of file From e72b32e74697617da77a20bd1148aeb6e70814bf Mon Sep 17 00:00:00 2001 From: rdonovan92 Date: Tue, 17 Mar 2026 16:34:34 -0500 Subject: [PATCH 3/9] script tweaks --- .../public/New-BoxFolderCollaboration.ps1 | 84 +++++++++++-------- .../functions/public/Remove-BoxFolder.ps1 | 23 ++++- 2 files changed, 67 insertions(+), 40 deletions(-) diff --git a/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 b/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 index 63ceebc..21632a2 100644 --- a/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 +++ b/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 @@ -11,8 +11,11 @@ Name of the folder to create. .PARAMETER ParentFolderId Parent folder ID. -.PARAMETER Collaborators -Array of collaborator objects containing login and role. +.PARAMETER Login +User email(s) to grant access. + +.PARAMETER Role +Array of roles corresponding to each login. .PARAMETER ReturnFolderLink Returns a link to the folder. @@ -30,57 +33,66 @@ function New-BoxFolderCollaboration { [string]$ParentFolderId = "0", [Parameter(Mandatory)] - [array]$Collaborators, + [string[]]$Login, + + [Parameter(Mandatory)] + [string[]]$Role, [switch]$ReturnFolderLink ) - if ($PSCmdlet.ShouldProcess("Box", "Create folder '$FolderName'")) { - - $FolderBody = @{ - name = $FolderName - parent = @{ - id = $ParentFolderId - } - } - - $CreateFolder = @{ - RelativeURI = "folders" - Method = "POST" - Body = $FolderBody - } - - $FolderResponse = Invoke-BoxRestCall @CreateFolder - $FolderId = $FolderResponse.id + if ($Login.Count -ne $Role.Count) { + throw "Login and Role counts must match." } - foreach ($User in $Collaborators) { + if ($PSCmdlet.ShouldProcess("Box", "Create folder '$FolderName'")) { - if ($PSCmdlet.ShouldProcess("Box", "Add collaborator $Target")) { - $CollabBody = @{ - item = @{ - type = "folder" - id = $FolderId - } - accessible_by = @{ - type = "user" - login = $User.login + $FolderBody = @{ + name = $FolderName + parent = @{ + id = $ParentFolderId } - role = $User.role } - $CollabCall = @{ - RelativeURI = "collaborations" + $CreateFolder = @{ + RelativeURI = "folders" Method = "POST" - Body = $CollabBody + Body = $FolderBody } - Invoke-BoxRestCall @CollabCall + $FolderResponse = Invoke-BoxRestCall @CreateFolder + $FolderId = $FolderResponse.id + + for ($i = 0; $i -lt $Login.Count; $i++) { + + $Target = "$($Login[$i]) as $($Role[$i])" + + if ($PSCmdlet.ShouldProcess("Box Folder $FolderId", "Add collaborator $Target")) { + + $CollabBody = @{ + item = @{ + type = "folder" + id = $FolderId + } + accessible_by = @{ + type = "user" + login = $Login[$i] + } + role = $Role[$i] + } + + $CollabCall = @{ + RelativeURI = "collaborations" + Method = "POST" + Body = $CollabBody + } + + Invoke-BoxRestCall @CollabCall + } } } if ($ReturnFolderLink) { - return [PSCustomObject]@{ FolderName = $FolderName FolderId = $FolderId diff --git a/src/UofIBox/functions/public/Remove-BoxFolder.ps1 b/src/UofIBox/functions/public/Remove-BoxFolder.ps1 index 12e8d2b..182c531 100644 --- a/src/UofIBox/functions/public/Remove-BoxFolder.ps1 +++ b/src/UofIBox/functions/public/Remove-BoxFolder.ps1 @@ -8,6 +8,9 @@ Deletes the specified folder and its contents from Box. .PARAMETER FolderId ID of the folder to delete. +.PARAMETER Recursive +If set, deletes the folder and all of its contents. If not set, the folder must be empty to be deleted. + .EXAMPLE Remove-BoxFolder -FolderId "123456789" #> @@ -17,12 +20,24 @@ function Remove-BoxFolder { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] - [string]$FolderId + [string]$FolderId, + + [switch]$Recursive ) + $Uri = "folders/$FolderId" + + if ($Recursive) { + $Uri += "?recursive=true" + } + if ($PSCmdlet.ShouldProcess("Folder $FolderId", "Delete")) { - Invoke-BoxRestCall ` - -RelativeURI "folders/$FolderId?recursive=true" ` - -Method DELETE + + $Call = @{ + RelativeURI = $Uri + Method = "DELETE" + } + + Invoke-BoxRestCall @Call } } \ No newline at end of file From 2f324987ddfc2ae8d075d78330689058285b9f94 Mon Sep 17 00:00:00 2001 From: rdonovan92 Date: Wed, 18 Mar 2026 07:05:26 -0500 Subject: [PATCH 4/9] added blank space after each function --- src/UofIBox/functions/public/Get-BoxFileData.ps1 | 2 +- src/UofIBox/functions/public/Get-BoxFolderData.ps1 | 2 +- src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 | 2 +- src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 | 2 +- src/UofIBox/functions/public/New-BoxSession.ps1 | 2 +- src/UofIBox/functions/public/Receive-BoxFile.ps1 | 2 +- src/UofIBox/functions/public/Receive-BoxFolder.ps1 | 2 +- src/UofIBox/functions/public/Remove-BoxFile.ps1 | 2 +- src/UofIBox/functions/public/Remove-BoxFolder.ps1 | 2 +- src/UofIBox/functions/public/Send-BoxFile.ps1 | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/UofIBox/functions/public/Get-BoxFileData.ps1 b/src/UofIBox/functions/public/Get-BoxFileData.ps1 index a3b0382..a7b34cb 100644 --- a/src/UofIBox/functions/public/Get-BoxFileData.ps1 +++ b/src/UofIBox/functions/public/Get-BoxFileData.ps1 @@ -26,4 +26,4 @@ function Get-BoxFileData { } Invoke-BoxRestCall @Call -} \ No newline at end of file +} diff --git a/src/UofIBox/functions/public/Get-BoxFolderData.ps1 b/src/UofIBox/functions/public/Get-BoxFolderData.ps1 index ed26442..2b7fce9 100644 --- a/src/UofIBox/functions/public/Get-BoxFolderData.ps1 +++ b/src/UofIBox/functions/public/Get-BoxFolderData.ps1 @@ -26,4 +26,4 @@ function Get-BoxFolderData { } Invoke-BoxRestCall @Call -} \ No newline at end of file +} diff --git a/src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 b/src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 index 9cca69f..47cadec 100644 --- a/src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 +++ b/src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 @@ -94,4 +94,4 @@ function Invoke-BoxRestCall { return $Result } } -} \ No newline at end of file +} diff --git a/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 b/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 index 21632a2..205326c 100644 --- a/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 +++ b/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 @@ -101,4 +101,4 @@ function New-BoxFolderCollaboration { } return $FolderResponse -} \ No newline at end of file +} diff --git a/src/UofIBox/functions/public/New-BoxSession.ps1 b/src/UofIBox/functions/public/New-BoxSession.ps1 index a3703ff..b68a748 100644 --- a/src/UofIBox/functions/public/New-BoxSession.ps1 +++ b/src/UofIBox/functions/public/New-BoxSession.ps1 @@ -63,4 +63,4 @@ function New-BoxSession { else { throw "$_" } -} \ No newline at end of file +} diff --git a/src/UofIBox/functions/public/Receive-BoxFile.ps1 b/src/UofIBox/functions/public/Receive-BoxFile.ps1 index e2ec050..628e828 100644 --- a/src/UofIBox/functions/public/Receive-BoxFile.ps1 +++ b/src/UofIBox/functions/public/Receive-BoxFile.ps1 @@ -33,4 +33,4 @@ function Receive-BoxFile { } Invoke-BoxRestCall @DownloadCall -} \ No newline at end of file +} diff --git a/src/UofIBox/functions/public/Receive-BoxFolder.ps1 b/src/UofIBox/functions/public/Receive-BoxFolder.ps1 index e7e3204..abf889e 100644 --- a/src/UofIBox/functions/public/Receive-BoxFolder.ps1 +++ b/src/UofIBox/functions/public/Receive-BoxFolder.ps1 @@ -72,4 +72,4 @@ function Receive-BoxFolder { Receive-BoxFolder @SubFolderCall } } -} \ No newline at end of file +} diff --git a/src/UofIBox/functions/public/Remove-BoxFile.ps1 b/src/UofIBox/functions/public/Remove-BoxFile.ps1 index 234d2e0..5ecad1c 100644 --- a/src/UofIBox/functions/public/Remove-BoxFile.ps1 +++ b/src/UofIBox/functions/public/Remove-BoxFile.ps1 @@ -27,4 +27,4 @@ function Remove-BoxFile { } Invoke-BoxRestCall @Call } -} \ No newline at end of file +} diff --git a/src/UofIBox/functions/public/Remove-BoxFolder.ps1 b/src/UofIBox/functions/public/Remove-BoxFolder.ps1 index 182c531..0b55d1c 100644 --- a/src/UofIBox/functions/public/Remove-BoxFolder.ps1 +++ b/src/UofIBox/functions/public/Remove-BoxFolder.ps1 @@ -40,4 +40,4 @@ function Remove-BoxFolder { Invoke-BoxRestCall @Call } -} \ No newline at end of file +} diff --git a/src/UofIBox/functions/public/Send-BoxFile.ps1 b/src/UofIBox/functions/public/Send-BoxFile.ps1 index c41e265..1342244 100644 --- a/src/UofIBox/functions/public/Send-BoxFile.ps1 +++ b/src/UofIBox/functions/public/Send-BoxFile.ps1 @@ -51,4 +51,4 @@ function Send-BoxFile { -Uri "$($Script:Settings.UploadBaseURI)files/content" ` -Headers $Headers ` -Form $Form -} \ No newline at end of file +} From 98639cac3abe69ec67b14ec1a5016fb68ad5b0d2 Mon Sep 17 00:00:00 2001 From: rdonovan92 Date: Thu, 19 Mar 2026 15:27:31 -0500 Subject: [PATCH 5/9] Comment changes --- .github/workflows/pester.yml | 4 ++-- .github/workflows/scriptanalyzer.yml | 2 +- CHANGELOG.md | 4 ++-- LICENSE | 2 +- README.md | 13 +++--------- src/UofIBox/UofIBox.psd1 | 20 +++++++++++++++---- src/UofIBox/UofIBox.psm1 | 5 ++++- .../functions/public/Get-BoxFileData.ps1 | 2 +- .../functions/public/Get-BoxFolderData.ps1 | 2 +- 9 files changed, 31 insertions(+), 23 deletions(-) diff --git a/.github/workflows/pester.yml b/.github/workflows/pester.yml index 16dfcba..7608511 100644 --- a/.github/workflows/pester.yml +++ b/.github/workflows/pester.yml @@ -8,10 +8,10 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04, macos-latest, windows-latest] + os: [macos-latest, windows-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - name: Install dependencies run: Install-Module -Name 'Pester' -Force -SkipPublisherCheck shell: pwsh diff --git a/.github/workflows/scriptanalyzer.yml b/.github/workflows/scriptanalyzer.yml index da85e6f..d1597cd 100644 --- a/.github/workflows/scriptanalyzer.yml +++ b/.github/workflows/scriptanalyzer.yml @@ -11,7 +11,7 @@ jobs: os: [windows-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - name: Install dependencies run: Install-Module -Name 'PSScriptAnalyzer' -Force -SkipPublisherCheck shell: pwsh diff --git a/CHANGELOG.md b/CHANGELOG.md index eb6da1b..4bc22af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed -## [1.0.0] - 2026-03-17 +## [0.0.1] - 2026-03-17 ### Added - - Initial Release, first batch of functions added to the module. + - Initial Push, first batch of functions added to the module. diff --git a/LICENSE b/LICENSE index 45c0855..d82e321 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2020 University of Illinois. All rights reserved. +Copyright (c) 2026 University of Illinois. All rights reserved. Developed by: Cybersecurity Engineering diff --git a/README.md b/README.md index d4f1c06..712552b 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,11 @@ It simplifies Box API usage by handling authentication, REST calls, and common o # How do I install it? -### Option 1: Clone the Repository -git clone -cd -Import-Module .\BoxAutomation.psm1 +The latest stable release is always available via the PSGallery. -### Option 2: Manual Import +This will install on the local machine: -Download the module files and import directly: - -Import-Module "C:\Path\To\BoxAutomation.psm1" +Install-Module -Name 'UofIBox' ### Prerequisites @@ -110,8 +105,6 @@ If contributing: Follow existing function structure and naming conventions -Use splats instead of backticks - Ensure all functions use Invoke-BoxRestCall Include comment-based help for all functions diff --git a/src/UofIBox/UofIBox.psd1 b/src/UofIBox/UofIBox.psd1 index 00efefb..bc15b85 100644 --- a/src/UofIBox/UofIBox.psd1 +++ b/src/UofIBox/UofIBox.psd1 @@ -67,16 +67,28 @@ PowerShellVersion = '7.0' # NestedModules = @() # 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. -# FunctionsToExport = '*' +FunctionsToExport = @( + 'Get-BoxFolderData,' + 'Get-BoxFileData', + 'Invoke-BoxRestCall', + 'New-BoxFolderCollaboration' + 'New-BoxSession', + 'Receive-BoxFile', + 'Receive-BoxFolder', + 'Remove-BoxFolder', + 'Remove-BoxFile' + 'Send-BoxFile' + ) + # 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. -# CmdletsToExport = '*' +CmdletsToExport = '*' # Variables to export from this module -# VariablesToExport = '*' +VariablesToExport = '*' # 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. -# AliasesToExport = '*' +AliasesToExport = '*' # DSC resources to export from this module # DscResourcesToExport = @() diff --git a/src/UofIBox/UofIBox.psm1 b/src/UofIBox/UofIBox.psm1 index 50d2a76..29b9165 100644 --- a/src/UofIBox/UofIBox.psm1 +++ b/src/UofIBox/UofIBox.psm1 @@ -1,6 +1,9 @@ +$Script:BoxSession = $NULL +[int]$Script:APICallCount = 0 + [String]$FunctionPath = Join-Path -Path $PSScriptRoot -ChildPath 'Functions' #All function files are executed while only public functions are exported to the shell. Get-ChildItem -Path $FunctionPath -Filter "*.ps1" -Recurse | ForEach-Object -Process { Write-Verbose -Message "Importing $($_.BaseName)" . $_.FullName | Out-Null -} \ No newline at end of file +} diff --git a/src/UofIBox/functions/public/Get-BoxFileData.ps1 b/src/UofIBox/functions/public/Get-BoxFileData.ps1 index a7b34cb..c09d984 100644 --- a/src/UofIBox/functions/public/Get-BoxFileData.ps1 +++ b/src/UofIBox/functions/public/Get-BoxFileData.ps1 @@ -9,7 +9,7 @@ Returns file information including size, name, and owner. The ID of the Box file. .EXAMPLE -Get-BoxFile -FileId "123456789" +Get-BoxFileData -FileId "123456789" #> function Get-BoxFileData { diff --git a/src/UofIBox/functions/public/Get-BoxFolderData.ps1 b/src/UofIBox/functions/public/Get-BoxFolderData.ps1 index 2b7fce9..4135721 100644 --- a/src/UofIBox/functions/public/Get-BoxFolderData.ps1 +++ b/src/UofIBox/functions/public/Get-BoxFolderData.ps1 @@ -9,7 +9,7 @@ Gets metadata information for a specified Box folder. The ID of the Box folder. .EXAMPLE -Get-BoxFolder -FolderId "123456789" +Get-BoxFolderData -FolderId "123456789" #> function Get-BoxFolderData { From 9948382491eaf7b09b9e23e5ceb1917ff4e090f2 Mon Sep 17 00:00:00 2001 From: rdonovan92 Date: Thu, 19 Mar 2026 15:41:06 -0500 Subject: [PATCH 6/9] fixed wildcard usage in psd1 --- src/UofIBox/UofIBox.psd1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UofIBox/UofIBox.psd1 b/src/UofIBox/UofIBox.psd1 index bc15b85..ce4f63e 100644 --- a/src/UofIBox/UofIBox.psd1 +++ b/src/UofIBox/UofIBox.psd1 @@ -82,13 +82,13 @@ FunctionsToExport = @( # 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. -CmdletsToExport = '*' +CmdletsToExport = @() # Variables to export from this module -VariablesToExport = '*' +VariablesToExport = @() # 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. -AliasesToExport = '*' +AliasesToExport = @() # DSC resources to export from this module # DscResourcesToExport = @() From 78e2ca0c34c6d5374158445d56ad62d612c8eb27 Mon Sep 17 00:00:00 2001 From: rdonovan92 Date: Fri, 20 Mar 2026 14:42:15 -0500 Subject: [PATCH 7/9] updated example in New-BoxFolderCollaboration --- src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 b/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 index 205326c..fd9c7b9 100644 --- a/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 +++ b/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 @@ -21,7 +21,7 @@ Array of roles corresponding to each login. Returns a link to the folder. .EXAMPLE -New-BoxFolderCollaboration -FolderName "Finance" -Collaborators $Users +New-BoxFolderCollaboration -FolderName "Finance" -Login "user1@company.com" -Role "Editor" -ReturnFolderLink #> function New-BoxFolderCollaboration { From 8f496c9ed6d1278dbf96d310c23586a4c33c155e Mon Sep 17 00:00:00 2001 From: rdonovan92 Date: Fri, 20 Mar 2026 14:45:37 -0500 Subject: [PATCH 8/9] one more parameter change example for New-BoxFolderCollaboration function. --- src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 b/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 index fd9c7b9..ac495ae 100644 --- a/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 +++ b/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 @@ -21,7 +21,7 @@ Array of roles corresponding to each login. Returns a link to the folder. .EXAMPLE -New-BoxFolderCollaboration -FolderName "Finance" -Login "user1@company.com" -Role "Editor" -ReturnFolderLink +New-BoxFolderCollaboration -FolderName "Finance" -ParentFolderId "0" -Login "user1@company.com" -Role "Editor" -ReturnFolderLink #> function New-BoxFolderCollaboration { From c91450c86d3d73efb83133d1ea7ae89f7c3d361b Mon Sep 17 00:00:00 2001 From: rdonovan92 Date: Tue, 24 Mar 2026 10:43:04 -0500 Subject: [PATCH 9/9] Removed New-Boxfoldercollaboration and split into two functions --- .../functions/public/New-BoxCollaboration.ps1 | 72 ++++++++++++ .../functions/public/New-BoxFolder.ps1 | 45 ++++++++ .../public/New-BoxFolderCollaboration.ps1 | 104 ------------------ 3 files changed, 117 insertions(+), 104 deletions(-) create mode 100644 src/UofIBox/functions/public/New-BoxCollaboration.ps1 create mode 100644 src/UofIBox/functions/public/New-BoxFolder.ps1 delete mode 100644 src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 diff --git a/src/UofIBox/functions/public/New-BoxCollaboration.ps1 b/src/UofIBox/functions/public/New-BoxCollaboration.ps1 new file mode 100644 index 0000000..fa8e322 --- /dev/null +++ b/src/UofIBox/functions/public/New-BoxCollaboration.ps1 @@ -0,0 +1,72 @@ +<# +.SYNOPSIS +Adds collaborators to a Box folder. + +.DESCRIPTION +Assigns one or more users to a folder with specified roles. + +.PARAMETER FolderId +ID of the folder. + +.PARAMETER Login +User email(s). + +.PARAMETER Role +Role(s) assigned to users. + +.EXAMPLE +New-BoxCollaboration -FolderId 123456 -Login user@company.com -Role editor + +.EXAMPLE +New-BoxCollaboration ` + -FolderId 123456 ` + -Login user1@company.com,user2@company.com ` + -Role editor,viewer +#> + +function New-BoxCollaboration { + + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string]$FolderId, + + [Parameter(Mandatory)] + [string[]]$Login, + + [Parameter(Mandatory)] + [string[]]$Role + ) + + if ($Login.Count -ne $Role.Count) { + throw "Login and Role counts must match." + } + + for ($i = 0; $i -lt $Login.Count; $i++) { + + $Target = "$($Login[$i]) as $($Role[$i]) on folder $FolderId" + + if ($PSCmdlet.ShouldProcess("Box", "Add collaborator $Target")) { + + $Body = @{ + item = @{ + type = "folder" + id = $FolderId + } + accessible_by = @{ + type = "user" + login = $Login[$i] + } + role = $Role[$i] + } + + $Call = @{ + RelativeURI = "collaborations" + Method = "POST" + Body = $Body + } + + Invoke-BoxRestCall @Call + } + } +} \ No newline at end of file diff --git a/src/UofIBox/functions/public/New-BoxFolder.ps1 b/src/UofIBox/functions/public/New-BoxFolder.ps1 new file mode 100644 index 0000000..10182e2 --- /dev/null +++ b/src/UofIBox/functions/public/New-BoxFolder.ps1 @@ -0,0 +1,45 @@ +<# +.SYNOPSIS +Creates a new folder in Box. + +.DESCRIPTION +Creates a folder in the specified parent folder. + +.PARAMETER FolderName +Name of the folder to create. + +.PARAMETER ParentFolderId +Parent folder ID. Defaults to root (0). + +.EXAMPLE +New-BoxFolder -FolderName "Finance" -ParentFolderId 0 +#> + +function New-BoxFolder { + + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string]$FolderName, + + [string]$ParentFolderId = "0" + ) + + if ($PSCmdlet.ShouldProcess("Box", "Create folder '$FolderName'")) { + + $Body = @{ + name = $FolderName + parent = @{ + id = $ParentFolderId + } + } + + $Call = @{ + RelativeURI = "folders" + Method = "POST" + Body = $Body + } + + Invoke-BoxRestCall @Call + } +} \ No newline at end of file diff --git a/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 b/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 deleted file mode 100644 index ac495ae..0000000 --- a/src/UofIBox/functions/public/New-BoxFolderCollaboration.ps1 +++ /dev/null @@ -1,104 +0,0 @@ -<# -.SYNOPSIS -Creates a Box folder and assigns collaborators. - -.DESCRIPTION -Creates a new folder and assigns users with the specified roles. - -.PARAMETER FolderName -Name of the folder to create. - -.PARAMETER ParentFolderId -Parent folder ID. - -.PARAMETER Login -User email(s) to grant access. - -.PARAMETER Role -Array of roles corresponding to each login. - -.PARAMETER ReturnFolderLink -Returns a link to the folder. - -.EXAMPLE -New-BoxFolderCollaboration -FolderName "Finance" -ParentFolderId "0" -Login "user1@company.com" -Role "Editor" -ReturnFolderLink -#> -function New-BoxFolderCollaboration { - - [CmdletBinding(SupportsShouldProcess)] - param( - [Parameter(Mandatory)] - [string]$FolderName, - - [string]$ParentFolderId = "0", - - [Parameter(Mandatory)] - [string[]]$Login, - - [Parameter(Mandatory)] - [string[]]$Role, - - [switch]$ReturnFolderLink - ) - - if ($Login.Count -ne $Role.Count) { - throw "Login and Role counts must match." - } - - if ($PSCmdlet.ShouldProcess("Box", "Create folder '$FolderName'")) { - - $FolderBody = @{ - name = $FolderName - parent = @{ - id = $ParentFolderId - } - } - - $CreateFolder = @{ - RelativeURI = "folders" - Method = "POST" - Body = $FolderBody - } - - $FolderResponse = Invoke-BoxRestCall @CreateFolder - $FolderId = $FolderResponse.id - - for ($i = 0; $i -lt $Login.Count; $i++) { - - $Target = "$($Login[$i]) as $($Role[$i])" - - if ($PSCmdlet.ShouldProcess("Box Folder $FolderId", "Add collaborator $Target")) { - - $CollabBody = @{ - item = @{ - type = "folder" - id = $FolderId - } - accessible_by = @{ - type = "user" - login = $Login[$i] - } - role = $Role[$i] - } - - $CollabCall = @{ - RelativeURI = "collaborations" - Method = "POST" - Body = $CollabBody - } - - Invoke-BoxRestCall @CollabCall - } - } - } - - if ($ReturnFolderLink) { - return [PSCustomObject]@{ - FolderName = $FolderName - FolderId = $FolderId - FolderUrl = "https://app.box.com/folder/$FolderId" - } - } - - return $FolderResponse -}