Skip to content

Commit

Permalink
Merge pull request #36 from KelvinTegelaar/master
Browse files Browse the repository at this point in the history
[pull] master from KelvinTegelaar:master
  • Loading branch information
pull[bot] authored Jun 25, 2024
2 parents 00c8ae2 + d513da2 commit df8af2c
Show file tree
Hide file tree
Showing 125 changed files with 1,786 additions and 1,045 deletions.
39 changes: 39 additions & 0 deletions .github/workflows/dev_cippacnqv.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action
# More GitHub Actions for Azure: https://github.com/Azure/actions

name: Build and deploy Powershell project to Azure Function App - cippacnqv

on:
push:
branches:
- dev
workflow_dispatch:

env:
AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root

jobs:
deploy:
runs-on: windows-latest
permissions:
id-token: write #This is required for requesting the JWT

steps:
- name: 'Checkout GitHub Action'
uses: actions/checkout@v4

- name: Login to Azure
uses: azure/login@v1
with:
client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_6085081ED1124B799258E9FF743FF4B9 }}
tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_9BDB2DDBFAFA4BC19C20A58B204BFAF3 }}
subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_02B5224812794971B05EDD557AF2B867 }}

- name: 'Run Azure Functions Action'
uses: Azure/functions-action@v1
id: fa
with:
app-name: 'cippacnqv'
slot-name: 'Production'
package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}

10 changes: 4 additions & 6 deletions BestPracticeAnalyser_OrchestrationStarter/run.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ if ($Request.Query.TenantFilter) {
$TenantList = Get-Tenants
$Name = 'Best Practice Analyser (All Tenants)'
}
$CippRoot = (Get-Item $PSScriptRoot).Parent.FullName
$TemplatesLoc = Get-ChildItem "$CippRoot\Config\*.BPATemplate.json"
$Templates = $TemplatesLoc | ForEach-Object {
$Template = $(Get-Content $_) | ConvertFrom-Json
$Template.Name
}

$BPATemplateTable = Get-CippTable -tablename 'templates'
$Filter = "PartitionKey eq 'BPATemplate'"
$Templates = ((Get-CIPPAzDataTableEntity @BPATemplateTable -Filter $Filter).JSON | ConvertFrom-Json).Name

