Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 97 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ All scripts follow a consistent three-part structure defined in `script-template
Scripts are organized by category prefixes:
- `app-*`: Application-specific scripts (Duo, Adobe, Teramind, etc.)
- `bdr-*`: Backup and Disaster Recovery (Veeam, MSP360)
- `db-*`: Database engines (MySQL, etc.)
- `iaas-*`: Infrastructure as a Service (Azure, Backblaze, Dynu)
- `mw-*`: Middleware/Microsoft 365 scripts
- `msft-*`: Microsoft Windows system scripts
Expand Down Expand Up @@ -533,13 +534,100 @@ if ($veeamVersion -ge $requiredVersion) {

## Git Workflow

See [DTC KB ... Change Taxonomy](https://kb.dtctoday.com/books/developer-operations-devops/page/change-taxonomy) for the canonical reference. This file mirrors the rules; the KB is authoritative.

**Branching Model:**
* `development` — default branch (HEAD), active work lands here
* `release` — stable/production branch, merged from development when ready
* `enhancement/{name}` — branched from development for new functionality
* `problem/{name}` — branched from development for bug fixes and issue resolution
* No `main` or `master` branches

**GitHub Issues & Labels:**
* New functionality uses the **enhancement** label, not "feature"
* Configure repository labels accordingly
* `main` ... default branch and release branch. Production code deployed to customer environments lives here.
* This repo has no `development` branch. Feature/fix PRs target `main` directly.
* All changes go through a typed branch and a pull request ... no direct commits to `main` (exception: minor `CLAUDE.md` / `README.md` doc updates that do not affect script functionality).

**CRITICAL: All changes must be made in a typed branch, never directly to `main`.**

### Change Taxonomy (two-tier ... Halo / GitHub / branch)

Every change to this repo maps to one of four categories under two parent types. The category drives the branch prefix, the GitHub labels, and the default semver bump (future-state: this repo has no semver versioning today, but the bump column is recorded for when it does). `Refactor` spans both parent types ... see the table.

| Halo Type | Halo Category | GitHub labels | Branch prefix | Default semver |
|---|---|---|---|---|
| `Problem` | `Bug` | `type:problem` + `category:bug` | `bug/{name}` | Patch |
| `Problem` | `Refactor` | `type:problem` + `category:refactor` | `refactor/{name}` | Patch |
| `Enhancement` | `Feature` | `type:enhancement` + `category:feature` | `feature/{name}` | Minor |
| `Enhancement` | `Improvement` | `type:enhancement` + `category:improvement` | `improvement/{name}` | Minor |
| `Enhancement` | `Refactor` | `type:enhancement` + `category:refactor` | `refactor/{name}` | Minor |

How to pick:
- **Bug** ... the script doesn't do what it was designed to do. Null check missing, wrong path, broken regex, off-by-one.
- **Refactor** ... a redesign of how something is shaped. Spans both parent types ... `Problem/Refactor` when the original design was wrong (broken-shape redo, patch bump); `Enhancement/Refactor` when the original design works but is clunky (working-but-clunky redo, minor bump). Same branch prefix and same `category:refactor` label either way; the parent type column disambiguates motivation and semver.
- **Improvement** ... an existing capability done better. Hardening, polish, clearer error output, integrity checks added to existing downloads.
- **Feature** ... net-new capability. A new script, new delivery mechanism, new integration target.

**`BREAKING:` PR title prefix** forces a major version bump regardless of category. (Future-state: applies once this repo carries a semver version.)

**Name branches after the change, not the fix.** Good: `bug/iso-dismount-fails-on-server-2022`, `feature/jsdelivr-script-delivery`. Bad: `bug/my-fix`, `feature/wip`.

**`dependabot/*` branches** ... categorize by the upstream change. CVE or upstream defect → `bug`. Major-version bump that takes new capability → `improvement` or `feature`.

**Legacy prefixes:** `enhancement/` and `problem/` are deprecated by this taxonomy. Existing branches with these prefixes are accepted as-is and can merge under their original names; new work uses the four-prefix model above (`bug/`, `refactor/`, `improvement/`, `feature/`).

### GitHub Labels

Two-tier label set, one of each per PR:

**Type labels:** `type:problem`, `type:enhancement`

**Category labels:** `category:bug`, `category:refactor`, `category:improvement`, `category:feature`

The legacy single-tier labels (`bug`, `enhancement`, `feature`) remain on the repo for issues opened under the old convention. New issues and PRs use the two-tier labels.

### Workflow for All Changes

1. **Create a typed branch off `main`**
```bash
git checkout main
git pull
git checkout -b feature/descriptive-name
# or bug/, refactor/, improvement/ per the taxonomy above
```

Examples:
- `feature/jsdelivr-script-delivery`
- `improvement/vendor-download-integrity`
- `bug/iso-dismount-error`
- `refactor/rmm-input-handler-shared-helper`

2. **Make changes on the branch**
- Make all code modifications on the typed branch
- Commit changes with descriptive messages
- Test thoroughly in both interactive and RMM modes

3. **Push branch**
```bash
git push -u origin feature/descriptive-name
```

4. **Create pull request**
- Open PR from your branch to `main`
- Apply the two labels from the taxonomy table (one `type:*` + one `category:*`)
- Include description of changes, testing performed, and RMM compatibility
- Wait for review and approval before merging

5. **Merge to `main`**
- Only merge after testing and approval
- Delete the branch after successful merge

### If Changes Are Accidentally Committed to Main

```bash
# Revert the commit from main
git revert <commit-hash> --no-edit
git push

# Create the typed branch and restore the changes
git checkout -b feature/descriptive-name
git cherry-pick <commit-hash>
git push -u origin feature/descriptive-name
```

### Exception: Documentation Updates

Minor documentation updates to `CLAUDE.md` or `README.md` may be committed directly to `main` if they do not affect script functionality.
177 changes: 177 additions & 0 deletions integrations/ninja-halo-guid-sync.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<#
.SYNOPSIS
Syncs DTC Org GUID from NinjaRMM organizations to Halo PSA client custom field.

.DESCRIPTION
Reads the dtcOrgGuid custom field from each NinjaRMM organization and writes
it to the CFDtcClientGuid field in Halo PSA for all exactly-matched clients.

Runs unattended via NinjaOne Scheduled Task (Sundays 2:00 AM EDT).
Matching is exact by client name. Name mismatches are logged and skipped.

.PARAMETER haloclientsecret
Halo PSA API client secret. Injected by NinjaOne Script Variable: haloclientsecret.

.PARAMETER ninjaclientsecret
NinjaRMM API client secret. Injected by NinjaOne Script Variable: ninjaclientsecret.

.NOTES
Author: Tyler Dantzler
Repo: DTC-Inc/msp-script-library/integrations/ninja-halo-guid-sync.ps1
BookStack: Page 1908
Schedule: Weekly, Sundays 2:00 AM EDT (NinjaOne Scheduled Task)

## PLEASE COMMENT YOUR VARIABLES DIRECTLY BELOW HERE IF YOU ARE RUNNING FROM A RMM
# $env:RMM -- set to "1" by NinjaOne at runtime; controls RMM vs interactive mode
# $env:Description -- human-readable audit trail label for this run
# $env:haloclientsecret -- Halo PSA API client secret (NinjaOne Script Variable)
# $env:ninjaclientsecret -- NinjaRMM API client secret (NinjaOne Script Variable)
#>

[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

# --- AUDIT TRAIL & LOGGING ---
$ScriptLogName = 'ninja-halo-guid-sync'
$Description = if ($env:Description) { $env:Description } else { 'NinjaRMM to Halo PSA GUID sync (manual run)' }
$logPath = "C:\ProgramData\DTC\Logs\$ScriptLogName-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
New-Item -ItemType Directory -Path (Split-Path $logPath) -Force | Out-Null
Start-Transcript -Path $logPath -Append

Write-Information "[$ScriptLogName] Starting — $Description" -InformationAction Continue

try {

# --- CONFIGURATION ---
# Client IDs are not secrets — hardcoded here.
# Client secrets are injected via NinjaOne Script Variables (env vars at runtime).
$HaloClientId = '25542a6d-2d0e-4093-bf82-e11edc64faf6'
$HaloClientSecret = $env:haloclientsecret
$HaloBaseUrl = 'https://psa.dtctoday.com'
$HaloScope = 'read:customers edit:customers'

$NinjaClientId = '0S1xEjce1FQp7Rbn_GJTrSWTp64'
$NinjaClientSecret = $env:ninjaclientsecret
$NinjaBaseUrl = 'https://app.ninjarmm.com'

# --- VALIDATE SECRETS ---
$missing = @()
if ([string]::IsNullOrWhiteSpace($HaloClientSecret)) { $missing += 'haloclientsecret' }
if ([string]::IsNullOrWhiteSpace($NinjaClientSecret)) { $missing += 'ninjaclientsecret' }

if ($missing.Count -gt 0) {
throw "Missing NinjaOne Script Variables: $($missing -join ', '). Set default values in Library > Automation > Halo GUID Sync > Script Variables."
}

# --- AUTHENTICATE: HALO ---
Write-Information ' Authenticating with Halo PSA...' -InformationAction Continue
$haloTokenResp = Invoke-RestMethod -Method Post -Uri "$HaloBaseUrl/auth/token" `
-ContentType 'application/x-www-form-urlencoded' -Body @{
grant_type = 'client_credentials'
client_id = $HaloClientId
client_secret = $HaloClientSecret
scope = $HaloScope
}
$haloToken = $haloTokenResp.access_token
$haloHeaders = @{ Authorization = "Bearer $haloToken" }
Write-Information ' Halo auth OK' -InformationAction Continue

# --- AUTHENTICATE: NINJA ---
Write-Information ' Authenticating with NinjaRMM...' -InformationAction Continue
$ninjaTokenResp = Invoke-RestMethod -Method Post -Uri "$NinjaBaseUrl/ws/oauth/token" `
-ContentType 'application/x-www-form-urlencoded' -Body @{
grant_type = 'client_credentials'
client_id = $NinjaClientId
client_secret = $NinjaClientSecret
scope = 'monitoring management'
}
$ninjaToken = $ninjaTokenResp.access_token
$ninjaHeaders = @{ Authorization = "Bearer $ninjaToken" }
Write-Information ' Ninja auth OK' -InformationAction Continue

# --- PULL NINJA ORGS ---
$orgs = Invoke-RestMethod -Method Get -Uri "$NinjaBaseUrl/v2/organizations" -Headers $ninjaHeaders
$results = @()

foreach ($org in $orgs) {
$orgName = $org.name
$orgId = $org.id

# Read DTC Org GUID custom field
try {
$cf = Invoke-RestMethod -Method Get `
-Uri "$NinjaBaseUrl/v2/organization/$orgId/custom-fields" -Headers $ninjaHeaders
$guid = $cf.dtcOrgGuid
} catch {
Write-Warning "Could not read custom fields for '$orgName' — skipping"
continue
}

if ([string]::IsNullOrWhiteSpace($guid)) {
$results += [PSCustomObject]@{ NinjaOrg = $orgName; Status = 'No GUID'; GUID = '' }
continue
}

# Search Halo for matching client by exact name
try {
$haloSearch = Invoke-RestMethod -Method Get `
-Uri "$HaloBaseUrl/api/client?search=$([uri]::EscapeDataString($orgName))" `
-Headers $haloHeaders
} catch {
Write-Warning "Halo search failed for '$orgName' — skipping"
continue
}

$haloClient = $haloSearch.clients | Where-Object { $_.name -eq $orgName } | Select-Object -First 1

if (-not $haloClient) {
Write-Warning "No exact Halo match for '$orgName' — skipping"
$results += [PSCustomObject]@{ NinjaOrg = $orgName; Status = 'No Halo match'; GUID = $guid }
continue
}

# Build body — PowerShell 5 unwraps single-item @() on ConvertTo-Json so manually wrap in []
$clientUpdate = @{
id = $haloClient.id
customfields = @(
@{ name = 'CFDtcClientGuid'; value = $guid }
)
}
$body = '[' + ($clientUpdate | ConvertTo-Json -Depth 5) + ']'

try {
Invoke-RestMethod -Method Post -Uri "$HaloBaseUrl/api/client" `
-Headers $haloHeaders -ContentType 'application/json' -Body $body | Out-Null
Write-Information " [OK] $orgName" -InformationAction Continue
$results += [PSCustomObject]@{ NinjaOrg = $orgName; Status = 'Updated'; GUID = $guid }
} catch {
$errorDetail = $_.ErrorDetails.Message
Write-Warning "Failed to update '$orgName' — $($_.Exception.Message) | $errorDetail"
$results += [PSCustomObject]@{ NinjaOrg = $orgName; Status = 'Failed'; GUID = $guid }
}
}

# --- SUMMARY ---
Write-Information '' -InformationAction Continue
Write-Information '===== SYNC SUMMARY =====' -InformationAction Continue
$results | Format-Table -AutoSize

$failed = $results | Where-Object { $_.Status -eq 'Failed' }
if ($failed) {
Write-Warning "$($failed.Count) client(s) failed to update — see above"
exit 2
}

Write-Information "[$ScriptLogName] Completed successfully." -InformationAction Continue
exit 0

} catch {
Write-Error "FAILED: $_"
Write-Error $_.ScriptStackTrace
exit 1
} finally {
Stop-Transcript
}
102 changes: 0 additions & 102 deletions msft-windows/msft-win-shutdown-disable.ps1

This file was deleted.

Loading