$BPAReports = foreach ($Tenant in $TenantList) {
foreach ($Template in $Templates) {
Expand Down
10 changes: 4 additions & 6 deletions BestPracticeAnalyser_OrchestrationStarterTimer/run.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ if ($env:DEV_SKIP_BPA_TIMER) {

$TenantList = Get-Tenants

$CippRoot = (Get-Item $PSScriptRoot).Parent.FullName
$TemplatesLoc = Get-ChildItem "$CippRoot\Config\*.BPATemplate.json"
$Templates = $TemplatesLoc | ForEach-Object {
$Template = $(Get-Content $_) | ConvertFrom-Json
$Template.Name
}
$BPATemplateTable = Get-CippTable -tablename 'templates'
$Filter = "PartitionKey eq 'BPATemplate'"
$Templates = ((Get-CIPPAzDataTableEntity @BPATemplateTable -Filter $Filter).JSON | ConvertFrom-Json).Name


$BPAReports = foreach ($Tenant in $TenantList) {
foreach ($Template in $Templates) {
Expand Down
29 changes: 2 additions & 27 deletions Cache_SAMSetup/SAMManifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@
"resourceAppId": "00000003-0000-0000-c000-000000000000",
"resourceAccess": [
{ "id": "aa07f155-3612-49b8-a147-6c590df35536", "type": "Scope" },
{ "id": "0f4595f7-64b1-4e13-81bc-11a249df07a9", "type": "Scope" },
{ "id": "73e75199-7c3e-41bb-9357-167164dbb415", "type": "Scope" },
{ "id": "7ab1d787-bae7-4d5d-8db6-37ea32df9186", "type": "Scope" },
{ "id": "d01b97e9-cbc0-49fe-810a-750afd5527a3", "type": "Scope" },
{ "id": "46ca0847-7e6b-426e-9775-ea810a948356", "type": "Scope" },
{ "id": "dc38509c-b87d-4da0-bd92-6bec988bac4a", "type": "Scope" },
Expand All @@ -36,7 +34,6 @@
{ "id": "0c5e8a55-87a6-4556-93ab-adc52c4d862d", "type": "Scope" },
{ "id": "44642bfe-8385-4adc-8fc6-fe3cb2c375c3", "type": "Scope" },
{ "id": "662ed50a-ac44-4eef-ad86-62eed9be2a29", "type": "Scope" },
{ "id": "8696daa5-bce5-4b2e-83f9-51b6defc4e1e", "type": "Scope" },
{ "id": "6aedf524-7e1c-45a7-bd76-ded8cab8d0fc", "type": "Scope" },
{ "id": "bac3b9c2-b516-4ef4-bd3b-c2ef73d8d804", "type": "Scope" },
{ "id": "11d4cd79-5ba5-460f-803f-e22c8ab85ccd", "type": "Scope" },
Expand All @@ -55,15 +52,13 @@
{ "id": "1d89d70c-dcac-4248-b214-903c457af83a", "type": "Scope" },
{ "id": "2b61aa8a-6d36-4b2f-ac7b-f29867937c53", "type": "Scope" },
{ "id": "ebf0f66e-9fb1-49e4-a278-222f76911cf4", "type": "Scope" },
{ "id": "c79f8feb-a9db-4090-85f9-90d820caa0eb", "type": "Scope" },
{ "id": "bdfbf15f-ee85-4955-8675-146e8e5296b5", "type": "Scope" },
{ "id": "f81125ac-d3b7-4573-a3b2-7099cc39df9e", "type": "Scope" },
{ "id": "cac97e40-6730-457d-ad8d-4852fddab7ad", "type": "Scope" },
{ "id": "b7887744-6746-4312-813d-72daeaee7e2d", "type": "Scope" },
{ "id": "48971fc1-70d7-4245-af77-0beb29b53ee2", "type": "Scope" },
{ "id": "aec28ec7-4d02-4e8c-b864-50163aea77eb", "type": "Scope" },
{ "id": "a9ff19c2-f369-4a95-9a25-ba9d460efc8e", "type": "Scope" },
{ "id": "59dacb05-e88d-4c13-a684-59f1afc8cc98", "type": "Scope" },
{ "id": "b98bfd41-87c6-45cc-b104-e2de4f0dafb9", "type": "Scope" },
{ "id": "2f9ee017-59c1-4f1d-9472-bd5529a7b311", "type": "Scope" },
{ "id": "951183d1-1a61-466f-a6d1-1fde911bfd95", "type": "Scope" },
Expand All @@ -86,22 +81,10 @@
{ "id": "7e823077-d88e-468f-a337-e18f1f0e6c7c", "type": "Scope" },
{ "id": "edd3c878-b384-41fd-95ad-e7407dd775be", "type": "Scope" },
{ "id": "40b534c3-9552-4550-901b-23879c90bcf9", "type": "Scope" },
{ "id": "bf3fbf03-f35f-4e93-963e-47e4d874c37a", "type": "Scope" },
{ "id": "5248dcb1-f83b-4ec3-9f4d-a4428a961a72", "type": "Scope" },
{ "id": "c395395c-ff9a-4dba-bc1f-8372ba9dca84", "type": "Scope" },
{ "id": "2e25a044-2580-450d-8859-42eeb6e996c0", "type": "Scope" },
{ "id": "0ce33576-30e8-43b7-99e5-62f8569a4002", "type": "Scope" },
{ "id": "207e0cb1-3ce7-4922-b991-5a760c346ebc", "type": "Scope" },
{ "id": "093f8818-d05f-49b8-95bc-9d2a73e9a43c", "type": "Scope" },
{ "id": "7825d5d6-6049-4ce7-bdf6-3b8d53f4bcd0", "type": "Scope" },
{ "id": "2104a4db-3a2f-4ea0-9dba-143d457dc666", "type": "Scope" },
{ "id": "eda39fa6-f8cf-4c3c-a909-432c683e4c9b", "type": "Scope" },
{ "id": "55896846-df78-47a7-aa94-8d3d4442ca7f", "type": "Scope" },
{ "id": "aa85bf13-d771-4d5d-a9e6-bca04ce44edf", "type": "Scope" },
{ "id": "ee928332-e9c2-4747-b4a0-f8c164b68de6", "type": "Scope" },
{ "id": "c975dd04-a06e-4fbb-9704-62daad77bb49", "type": "Scope" },
{ "id": "c37c9b61-7762-4bff-a156-afc0005847a0", "type": "Scope" },
{ "id": "b9abcc4f-94fc-4457-9141-d20ce80ec952", "type": "Scope" },
{ "id": "128ca929-1a19-45e6-a3b8-435ec44a36ba", "type": "Scope" },
{ "id": "b27add92-efb2-4f16-84f5-8108ba77985c", "type": "Scope" },
{ "id": "3404d2bf-2b13-457e-a330-c24615765193", "type": "Scope" },
Expand Down Expand Up @@ -156,18 +139,10 @@
{ "id": "280b3b69-0437-44b1-bc20-3b2fca1ee3e9", "type": "Scope" },
{ "id": "885f682f-a990-4bad-a642-36736a74b0c7", "type": "Scope" },
{ "id": "913b9306-0ce1-42b8-9137-6a7df690a760", "type": "Role" },
{ "id": "cb8f45a0-5c2e-4ea1-b803-84b870a7d7ec", "type": "Scope" },
{ "id": "4c06a06a-098a-4063-868e-5dfee3827264", "type": "Scope" },
{ "id": "1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9", "type": "Role" },
{ "id": "e67e6727-c080-415e-b521-e3f35d5248e9", "type": "Scope" }
]
},
{
"resourceAppId": "00000002-0000-0000-c000-000000000000",
"resourceAccess": [
{ "id": "5778995a-e1bf-45b8-affa-663a9f3f4d04", "type": "Role" },
{ "id": "a42657d6-7f20-40e3-b6f0-cee03008a62a", "type": "Scope" },
{ "id": "311a71cc-e848-46a1-bdf8-97ff7156d8e6", "type": "Scope" }
{ "id": "e67e6727-c080-415e-b521-e3f35d5248e9", "type": "Scope" },
{ "id": "b6890674-9dd5-4e42-bb15-5af07f541ae1", "type": "Role" }
]
},
{
Expand Down
12 changes: 10 additions & 2 deletions Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuotaUsed.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,17 @@ function Get-CIPPAlertQuotaUsed {
return
}
$AlertData | ForEach-Object {
if ($_.StorageUsedInBytes -eq 0) { return }
if ($_.StorageUsedInBytes -eq 0 -or $_.prohibitSendReceiveQuotaInBytes -eq 0) { return }
$PercentLeft = [math]::round(($_.storageUsedInBytes / $_.prohibitSendReceiveQuotaInBytes) * 100)
if ($InputValue) { $Value = [int]$InputValue } else { $Value = 90 }
try {
if ([int]$InputValue -gt 0) {
$Value = [int]$InputValue
} else {
$Value = 90
}
} catch {
$Value = 90
}
if ($PercentLeft -gt $Value) {
"$($_.userPrincipalName): Mailbox is more than $($value)% full. Mailbox is $PercentLeft% full"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@ function Get-CIPPAlertSharepointQuota {
$TenantFilter
)
Try {
$tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $TenantFilter | Where-Object { $_.isInitial -eq $true }).id.Split('.')[0]
$tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -tenantid $TenantFilter).id.Split('.')[0]
$sharepointToken = (Get-GraphToken -scope "https://$($tenantName)-admin.sharepoint.com/.default" -tenantid $TenantFilter)
$sharepointToken.Add('accept', 'application/json')
$sharepointQuota = (Invoke-RestMethod -Method 'GET' -Headers $sharepointToken -Uri "https://$($tenantName)-admin.sharepoint.com/_api/StorageQuotas()?api-version=1.3.2" -ErrorAction Stop).value
} catch {
return
}
if ($sharepointQuota) {
if ($InputValue -Is [Boolean]) { $Value = 90 } else { $Value = $InputValue }
try {
if ([int]$InputValue -gt 0) { $Value = [int]$InputValue } else { $Value = 90 }
} catch {
$Value = 90
}
$UsedStoragePercentage = [int](($sharepointQuota.GeoUsedStorageMB / $sharepointQuota.TenantStorageMB) * 100)
if ($UsedStoragePercentage -gt $Value) {
$AlertData = "SharePoint Storage is at $($UsedStoragePercentage)%. Your alert threshold is $($Value)%"
Expand Down
1 change: 1 addition & 0 deletions Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function Invoke-ListCippQueue {
$TotalCompleted = $TaskStatus.Completed ?? 0
$TotalFailed = $TaskStatus.Failed ?? 0
$TotalRunning = $TaskStatus.Running ?? 0
if ($Queue.TotalTasks -eq 0) { $Queue.TotalTasks = 1 }

[PSCustomObject]@{
PartitionKey = $Queue.PartitionKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ function Push-BPACollectData {
param($Item)

$TenantName = Get-Tenants | Where-Object -Property defaultDomainName -EQ $Item.Tenant
$CippRoot = (Get-Item $PSScriptRoot).Parent.Parent.Parent.Parent.Parent.Parent.FullName
$TemplatesLoc = Get-ChildItem "$CippRoot\Config\*.BPATemplate.json"
$BPATemplateTable = Get-CippTable -tablename 'templates'
$Filter = "PartitionKey eq 'BPATemplate'"
$TemplatesLoc = (Get-CIPPAzDataTableEntity @BPATemplateTable -Filter $Filter).JSON | ConvertFrom-Json

$Templates = $TemplatesLoc | ForEach-Object {
$Template = $(Get-Content $_) | ConvertFrom-Json
$Template = $_
[PSCustomObject]@{
Data = $Template
Name = $Template.Name
Style = $Template.Style
}
}
$Table = Get-CippTable -tablename 'cachebpav2'

Write-Host "Working on BPA for $($TenantName.displayName) with GUID $($TenantName.customerId) - Report ID $($Item.Template)"
$Template = $Templates | Where-Object -Property Name -EQ -Value $Item.Template
# Build up the result object that will be stored in tables
$Result = @{
Expand All @@ -39,13 +41,13 @@ function Push-BPACollectData {
}
if ($Field.parameters.psobject.properties.name) {
$field.Parameters | ForEach-Object {
Write-Information "Doing: $($_.psobject.properties.name) with value $($_.psobject.properties.value)"
$paramsField[$_.psobject.properties.name] = $_.psobject.properties.value
}
}
$FieldInfo = New-GraphGetRequest @paramsField | Where-Object $filterscript | Select-Object $field.ExtractFields
}
'Exchange' {
Write-Host "Trying to execute $($field.Command) for $($TenantName.displayName) with GUID $($TenantName.customerId)"
if ($field.Command -notlike 'get-*') {
Write-LogMessage -API 'BPA' -tenant $tenant -message 'The BPA only supports get- exchange commands. A set or update command was used.' -sev Error
break
Expand Down Expand Up @@ -93,6 +95,7 @@ function Push-BPACollectData {
}
'JSON' {
if ($FieldInfo -eq $null) { $JsonString = '{}' } else { $JsonString = (ConvertTo-Json -Depth 15 -InputObject $FieldInfo -Compress) }
Write-Host "Adding $($field.Name) to table with value $JsonString"
$Result.Add($field.Name, $JSONString)
}
'string' {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function Push-DomainAnalyserDomain {
}
Set-DnsResolver -Resolver $Resolver

$Domain = $DomainObject.rowKey
$Domain = $DomainObject.RowKey

try {
$Tenant = $DomainObject.TenantDetails | ConvertFrom-Json -ErrorAction Stop
Expand Down Expand Up @@ -250,7 +250,7 @@ function Push-DomainAnalyserDomain {
# Final Write to Output
Write-LogMessage -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message "DNS Analyser Finished For $Domain" -sev Info
} catch {
Write-LogMessage -API -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message "Error saving domain $Domain to table " -sev Error -LogData (Get-CippException -Exception $_)
Write-LogMessage -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message "Error saving domain $Domain to table " -sev Error -LogData (Get-CippException -Exception $_)
}
return $null
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function Push-DomainAnalyserTenant {
return
} else {
try {
$Domains = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/domains' -tenantid $Tenant.customerId | Where-Object { ($_.id -notlike '*.microsoftonline.com' -and $_.id -NotLike '*.exclaimer.cloud' -and $_.id -Notlike '*.excl.cloud' -and $_.id -NotLike '*.codetwo.online' -and $_.id -NotLike '*.call2teams.com' -and $_.isVerified) }
$Domains = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $Tenant.customerId | Where-Object { ($_.id -notlike '*.microsoftonline.com' -and $_.id -NotLike '*.exclaimer.cloud' -and $_.id -Notlike '*.excl.cloud' -and $_.id -NotLike '*.codetwo.online' -and $_.id -NotLike '*.call2teams.com' -and $_.isVerified) }

$TenantDomains = foreach ($d in $Domains) {
[PSCustomObject]@{
Expand All @@ -38,9 +38,11 @@ function Push-DomainAnalyserTenant {
}
}

Write-Information ($TenantDomains | ConvertTo-Json -Depth 10)

$DomainCount = ($TenantDomains | Measure-Object).Count
if ($DomainCount -gt 0) {
Write-Host "$DomainCount tenant Domains"
Write-Host "############# $DomainCount tenant Domains"
$TenantDomainObjects = [System.Collections.Generic.List[object]]::new()
try {
foreach ($TenantDomain in $TenantDomains) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ function Push-ExecScheduledCommand {
$TableDesign = '<style>table.blueTable{border:1px solid #1C6EA4;background-color:#EEE;width:100%;text-align:left;border-collapse:collapse}table.blueTable td,table.blueTable th{border:1px solid #AAA;padding:3px 2px}table.blueTable tbody td{font-size:13px}table.blueTable tr:nth-child(even){background:#D0E4F5}table.blueTable thead{background:#1C6EA4;background:-moz-linear-gradient(top,#5592bb 0,#327cad 66%,#1C6EA4 100%);background:-webkit-linear-gradient(top,#5592bb 0,#327cad 66%,#1C6EA4 100%);background:linear-gradient(to bottom,#5592bb 0,#327cad 66%,#1C6EA4 100%);border-bottom:2px solid #444}table.blueTable thead th{font-size:15px;font-weight:700;color:#FFF;border-left:2px solid #D0E4F5}table.blueTable thead th:first-child{border-left:none}table.blueTable tfoot{font-size:14px;font-weight:700;color:#FFF;background:#D0E4F5;background:-moz-linear-gradient(top,#dcebf7 0,#d4e6f6 66%,#D0E4F5 100%);background:-webkit-linear-gradient(top,#dcebf7 0,#d4e6f6 66%,#D0E4F5 100%);background:linear-gradient(to bottom,#dcebf7 0,#d4e6f6 66%,#D0E4F5 100%);border-top:2px solid #444}table.blueTable tfoot td{font-size:14px}table.blueTable tfoot .links{text-align:right}table.blueTable tfoot .links a{display:inline-block;background:#1C6EA4;color:#FFF;padding:2px 8px;border-radius:5px}</style>'
$FinalResults = if ($results -is [array] -and $results[0] -is [string]) { $Results | ConvertTo-Html -Fragment -Property @{ l = 'Text'; e = { $_ } } } else { $Results | ConvertTo-Html -Fragment }
$HTML = $FinalResults -replace '<table>', "This alert is for tenant $tenant. <br /><br /> $TableDesign<table class=blueTable>" | Out-String
$title = "$TaskType - $($task.Name) - $tenant"
$title = "$TaskType - $tenant - $($task.Name)"
Write-Host 'Scheduler: Sending the results to the target.'
Write-Host "The content of results is: $Results"
switch -wildcard ($task.PostExecution) {
'*psa*' { Send-CIPPAlert -Type 'psa' -Title $title -HTMLContent $HTML }
'*email*' { Send-CIPPAlert -Type 'email' -Title $title -HTMLContent $HTML }
'*psa*' { Send-CIPPAlert -Type 'psa' -Title $title -HTMLContent $HTML -TenantFilter $tenant }
'*email*' { Send-CIPPAlert -Type 'email' -Title $title -HTMLContent $HTML -TenantFilter $tenant }
'*webhook*' {
$Webhook = [PSCustomObject]@{
'Tenant' = $tenant
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
function Push-AuditLogBundleProcessing {
Param($Item)

try {
$AuditBundleTable = Get-CippTable -tablename 'AuditLogBundles'
$AuditLogBundle = Get-CIPPAzDataTableEntity @AuditBundleTable -Filter "PartitionKey eq '$($Item.TenantFilter)' and RowKey eq '$($Item.ContentId)'"
if ($AuditLogBundle.ProcessingStatus -ne 'Pending') {
Write-Information 'Audit log bundle already processed'
return
}
try {
$AuditLogTest = Test-CIPPAuditLogRules -TenantFilter $Item.TenantFilter -LogType $AuditLogBundle.ContentType -ContentUri $AuditLogBundle.ContentUri
$AuditLogBundle.ProcessingStatus = 'Completed'
$AuditLogBundle.MatchedRules = [string](ConvertTo-Json -Compress -Depth 10 -InputObject $AuditLogTest.MatchedRules)
$AuditLogBundle.MatchedLogs = $AuditLogTest.MatchedLogs
} catch {
$AuditLogBundle.ProcessingStatus = 'Failed'
$AuditLogBundle | Add-Member -NotePropertyName Error -NotePropertyValue $_.InvocationInfo.PositionMessage -TypeName string
}
try {
Add-CIPPAzDataTableEntity @AuditBundleTable -Entity $AuditLogBundle -Force
} catch {
Write-Host ( 'Error logging audit bundle: {0} line {1} - {2}' -f $_.InvocationInfo.ScriptName, $_.InvocationInfo.ScriptLineNumber, $_.Exception.Message)
}

$DataToProcess = ($AuditLogTest).DataToProcess
Write-Information "Webhook: Data to process found: $($DataToProcess.count) items"
foreach ($AuditLog in $DataToProcess) {
Write-Information "Processing $($AuditLog.operation)"
$Webhook = @{
Data = $AuditLog
CIPPURL = [string]$AuditLogBundle.CIPPURL
TenantFilter = $Item.TenantFilter
}
Invoke-CippWebhookProcessing @Webhook
}
} catch {
Write-Host ( 'Audit log error {0} line {1} - {2}' -f $_.InvocationInfo.ScriptName, $_.InvocationInfo.ScriptLineNumber, $_.Exception.Message)
}
}
Loading

0 comments on commit df8af2c

Please sign in to comment.