Repository moved to Forgejo
https://forgejo.oskamp.info/ivooskamp/Random_scripts.git
This commit is contained in:
parent
5514553dd3
commit
d26a5d4a0a
13
.gitignore
vendored
13
.gitignore
vendored
@ -1,13 +0,0 @@
|
|||||||
# Runtime/output
|
|
||||||
output/*
|
|
||||||
!output/.gitkeep
|
|
||||||
|
|
||||||
# PowerShell logs/transcripts
|
|
||||||
*.log
|
|
||||||
*.txt
|
|
||||||
|
|
||||||
# OS/editor
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
46
README.md
46
README.md
@ -1,43 +1,9 @@
|
|||||||
# Random Scripts
|
# Random_scripts — moved
|
||||||
|
|
||||||
Collection of operational scripts for Microsoft 365, Windows endpoint management, macOS, and Docker automation.
|
This repository has **moved to Forgejo**.
|
||||||
|
|
||||||
## Structure
|
➡️ **New location:** https://forgejo.oskamp.info/ivooskamp/Random_scripts.git
|
||||||
|
|
||||||
- `scripts/m365/smtp/`
|
```bash
|
||||||
- `Test-SMTPRelay-M365.ps1`
|
git remote set-url origin https://forgejo.oskamp.info/ivooskamp/Random_scripts.git
|
||||||
- `Test-SMTPSend-Auth-M365.ps1`
|
```
|
||||||
- `scripts/m365/exchange/`
|
|
||||||
- `get-mailbox-with-permissions.ps1`
|
|
||||||
- `scripts/m365/identity/`
|
|
||||||
- `m365-users-mfa.ps1`
|
|
||||||
- `scripts/m365/sharepoint/`
|
|
||||||
- `get-sharepoint-permissions.ps1`
|
|
||||||
- `scripts/windows/autopilot/`
|
|
||||||
- `Get-WindowsAutoPilotInfo.ps1`
|
|
||||||
- `scripts/windows/endpoint/`
|
|
||||||
- `Update rmm.ps1`
|
|
||||||
- `schedule_reboot.ps1`
|
|
||||||
- `uninstall-MS-Visual-C++-2010.ps1`
|
|
||||||
- `uuid.ps1`
|
|
||||||
- `scripts/macos/`
|
|
||||||
- `serialToAdminPassword.sh`
|
|
||||||
- `scripts/content/`
|
|
||||||
- `convert-md.bat`
|
|
||||||
- `automation/docker/`
|
|
||||||
- `build-and-push.sh`
|
|
||||||
- `update-and-build.sh`
|
|
||||||
- `docs/`
|
|
||||||
- working practices and conventions
|
|
||||||
- `output/`
|
|
||||||
- generated output files (CSV/logs), not committed
|
|
||||||
|
|
||||||
## Important Note
|
|
||||||
|
|
||||||
Legacy script filenames are intentionally **unchanged** to preserve compatibility with existing guides and runbooks. New scripts may use consistent lowercase naming.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
- Run scripts from the correct environment (PowerShell/Bash/Batch).
|
|
||||||
- Check required modules/permissions per script.
|
|
||||||
- Write exports to `output/` when possible.
|
|
||||||
|
|||||||
@ -1,269 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# build-and-push.sh
|
|
||||||
# Location: repo root
|
|
||||||
#
|
|
||||||
# Purpose:
|
|
||||||
# - Automatic version bump:
|
|
||||||
# 1 = patch, 2 = minor, 3 = major, t = test
|
|
||||||
# - Test builds: only update :dev (no commit/tag)
|
|
||||||
# - Release builds: update version.txt, commit, tag, push (to the current branch)
|
|
||||||
# - Build & push Docker images for each service under ./compose/*
|
|
||||||
# - Preflight checks: Docker daemon up, logged in to registry, valid names/tags
|
|
||||||
# - Summary: show all images + tags built and pushed
|
|
||||||
# - Branch visibility:
|
|
||||||
# - Shows currently checked out branch (authoritative)
|
|
||||||
# - Reads .last-branch for info (if present) when BRANCH is not set
|
|
||||||
# - Writes the current branch back to .last-branch at the end
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# BRANCH=<branch> ./build-and-push.sh [bump] # BRANCH is optional; informative only
|
|
||||||
# ./build-and-push.sh [bump]
|
|
||||||
# If [bump] is omitted, you will be prompted (default = t).
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
DOCKER_REGISTRY="gitea.oskamp.info"
|
|
||||||
DOCKER_NAMESPACE="ivooskamp"
|
|
||||||
|
|
||||||
VERSION_FILE="version.txt"
|
|
||||||
START_VERSION="v0.1.0"
|
|
||||||
COMPOSE_DIR="containers"
|
|
||||||
LAST_BRANCH_FILE=".last-branch" # stored in repo root
|
|
||||||
|
|
||||||
# --- Input: prompt if missing ------------------------------------------------
|
|
||||||
BUMP="${1:-}"
|
|
||||||
if [[ -z "${BUMP}" ]]; then
|
|
||||||
echo "Select bump type: [1] patch, [2] minor, [3] major, [t] test (default: t)"
|
|
||||||
read -r BUMP
|
|
||||||
BUMP="${BUMP:-t}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$BUMP" != "1" && "$BUMP" != "2" && "$BUMP" != "3" && "$BUMP" != "t" ]]; then
|
|
||||||
echo "[ERROR] Unknown bump type '$BUMP' (use 1, 2, 3, or t)."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Helpers -----------------------------------------------------------------
|
|
||||||
read_version() {
|
|
||||||
if [[ -f "$VERSION_FILE" ]]; then
|
|
||||||
tr -d ' \t\n\r' < "$VERSION_FILE"
|
|
||||||
else
|
|
||||||
echo "$START_VERSION"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
write_version() {
|
|
||||||
echo "$1" > "$VERSION_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
bump_version() {
|
|
||||||
local cur="$1"
|
|
||||||
local kind="$2"
|
|
||||||
local core="${cur#v}"
|
|
||||||
IFS='.' read -r MA MI PA <<< "$core"
|
|
||||||
case "$kind" in
|
|
||||||
1) PA=$((PA + 1));;
|
|
||||||
2) MI=$((MI + 1)); PA=0;;
|
|
||||||
3) MA=$((MA + 1)); MI=0; PA=0;;
|
|
||||||
*) echo "[ERROR] Unknown bump kind"; exit 1;;
|
|
||||||
esac
|
|
||||||
echo "v${MA}.${MI}.${PA}"
|
|
||||||
}
|
|
||||||
|
|
||||||
check_docker_ready() {
|
|
||||||
if ! docker info >/dev/null 2>&1; then
|
|
||||||
echo "[ERROR] Docker daemon not reachable. Is Docker running and do you have permission to use it?"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_registry_login() {
|
|
||||||
local cfg="${HOME}/.docker/config.json"
|
|
||||||
if [[ ! -f "$cfg" ]]; then
|
|
||||||
echo "[ERROR] Docker config not found at $cfg. Please login: docker login ${DOCKER_REGISTRY}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! grep -q "\"${DOCKER_REGISTRY}\"" "$cfg"; then
|
|
||||||
echo "[ERROR] No registry auth found for ${DOCKER_REGISTRY}. Please run: docker login ${DOCKER_REGISTRY}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_repo_component() {
|
|
||||||
local comp="$1"
|
|
||||||
if [[ ! "$comp" =~ ^[a-z0-9]+([._-][a-z0-9]+)*$ ]]; then
|
|
||||||
echo "[ERROR] Invalid repository component '$comp'."
|
|
||||||
echo " Must match: ^[a-z0-9]+([._-][a-z0-9]+)*$ (lowercase, digits, ., _, - as separators)."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_tag() {
|
|
||||||
local tag="$1"
|
|
||||||
local len="${#tag}"
|
|
||||||
if (( len < 1 || len > 128 )); then
|
|
||||||
echo "[ERROR] Invalid tag length ($len). Must be between 1 and 128 characters."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [[ ! "$tag" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]]; then
|
|
||||||
echo "[ERROR] Invalid tag '$tag'. Allowed: [A-Za-z0-9_.-], must start with alphanumeric or underscore."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Preflight ---------------------------------------------------------------
|
|
||||||
if [[ ! -d ".git" ]]; then
|
|
||||||
echo "[ERROR] Not a git repository (.git missing)."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -d "$COMPOSE_DIR" ]]; then
|
|
||||||
echo "[ERROR] '$COMPOSE_DIR' directory missing. Expected ./compose/<service>/ with a Dockerfile."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
check_docker_ready
|
|
||||||
ensure_registry_login
|
|
||||||
validate_repo_component "$DOCKER_NAMESPACE"
|
|
||||||
|
|
||||||
# Detect currently checked out branch (authoritative for this script)
|
|
||||||
DETECTED_BRANCH="$(git branch --show-current 2>/dev/null || true)"
|
|
||||||
if [[ -z "$DETECTED_BRANCH" ]]; then
|
|
||||||
DETECTED_BRANCH="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
if [[ -z "$DETECTED_BRANCH" ]]; then
|
|
||||||
# Try to derive from upstream
|
|
||||||
UPSTREAM_REF_DERIVED="$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || true)"
|
|
||||||
if [[ -n "$UPSTREAM_REF_DERIVED" ]]; then
|
|
||||||
DETECTED_BRANCH="${UPSTREAM_REF_DERIVED#origin/}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [[ -z "$DETECTED_BRANCH" ]]; then
|
|
||||||
DETECTED_BRANCH="main"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Optional signals: BRANCH env and .last-branch (informational only)
|
|
||||||
ENV_BRANCH="${BRANCH:-}"
|
|
||||||
LAST_BRANCH_FILE_PATH="$(pwd)/$LAST_BRANCH_FILE"
|
|
||||||
LAST_BRANCH_VALUE=""
|
|
||||||
if [[ -z "$ENV_BRANCH" && -f "$LAST_BRANCH_FILE_PATH" ]]; then
|
|
||||||
LAST_BRANCH_VALUE="$(tr -d ' \t\n\r' < "$LAST_BRANCH_FILE_PATH")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
UPSTREAM_REF="$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || echo "origin/$DETECTED_BRANCH")"
|
|
||||||
HEAD_SHA="$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")"
|
|
||||||
|
|
||||||
echo "[INFO] Repo: $(pwd)"
|
|
||||||
echo "[INFO] Current branch: $DETECTED_BRANCH"
|
|
||||||
echo "[INFO] Upstream: $UPSTREAM_REF"
|
|
||||||
echo "[INFO] HEAD (sha): $HEAD_SHA"
|
|
||||||
|
|
||||||
if [[ -n "$ENV_BRANCH" && "$ENV_BRANCH" != "$DETECTED_BRANCH" ]]; then
|
|
||||||
echo "[WARNING] BRANCH='$ENV_BRANCH' differs from checked out branch '$DETECTED_BRANCH'."
|
|
||||||
echo "[WARNING] This script does not switch branches; continuing on '$DETECTED_BRANCH'."
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$LAST_BRANCH_VALUE" && "$LAST_BRANCH_VALUE" != "$DETECTED_BRANCH" && -z "$ENV_BRANCH" ]]; then
|
|
||||||
echo "[INFO] .last-branch suggests '$LAST_BRANCH_VALUE', but current checkout is '$DETECTED_BRANCH'."
|
|
||||||
echo "[INFO] If you intended to build '$LAST_BRANCH_VALUE', switch branches first (use update-and-build.sh)."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Versioning --------------------------------------------------------------
|
|
||||||
CURRENT_VERSION="$(read_version)"
|
|
||||||
NEW_VERSION="$CURRENT_VERSION"
|
|
||||||
DO_TAG_AND_BUMP=true
|
|
||||||
|
|
||||||
if [[ "$BUMP" == "t" ]]; then
|
|
||||||
echo "[INFO] Test build: keeping version $CURRENT_VERSION; will only update :dev."
|
|
||||||
DO_TAG_AND_BUMP=false
|
|
||||||
else
|
|
||||||
NEW_VERSION="$(bump_version "$CURRENT_VERSION" "$BUMP")"
|
|
||||||
echo "[INFO] New version: $NEW_VERSION"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if $DO_TAG_AND_BUMP; then
|
|
||||||
validate_tag "$NEW_VERSION"
|
|
||||||
fi
|
|
||||||
validate_tag "latest"
|
|
||||||
|
|
||||||
# --- Version update + VCS ops (release builds only) --------------------------
|
|
||||||
if $DO_TAG_AND_BUMP; then
|
|
||||||
echo "[INFO] Writing $NEW_VERSION to $VERSION_FILE"
|
|
||||||
write_version "$NEW_VERSION"
|
|
||||||
|
|
||||||
echo "[INFO] Git add + commit (branch: $DETECTED_BRANCH)"
|
|
||||||
git add "$VERSION_FILE"
|
|
||||||
git commit -m "Release $NEW_VERSION on branch $DETECTED_BRANCH (bump type $BUMP)"
|
|
||||||
|
|
||||||
echo "[INFO] Git tag $NEW_VERSION"
|
|
||||||
git tag -a "$NEW_VERSION" -m "Release $NEW_VERSION"
|
|
||||||
|
|
||||||
echo "[INFO] Git push + tags"
|
|
||||||
git push origin "$DETECTED_BRANCH"
|
|
||||||
git push --tags
|
|
||||||
else
|
|
||||||
echo "[INFO] Skipping commit/tagging (test build)."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Build & push per service ------------------------------------------------
|
|
||||||
shopt -s nullglob
|
|
||||||
services=( "$COMPOSE_DIR"/* )
|
|
||||||
if [[ ${#services[@]} -eq 0 ]]; then
|
|
||||||
echo "[ERROR] No services found under $COMPOSE_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
BUILT_IMAGES=()
|
|
||||||
|
|
||||||
for svc_path in "${services[@]}"; do
|
|
||||||
[[ -d "$svc_path" ]] || continue
|
|
||||||
svc="$(basename "$svc_path")"
|
|
||||||
dockerfile="$svc_path/Dockerfile"
|
|
||||||
|
|
||||||
validate_repo_component "$svc"
|
|
||||||
|
|
||||||
if [[ ! -f "$dockerfile" ]]; then
|
|
||||||
echo "[WARNING] Skipping '${svc}': Dockerfile not found in ${svc_path}"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
IMAGE_BASE="${DOCKER_REGISTRY}/${DOCKER_NAMESPACE}/${svc}"
|
|
||||||
|
|
||||||
if $DO_TAG_AND_BUMP; then
|
|
||||||
echo "============================================================"
|
|
||||||
echo "[INFO] Building ${svc} -> tags: ${NEW_VERSION}, latest"
|
|
||||||
echo "============================================================"
|
|
||||||
docker build -t "${IMAGE_BASE}:${NEW_VERSION}" -t "${IMAGE_BASE}:dev" "$svc_path"
|
|
||||||
docker push "${IMAGE_BASE}:${NEW_VERSION}"
|
|
||||||
docker push "${IMAGE_BASE}:dev"
|
|
||||||
BUILT_IMAGES+=("${IMAGE_BASE}:${NEW_VERSION}" "${IMAGE_BASE}:dev")
|
|
||||||
else
|
|
||||||
echo "============================================================"
|
|
||||||
echo "[INFO] Test build ${svc} -> tag: latest"
|
|
||||||
echo "============================================================"
|
|
||||||
docker build -t "${IMAGE_BASE}:dev" "$svc_path"
|
|
||||||
docker push "${IMAGE_BASE}:dev"
|
|
||||||
BUILT_IMAGES+=("${IMAGE_BASE}:dev")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# --- Persist current branch to .last-branch ----------------------------------
|
|
||||||
# (This helps script 1 to preselect next time, and is informative if you run script 2 standalone)
|
|
||||||
echo "$DETECTED_BRANCH" > "$LAST_BRANCH_FILE_PATH"
|
|
||||||
|
|
||||||
# --- Summary -----------------------------------------------------------------
|
|
||||||
echo ""
|
|
||||||
echo "============================================================"
|
|
||||||
echo "[SUMMARY] Build & push complete (branch: $DETECTED_BRANCH)"
|
|
||||||
if $DO_TAG_AND_BUMP; then
|
|
||||||
echo "[INFO] Release version: $NEW_VERSION"
|
|
||||||
else
|
|
||||||
echo "[INFO] Test build (no version bump)"
|
|
||||||
fi
|
|
||||||
echo "[INFO] Images pushed:"
|
|
||||||
for img in "${BUILT_IMAGES[@]}"; do
|
|
||||||
echo " - $img"
|
|
||||||
done
|
|
||||||
echo "============================================================"
|
|
||||||
@ -1,232 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# update-and-build.sh
|
|
||||||
# Location: /docker/develop
|
|
||||||
#
|
|
||||||
# Purpose:
|
|
||||||
# - Laat je een branch kiezen (of via BRANCH= meegeven)
|
|
||||||
# - Onthoudt de laatst gebruikte branch in .last-branch in de repo root
|
|
||||||
# - Commit en pusht altijd lokale wijzigingen (AUTO_COMMIT standaard aan)
|
|
||||||
# - Haalt daarna updates op uit origin/<branch> (git pull --rebase)
|
|
||||||
# - Roept daarna het build-script in de repo aan (build-and-push.sh) met de
|
|
||||||
# gekozen bump type (1/2/3/t)
|
|
||||||
#
|
|
||||||
# Gebruik:
|
|
||||||
# BRANCH=<branch> sudo bash /docker/develop/update-and-build.sh [bump]
|
|
||||||
# (BRANCH optioneel; zonder krijg je een menu met keuze, of kun je
|
|
||||||
# handmatig een nieuwe branchnaam intypen)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
REPO_DIR="/docker/develop/backup-v9" # pas aan indien nodig
|
|
||||||
BUILD_SCRIPT_NAME="build-and-push.sh"
|
|
||||||
LAST_BRANCH_FILE=".last-branch" # wordt in REPO_DIR opgeslagen
|
|
||||||
|
|
||||||
# Omgeving: optionele flags voor non-interactief gebruik
|
|
||||||
AUTO_COMMIT="${AUTO_COMMIT:-1}" # 1 = altijd committen als er wijzigingen zijn
|
|
||||||
AUTO_PUSH="${AUTO_PUSH:-1}" # 1 = push naar origin/<branch> na commit
|
|
||||||
AUTO_CREATE_BRANCH="${AUTO_CREATE_BRANCH:-1}" # 1 = nieuwe branch lokaal maken als hij nog niet bestaat
|
|
||||||
|
|
||||||
# Commit message (optioneel prefix via COMMIT_MSG_PREFIX)
|
|
||||||
DEFAULT_COMMIT_MSG_PREFIX="${COMMIT_MSG_PREFIX:-Auto-commit local changes before build}"
|
|
||||||
|
|
||||||
# --- Input: bump type --------------------------------------------------------
|
|
||||||
BUMP_TYPE="${1:-}"
|
|
||||||
if [[ -z "${BUMP_TYPE}" ]]; then
|
|
||||||
echo "Select bump type: [1] patch, [2] minor, [3] major, [t] test (default: t)"
|
|
||||||
read -r BUMP_TYPE
|
|
||||||
BUMP_TYPE="${BUMP_TYPE:-t}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$BUMP_TYPE" != "1" && "$BUMP_TYPE" != "2" && "$BUMP_TYPE" != "3" && "$BUMP_TYPE" != "t" ]]; then
|
|
||||||
echo "[ERROR] Invalid bump type '$BUMP_TYPE' (use 1, 2, 3, or t)."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Naar repo gaan ----------------------------------------------------------
|
|
||||||
echo "[INFO] Switching to repo: $REPO_DIR"
|
|
||||||
cd "$REPO_DIR"
|
|
||||||
|
|
||||||
# --- Fetch alle branches -----------------------------------------------------
|
|
||||||
echo "[INFO] Fetching all branches (origin)..."
|
|
||||||
git fetch --all --prune
|
|
||||||
|
|
||||||
# Lijst met origin branches (zonder origin/ prefix, HEAD gefilterd)
|
|
||||||
mapfile -t BRANCHES < <(
|
|
||||||
git for-each-ref \
|
|
||||||
--format='%(refname:short)' refs/remotes/origin \
|
|
||||||
| sed 's|^origin/||' \
|
|
||||||
| grep -vE '^HEAD$' \
|
|
||||||
| sort -u
|
|
||||||
)
|
|
||||||
|
|
||||||
# Fallback: als er geen origin branches zijn, kijk dan naar lokale branches
|
|
||||||
if [[ "${#BRANCHES[@]}" -eq 0 ]]; then
|
|
||||||
mapfile -t BRANCHES < <(
|
|
||||||
git for-each-ref \
|
|
||||||
--format='%(refname:short)' refs/heads \
|
|
||||||
| sort -u
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${#BRANCHES[@]}" -eq 0 ]]; then
|
|
||||||
echo "[ERROR] Geen branches gevonden (noch lokaal, noch bij origin)."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Bepaal default branch/last branch ---------------------------------------
|
|
||||||
ENV_BRANCH="${BRANCH:-}"
|
|
||||||
|
|
||||||
LAST_BRANCH=""
|
|
||||||
if [[ -f "$LAST_BRANCH_FILE" ]]; then
|
|
||||||
LAST_BRANCH="$(<"$LAST_BRANCH_FILE")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
CURRENT_BRANCH="$(git branch --show-current 2>/dev/null || true)"
|
|
||||||
|
|
||||||
DEFAULT_BRANCH="$CURRENT_BRANCH"
|
|
||||||
if [[ -z "$DEFAULT_BRANCH" ]]; then
|
|
||||||
if [[ -n "$LAST_BRANCH" ]]; then
|
|
||||||
DEFAULT_BRANCH="$LAST_BRANCH"
|
|
||||||
else
|
|
||||||
DEFAULT_BRANCH="${BRANCHES[0]}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
CHOSEN_BRANCH=""
|
|
||||||
|
|
||||||
# 1) Als BRANCH= in de omgeving is gezet, gebruik die direct
|
|
||||||
if [[ -n "$ENV_BRANCH" ]]; then
|
|
||||||
CHOSEN_BRANCH="$ENV_BRANCH"
|
|
||||||
echo "[INFO] Branch via BRANCH env: $CHOSEN_BRANCH"
|
|
||||||
else
|
|
||||||
echo "[INFO] Beschikbare bestaande branches:"
|
|
||||||
i=1
|
|
||||||
DEFAULT_INDEX=1
|
|
||||||
for b in "${BRANCHES[@]}"; do
|
|
||||||
if [[ "$b" == "$DEFAULT_BRANCH" ]]; then
|
|
||||||
echo " [$i] $b (default)"
|
|
||||||
DEFAULT_INDEX="$i"
|
|
||||||
else
|
|
||||||
echo " [$i] $b"
|
|
||||||
fi
|
|
||||||
((i++))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Je kunt een nummer kiezen voor een bestaande branch,"
|
|
||||||
echo "of een nieuwe branch-naam intypen."
|
|
||||||
read -r -p "Selecteer nummer of typ nieuwe branch-naam (leeg = default): " choice
|
|
||||||
|
|
||||||
# Leeg = default branch
|
|
||||||
if [[ -z "$choice" ]]; then
|
|
||||||
CHOSEN_BRANCH="$DEFAULT_BRANCH"
|
|
||||||
echo "[INFO] Default branch geselecteerd: $CHOSEN_BRANCH"
|
|
||||||
# Als gebruiker een nummer kiest
|
|
||||||
elif [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#BRANCHES[@]} )); then
|
|
||||||
CHOSEN_BRANCH="${BRANCHES[choice-1]}"
|
|
||||||
echo "[INFO] Bestaande branch gekozen: $CHOSEN_BRANCH"
|
|
||||||
else
|
|
||||||
# Anders: dit is een nieuwe branch
|
|
||||||
CHOSEN_BRANCH="$choice"
|
|
||||||
echo "[INFO] Nieuwe branch gekozen: $CHOSEN_BRANCH (wordt aangemaakt als hij nog niet bestaat)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Schrijf gekozen branch weg als last-branch
|
|
||||||
echo "$CHOSEN_BRANCH" > "$LAST_BRANCH_FILE"
|
|
||||||
|
|
||||||
# --- Branch checkout / aanmaken ----------------------------------------------
|
|
||||||
REMOTE_REF_EXISTS=0
|
|
||||||
if git show-ref --verify --quiet "refs/remotes/origin/$CHOSEN_BRANCH"; then
|
|
||||||
REMOTE_REF_EXISTS=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
LOCAL_REF_EXISTS=0
|
|
||||||
if git show-ref --verify --quiet "refs/heads/$CHOSEN_BRANCH"; then
|
|
||||||
LOCAL_REF_EXISTS=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$LOCAL_REF_EXISTS" -eq 1 ]]; then
|
|
||||||
echo "[INFO] Checkout bestaande lokale branch: $CHOSEN_BRANCH"
|
|
||||||
git switch "$CHOSEN_BRANCH"
|
|
||||||
else
|
|
||||||
if [[ "$REMOTE_REF_EXISTS" -eq 1 ]]; then
|
|
||||||
echo "[INFO] Nieuwe lokale branch maken vanaf origin/$CHOSEN_BRANCH"
|
|
||||||
git switch -c "$CHOSEN_BRANCH" --track "origin/$CHOSEN_BRANCH"
|
|
||||||
else
|
|
||||||
if [[ "$AUTO_CREATE_BRANCH" -eq 1 ]]; then
|
|
||||||
echo "[INFO] Nieuwe lokale branch maken (zonder bestaande remote): $CHOSEN_BRANCH"
|
|
||||||
git switch -c "$CHOSEN_BRANCH"
|
|
||||||
else
|
|
||||||
echo "[ERROR] Branch '$CHOSEN_BRANCH' bestaat niet lokaal en niet bij origin."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Functie: auto-commit lokale wijzigingen --------------------------------
|
|
||||||
auto_commit_if_needed() {
|
|
||||||
if [[ "$AUTO_COMMIT" -ne 1 ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check of er iets te committen is
|
|
||||||
if [[ -z "$(git status --porcelain)" ]]; then
|
|
||||||
echo "[INFO] Geen lokale wijzigingen om te committen."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[INFO] Lokale wijzigingen gedetecteerd in branch '$CHOSEN_BRANCH':"
|
|
||||||
git status --short
|
|
||||||
|
|
||||||
echo "[INFO] Auto-commit lokale wijzigingen..."
|
|
||||||
git add -A
|
|
||||||
|
|
||||||
# Commit message opbouwen (met timestamp)
|
|
||||||
local ts
|
|
||||||
ts="$(date +'%Y-%m-%d %H:%M:%S')"
|
|
||||||
local msg="${DEFAULT_COMMIT_MSG_PREFIX} (${ts})"
|
|
||||||
|
|
||||||
git commit -m "$msg"
|
|
||||||
|
|
||||||
if [[ "$AUTO_PUSH" -eq 1 ]]; then
|
|
||||||
echo "[INFO] Pushing naar origin/$CHOSEN_BRANCH..."
|
|
||||||
git push -u origin "$CHOSEN_BRANCH"
|
|
||||||
else
|
|
||||||
echo "[INFO] AUTO_PUSH=0, push wordt overgeslagen."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Eerst altijd lokale wijzigingen committen (voor git pull) --------------
|
|
||||||
auto_commit_if_needed
|
|
||||||
|
|
||||||
# --- Daarna updaten vanaf origin (als remote bestaat) ------------------------
|
|
||||||
if git rev-parse --verify --quiet "origin/$CHOSEN_BRANCH" >/dev/null; then
|
|
||||||
echo "[INFO] Pulling latest changes from origin/$CHOSEN_BRANCH (rebase)..."
|
|
||||||
if ! git pull --rebase origin "$CHOSEN_BRANCH"; then
|
|
||||||
echo "[ERROR] git pull --rebase gaf een fout. Los eerst conflicten op en probeer opnieuw."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "[INFO] Geen origin/$CHOSEN_BRANCH gevonden; sla git pull over."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Na pull nogmaals checken of er iets open staat --------------------------
|
|
||||||
if [[ -n "$(git status --porcelain)" ]]; then
|
|
||||||
echo "[INFO] Er zijn na git pull nog niet-gecommit-te wijzigingen."
|
|
||||||
auto_commit_if_needed
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Build script uitvoeren --------------------------------------------------
|
|
||||||
if [[ ! -f "./$BUILD_SCRIPT_NAME" ]]; then
|
|
||||||
echo "[ERROR] Build script ./$BUILD_SCRIPT_NAME niet gevonden in $REPO_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
chmod +x "./$BUILD_SCRIPT_NAME"
|
|
||||||
echo "[INFO] Running build: ./$BUILD_SCRIPT_NAME $BUMP_TYPE"
|
|
||||||
"./$BUILD_SCRIPT_NAME" "$BUMP_TYPE"
|
|
||||||
|
|
||||||
echo "[INFO] Done."
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
# Repository Conventions
|
|
||||||
|
|
||||||
## 1. Naming
|
|
||||||
|
|
||||||
- Existing script filenames stay unchanged for documentation compatibility.
|
|
||||||
- New scripts should use clear, stable names and should not be renamed later without a migration plan.
|
|
||||||
|
|
||||||
## 2. Placement
|
|
||||||
|
|
||||||
- Place scripts by platform + domain:
|
|
||||||
- `scripts/m365/...`
|
|
||||||
- `scripts/windows/...`
|
|
||||||
- `scripts/macos/...`
|
|
||||||
- `scripts/content/...`
|
|
||||||
- Place build/deploy automation in `automation/...`.
|
|
||||||
|
|
||||||
## 3. Script Header (minimum)
|
|
||||||
|
|
||||||
Use these fields at the top of each script where possible:
|
|
||||||
|
|
||||||
- Purpose/Synopsis
|
|
||||||
- Requirements (modules, permissions, OS)
|
|
||||||
- Input parameters
|
|
||||||
- Output (file/console)
|
|
||||||
- Example usage
|
|
||||||
|
|
||||||
## 4. Secrets and Variables
|
|
||||||
|
|
||||||
- Do not hardcode secrets.
|
|
||||||
- Use variables, prompts, or a secure vault mechanism.
|
|
||||||
|
|
||||||
## 5. Output and Logging
|
|
||||||
|
|
||||||
- Default output goes to `output/` unless operational requirements dictate otherwise.
|
|
||||||
- Use predictable filenames for exports.
|
|
||||||
|
|
||||||
## 6. Change Policy
|
|
||||||
|
|
||||||
- For file moves: update README and relevant guides.
|
|
||||||
- For any future filename changes: add a temporary compatibility layer (wrapper/alias) and define a deprecation window.
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
# Runbooks Index
|
|
||||||
|
|
||||||
This overview helps update guides after the repository restructuring.
|
|
||||||
|
|
||||||
## Important
|
|
||||||
|
|
||||||
- Legacy script filenames were not changed.
|
|
||||||
- Only folder locations were updated.
|
|
||||||
|
|
||||||
## Script Locations
|
|
||||||
|
|
||||||
- `Get-WindowsAutoPilotInfo.ps1`
|
|
||||||
- New path: `scripts/windows/autopilot/Get-WindowsAutoPilotInfo.ps1`
|
|
||||||
- `Test-SMTPRelay-M365.ps1`
|
|
||||||
- New path: `scripts/m365/smtp/Test-SMTPRelay-M365.ps1`
|
|
||||||
- `Test-SMTPSend-Auth-M365.ps1`
|
|
||||||
- New path: `scripts/m365/smtp/Test-SMTPSend-Auth-M365.ps1`
|
|
||||||
- `Update rmm.ps1`
|
|
||||||
- New path: `scripts/windows/endpoint/Update rmm.ps1`
|
|
||||||
- `get-mailbox-with-permissions.ps1`
|
|
||||||
- New path: `scripts/m365/exchange/get-mailbox-with-permissions.ps1`
|
|
||||||
- `m365-users-mfa.ps1`
|
|
||||||
- New path: `scripts/m365/identity/m365-users-mfa.ps1`
|
|
||||||
- `Get-SharePointPermissions.ps1`
|
|
||||||
- New path: `scripts/m365/sharepoint/get-sharepoint-permissions.ps1`
|
|
||||||
- `schedule_reboot.ps1`
|
|
||||||
- New path: `scripts/windows/endpoint/schedule_reboot.ps1`
|
|
||||||
- `uninstall-MS-Visual-C++-2010.ps1`
|
|
||||||
- New path: `scripts/windows/endpoint/uninstall-MS-Visual-C++-2010.ps1`
|
|
||||||
- `uuid.ps1`
|
|
||||||
- New path: `scripts/windows/endpoint/uuid.ps1`
|
|
||||||
- `serialToAdminPassword.sh`
|
|
||||||
- New path: `scripts/macos/serialToAdminPassword.sh`
|
|
||||||
- `convert-md.bat`
|
|
||||||
- New path: `scripts/content/convert-md.bat`
|
|
||||||
- `build-and-push.sh`
|
|
||||||
- New path: `automation/docker/build-and-push.sh`
|
|
||||||
- `update-and-build.sh`
|
|
||||||
- New path: `automation/docker/update-and-build.sh`
|
|
||||||
|
|
||||||
## Guide Update Checklist
|
|
||||||
|
|
||||||
- Replace references to root-level paths with the new paths above.
|
|
||||||
- Check automated jobs/tasks for hardcoded script locations.
|
|
||||||
- Test at least one script per domain after path updates.
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
@echo off
|
|
||||||
setlocal enabledelayedexpansion
|
|
||||||
|
|
||||||
:: ============================================================
|
|
||||||
:: EPUB Conversiescript - .md naar .epub met Pandoc
|
|
||||||
:: Laatste update: 2025-06-20
|
|
||||||
::
|
|
||||||
:: Changelog:
|
|
||||||
:: - Vervanging van verouderde 'wmic' door PowerShell Get-Date
|
|
||||||
:: - Fallback op datum van vandaag bij lege invoer
|
|
||||||
:: - Overstap van --epub-metadata XML naar --metadata vlaggen
|
|
||||||
:: - Vermeden gebruik van systeemvariabele %DATE%
|
|
||||||
:: - Invoervelden ontdaan van spaties en line breaks via PowerShell .Trim()
|
|
||||||
:: - Gebruik van !VAR! (delayed expansion) om correcte waardes te behouden
|
|
||||||
:: - PowerShell Console.Write gebruikt om onzichtbare tekens zoals '=' of '+' te voorkomen
|
|
||||||
:: - Alle metadata wordt correct opgenomen in EPUB (title, author, publisher, date)
|
|
||||||
:: - Pauzes verwijderd voor geautomatiseerd gebruik, metadata wordt alleen getoond
|
|
||||||
:: - Eindpauze toegevoegd zodat resultaat zichtbaar blijft
|
|
||||||
:: - Spaties toegevoegd bij invoer voor leesbaarheid
|
|
||||||
:: ============================================================
|
|
||||||
|
|
||||||
cd /d "C:\Users\ms-iv\Downloads"
|
|
||||||
|
|
||||||
:: Datum ophalen in jjjj-mm-dd formaat
|
|
||||||
powershell -NoProfile -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = 'en-US'; Get-Date -Format 'yyyy-MM-dd'" > temp_datum.txt
|
|
||||||
set /p TODAY=<temp_datum.txt
|
|
||||||
del temp_datum.txt
|
|
||||||
|
|
||||||
:: Metadata invoer
|
|
||||||
set /p TITLE=Voer de titel van het boek in:
|
|
||||||
set /p AUTHOR=Voer de auteur in:
|
|
||||||
set /p PUBLISHER=Voer de uitgever (publisher) in:
|
|
||||||
set /p BOOKDATEINPUT=Voer de publicatiedatum in (formaat: jjjj-mm-dd of leeg = vandaag):
|
|
||||||
|
|
||||||
:: Invoer normaliseren via PowerShell (verwijdert CR/LF/spaties)
|
|
||||||
for /f %%x in ('powershell -NoProfile -Command "[Console]::Write((\"%BOOKDATEINPUT%\").Trim())"') do set BOOKDATEINPUT=%%x
|
|
||||||
|
|
||||||
:: Boekdatum instellen (fallback op TODAY)
|
|
||||||
if not "!BOOKDATEINPUT!"=="" (
|
|
||||||
set "BOOKDATE=!BOOKDATEINPUT!"
|
|
||||||
) else (
|
|
||||||
set "BOOKDATE=!TODAY!"
|
|
||||||
)
|
|
||||||
|
|
||||||
:: Metadata tonen vóór conversie
|
|
||||||
echo.
|
|
||||||
echo Titel: !TITLE!
|
|
||||||
echo Auteur: !AUTHOR!
|
|
||||||
echo Uitgever: !PUBLISHER!
|
|
||||||
echo Publicatiedatum: !BOOKDATE!
|
|
||||||
echo.
|
|
||||||
|
|
||||||
:: Converteren van elk .md bestand
|
|
||||||
for %%f in (*.md) do (
|
|
||||||
echo Converting "%%f"...
|
|
||||||
pandoc "%%f" -o "%%~nf.epub" ^
|
|
||||||
--metadata=title:"!TITLE!" ^
|
|
||||||
--metadata=author:"!AUTHOR!" ^
|
|
||||||
--metadata=publisher:"!PUBLISHER!" ^
|
|
||||||
--metadata=date:"!BOOKDATE!"
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Alle bestanden zijn geconverteerd.
|
|
||||||
pause
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
# Output file
|
|
||||||
$OutputFile = "MailboxPermissionsWithSMTPAndName.csv"
|
|
||||||
|
|
||||||
# Initialize an array for the results
|
|
||||||
$Results = @()
|
|
||||||
|
|
||||||
# Retrieve all mailboxes (all types)
|
|
||||||
$Mailboxes = Get-Mailbox -RecipientTypeDetails UserMailbox,SharedMailbox -ResultSize Unlimited
|
|
||||||
|
|
||||||
# Loop through all mailboxes
|
|
||||||
foreach ($Mailbox in $Mailboxes) {
|
|
||||||
# Retrieve the primary SMTP address and display name
|
|
||||||
$PrimarySmtpAddress = $Mailbox.PrimarySmtpAddress
|
|
||||||
$DisplayName = $Mailbox.DisplayName
|
|
||||||
|
|
||||||
# Retrieve mailbox permissions
|
|
||||||
$Permissions = Get-MailboxPermission -Identity $Mailbox.Identity
|
|
||||||
|
|
||||||
# Check if specific permissions are set
|
|
||||||
if ($Permissions.Count -eq 0) {
|
|
||||||
# Add mailbox without additional permissions
|
|
||||||
$Results += [PSCustomObject]@{
|
|
||||||
DisplayName = $DisplayName
|
|
||||||
MailboxType = $Mailbox.RecipientTypeDetails
|
|
||||||
PrimarySmtpAddress = $PrimarySmtpAddress
|
|
||||||
User = "Default owner"
|
|
||||||
AccessRights = "FullAccess"
|
|
||||||
IsInherited = "N/A"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
# Add permissions
|
|
||||||
foreach ($Permission in $Permissions) {
|
|
||||||
# Filter default permissions such as NT AUTHORITY\SELF
|
|
||||||
if ($Permission.User -notlike "NT AUTHORITY\SELF") {
|
|
||||||
$Results += [PSCustomObject]@{
|
|
||||||
DisplayName = $DisplayName
|
|
||||||
MailboxType = $Mailbox.RecipientTypeDetails
|
|
||||||
PrimarySmtpAddress = $PrimarySmtpAddress
|
|
||||||
User = $Permission.User
|
|
||||||
AccessRights = $Permission.AccessRights -join ", "
|
|
||||||
IsInherited = $Permission.IsInherited
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add mailboxes that may not have displayed permissions
|
|
||||||
foreach ($Mailbox in $Mailboxes) {
|
|
||||||
if (-not ($Results.DisplayName -contains $Mailbox.DisplayName)) {
|
|
||||||
$Results += [PSCustomObject]@{
|
|
||||||
DisplayName = $Mailbox.DisplayName
|
|
||||||
MailboxType = $Mailbox.RecipientTypeDetails
|
|
||||||
PrimarySmtpAddress = $Mailbox.PrimarySmtpAddress
|
|
||||||
User = "Default owner"
|
|
||||||
AccessRights = "FullAccess"
|
|
||||||
IsInherited = "N/A"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Export the results to a CSV file
|
|
||||||
$Results | Export-Csv -Path $OutputFile -NoTypeInformation -Encoding UTF8
|
|
||||||
|
|
||||||
Write-Host "All mailboxes, permissions, names, and SMTP addresses have been exported to $OutputFile"
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
# Install the MSOnline module if it is not already installed
|
|
||||||
Install-Module -Name MSOnline -Force
|
|
||||||
|
|
||||||
# Connect to the MSOnline service
|
|
||||||
Connect-MsolService
|
|
||||||
|
|
||||||
# Retrieve all users, suppressing any errors by redirecting to $null
|
|
||||||
$AllUsers = Get-MsolUser -All 2>$null
|
|
||||||
|
|
||||||
# Container for MFA information
|
|
||||||
$MFAInfo = @()
|
|
||||||
|
|
||||||
# Loop through all users to check their MFA status
|
|
||||||
foreach ($user in $AllUsers) {
|
|
||||||
# Check if MFA is enabled by looking at StrongAuthenticationRequirements
|
|
||||||
if ($user.StrongAuthenticationRequirements.State -ne $null) {
|
|
||||||
$MFAInfo += [pscustomobject]@{
|
|
||||||
UserPrincipalName = $user.UserPrincipalName # User's UPN (email address)
|
|
||||||
DisplayName = $user.DisplayName # User's display name
|
|
||||||
MFAEnabled = $true # MFA is enabled
|
|
||||||
Method = "Conditional Access/MFA Policy" # MFA policy used
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
# If no MFA policy is found, mark MFA as disabled
|
|
||||||
$MFAInfo += [pscustomobject]@{
|
|
||||||
UserPrincipalName = $user.UserPrincipalName
|
|
||||||
DisplayName = $user.DisplayName
|
|
||||||
MFAEnabled = $false # MFA is not enabled
|
|
||||||
Method = "None" # No MFA method applied
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Define the path for the CSV file
|
|
||||||
$CsvFilePath = "C:\MFA_Overview.csv"
|
|
||||||
|
|
||||||
# Export the MFA data to a CSV file without including type information
|
|
||||||
$MFAInfo | Export-Csv -Path $CsvFilePath -NoTypeInformation
|
|
||||||
|
|
||||||
# Display a message indicating that the export was successful
|
|
||||||
Write-Host "MFA overview successfully exported to $CsvFilePath"
|
|
||||||
@ -1,549 +0,0 @@
|
|||||||
#Requires -Modules Microsoft.Graph.Authentication, ImportExcel
|
|
||||||
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Audits unique SharePoint permissions at library, folder, and file level across all sites.
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Reads site URLs from a CSV export (SharePoint Admin Center) and retrieves all
|
|
||||||
permissions via the Microsoft Graph API. Results are streamed directly to CSV
|
|
||||||
after each site to minimize memory usage. An Excel report is generated at the end.
|
|
||||||
|
|
||||||
=========================================================================
|
|
||||||
REQUIRED AZURE APP REGISTRATION PERMISSIONS
|
|
||||||
=========================================================================
|
|
||||||
Create an App Registration in Azure Portal (Entra ID) with the following
|
|
||||||
APPLICATION permissions (not Delegated) and grant Admin Consent for all:
|
|
||||||
|
|
||||||
Microsoft Graph:
|
|
||||||
- Sites.Read.All : Read all SharePoint site collections and their contents
|
|
||||||
- Files.Read.All : Read all files in all site document libraries
|
|
||||||
- User.Read.All : Resolve user IDs to display names / UPNs
|
|
||||||
- Group.Read.All : Resolve group IDs to display names
|
|
||||||
|
|
||||||
How to set up:
|
|
||||||
1. Go to Azure Portal > Microsoft Entra ID > App Registrations > New Registration
|
|
||||||
2. Give it a name (e.g. "SharePoint Permissions Audit")
|
|
||||||
3. Go to API Permissions > Add a Permission > Microsoft Graph > Application Permissions
|
|
||||||
4. Add: Sites.Read.All, Files.Read.All, User.Read.All, Group.Read.All
|
|
||||||
5. Click "Grant Admin Consent for <tenant>"
|
|
||||||
6. Go to Certificates & Secrets > New Client Secret
|
|
||||||
7. Copy the SECRET VALUE (not the Secret ID) immediately after creation
|
|
||||||
8. Note down the Application (client) ID and the Directory (tenant) ID
|
|
||||||
=========================================================================
|
|
||||||
|
|
||||||
.PARAMETER TenantId
|
|
||||||
Azure AD / Entra ID Tenant ID (found on the App Registration overview page).
|
|
||||||
|
|
||||||
.PARAMETER ClientId
|
|
||||||
Application (Client) ID of the App Registration.
|
|
||||||
|
|
||||||
.PARAMETER ClientSecret
|
|
||||||
Client Secret VALUE (not the Secret ID) of the App Registration.
|
|
||||||
|
|
||||||
.PARAMETER SitesCsvPath
|
|
||||||
Path to the CSV file exported from the SharePoint Admin Center.
|
|
||||||
The CSV must contain a column named 'URL' with the site URLs.
|
|
||||||
Export: SharePoint Admin Center > Active Sites > Export to CSV
|
|
||||||
|
|
||||||
.PARAMETER OutputPath
|
|
||||||
Folder where output files (CSV, Excel, log) will be saved. Defaults to current directory.
|
|
||||||
|
|
||||||
.PARAMETER ExcludedSites
|
|
||||||
Optional array of site URLs to skip during the scan.
|
|
||||||
|
|
||||||
.PARAMETER IncludeFileLevel
|
|
||||||
Switch to enable file-level permission scanning in addition to libraries and folders.
|
|
||||||
Warning: this significantly increases scan time on large tenants.
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
# Scan all sites using the exported CSV
|
|
||||||
.\Get-SharePointPermissions.ps1 `
|
|
||||||
-TenantId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
|
|
||||||
-ClientId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
|
|
||||||
-ClientSecret "your-secret-value-here" `
|
|
||||||
-SitesCsvPath "C:\scripts\Sites.csv" `
|
|
||||||
-OutputPath "C:\scripts\Reports"
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
# Include file-level permissions and skip a specific site
|
|
||||||
.\Get-SharePointPermissions.ps1 `
|
|
||||||
-TenantId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
|
|
||||||
-ClientId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
|
|
||||||
-ClientSecret "your-secret-value-here" `
|
|
||||||
-SitesCsvPath "C:\scripts\Sites.csv" `
|
|
||||||
-OutputPath "C:\scripts\Reports" `
|
|
||||||
-IncludeFileLevel `
|
|
||||||
-ExcludedSites @("https://contoso.sharepoint.com/sites/HR")
|
|
||||||
|
|
||||||
.NOTES
|
|
||||||
Monitor progress: Get-Content "C:\scripts\Reports\SharePoint_Permissions_*.csv" -Wait -Tail 20
|
|
||||||
Do NOT open the CSV in Excel while the script is running - this locks the file.
|
|
||||||
Use Notepad or Notepad++ to view the CSV during the scan.
|
|
||||||
#>
|
|
||||||
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$TenantId,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$ClientId,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$ClientSecret,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$SitesCsvPath,
|
|
||||||
|
|
||||||
[string]$OutputPath = (Get-Location).Path,
|
|
||||||
|
|
||||||
[string[]]$ExcludedSites = @(),
|
|
||||||
|
|
||||||
[switch]$IncludeFileLevel
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
#region ── Module check ──────────────────────────────────────────────────────
|
|
||||||
# Ensure required PowerShell modules are installed and loaded.
|
|
||||||
# Microsoft.Graph.Authentication : Connect to Graph API and make REST calls
|
|
||||||
# ImportExcel : Generate formatted Excel reports without Excel installed
|
|
||||||
|
|
||||||
foreach ($mod in @('Microsoft.Graph.Authentication', 'ImportExcel')) {
|
|
||||||
if (-not (Get-Module -ListAvailable -Name $mod)) {
|
|
||||||
Write-Host "[SETUP] Installing module '$mod'..." -ForegroundColor Yellow
|
|
||||||
Install-Module $mod -Scope CurrentUser -Force -AllowClobber
|
|
||||||
}
|
|
||||||
Import-Module $mod -ErrorAction Stop
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ── Output file paths ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
|
|
||||||
$csvOut = Join-Path $OutputPath "SharePoint_Permissions_$timestamp.csv"
|
|
||||||
$xlsxOut = Join-Path $OutputPath "SharePoint_Permissions_$timestamp.xlsx"
|
|
||||||
$logOut = Join-Path $OutputPath "SharePoint_Permissions_$timestamp.log"
|
|
||||||
|
|
||||||
if (-not (Test-Path $OutputPath)) {
|
|
||||||
New-Item -ItemType Directory -Path $OutputPath | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ── Logging ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function Write-Log {
|
|
||||||
param([string]$Message, [string]$Level = 'INFO')
|
|
||||||
$entry = "[{0}] [{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message
|
|
||||||
Add-Content -Path $logOut -Value $entry
|
|
||||||
switch ($Level) {
|
|
||||||
'ERROR' { Write-Host $entry -ForegroundColor Red }
|
|
||||||
'WARNING' { Write-Host $entry -ForegroundColor Yellow }
|
|
||||||
'SUCCESS' { Write-Host $entry -ForegroundColor Green }
|
|
||||||
default { Write-Host $entry }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ── Authentication ────────────────────────────────────────────────────
|
|
||||||
# Uses the OAuth2 client credentials flow directly via Invoke-RestMethod.
|
|
||||||
# This avoids MSAL.PS compatibility issues with PowerShell strict mode.
|
|
||||||
|
|
||||||
Write-Log "Script started."
|
|
||||||
|
|
||||||
try {
|
|
||||||
$tokenBody = @{
|
|
||||||
grant_type = 'client_credentials'
|
|
||||||
client_id = $ClientId
|
|
||||||
client_secret = $ClientSecret
|
|
||||||
scope = 'https://graph.microsoft.com/.default'
|
|
||||||
}
|
|
||||||
$tokenResponse = Invoke-RestMethod `
|
|
||||||
-Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" `
|
|
||||||
-Method POST -Body $tokenBody -ContentType 'application/x-www-form-urlencoded'
|
|
||||||
|
|
||||||
Connect-MgGraph `
|
|
||||||
-AccessToken ($tokenResponse.access_token | ConvertTo-SecureString -AsPlainText -Force) `
|
|
||||||
-NoWelcome
|
|
||||||
|
|
||||||
Write-Log "Authentication successful." -Level 'SUCCESS'
|
|
||||||
} catch {
|
|
||||||
Write-Log "Authentication failed: $_" -Level 'ERROR'
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ── Graph REST helper with automatic pagination ───────────────────────
|
|
||||||
# Graph API returns results in pages (default 200 items per page).
|
|
||||||
# This function follows @odata.nextLink until all results are retrieved.
|
|
||||||
|
|
||||||
function Invoke-GraphGet {
|
|
||||||
param([string]$Uri)
|
|
||||||
$results = [System.Collections.Generic.List[object]]::new()
|
|
||||||
$nextUri = $Uri
|
|
||||||
while ($nextUri) {
|
|
||||||
$resp = Invoke-MgGraphRequest -Uri $nextUri -Method GET -OutputType PSObject
|
|
||||||
$nextUri = $null
|
|
||||||
if ($resp.PSObject.Properties.Name -contains '@odata.nextLink') {
|
|
||||||
$nextUri = $resp.'@odata.nextLink'
|
|
||||||
}
|
|
||||||
if ($resp.PSObject.Properties.Name -contains 'value') {
|
|
||||||
foreach ($item in $resp.value) { $results.Add($item) }
|
|
||||||
} elseif ($null -ne $resp) {
|
|
||||||
$results.Add($resp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $results
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ── Recursive drive item retrieval ────────────────────────────────────
|
|
||||||
# Recursively fetches all folders (and optionally files) within a drive.
|
|
||||||
# Recurses into subfolders to build a complete tree.
|
|
||||||
|
|
||||||
function Get-AllDriveItems {
|
|
||||||
param([string]$DriveId, [string]$ItemId = 'root')
|
|
||||||
$all = [System.Collections.Generic.List[object]]::new()
|
|
||||||
try {
|
|
||||||
$children = Invoke-GraphGet -Uri "https://graph.microsoft.com/v1.0/drives/$DriveId/items/$ItemId/children"
|
|
||||||
foreach ($child in $children) {
|
|
||||||
$all.Add($child)
|
|
||||||
if ($child.PSObject.Properties.Name -contains 'folder') {
|
|
||||||
$sub = Get-AllDriveItems -DriveId $DriveId -ItemId $child.id
|
|
||||||
foreach ($s in $sub) { $all.Add($s) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Log " Error retrieving children of '$ItemId': $_" -Level 'WARNING'
|
|
||||||
}
|
|
||||||
return $all
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ── Principal name lookup with caching ────────────────────────────────
|
|
||||||
# Resolves Azure AD user/group IDs to readable display names.
|
|
||||||
# Results are cached to avoid duplicate API calls for the same principal.
|
|
||||||
|
|
||||||
$principalCache = @{}
|
|
||||||
|
|
||||||
function Get-PrincipalName {
|
|
||||||
param([string]$Id, [string]$Type)
|
|
||||||
if (-not $Id) { return 'Unknown' }
|
|
||||||
$key = "$Type|$Id"
|
|
||||||
if ($principalCache.ContainsKey($key)) { return $principalCache[$key] }
|
|
||||||
$name = "$Type`: $Id"
|
|
||||||
try {
|
|
||||||
if ($Type -eq 'user') {
|
|
||||||
$u = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/users/${Id}?`$select=displayName,userPrincipalName" -Method GET -OutputType PSObject
|
|
||||||
$name = "$($u.displayName) ($($u.userPrincipalName))"
|
|
||||||
} elseif ($Type -eq 'group') {
|
|
||||||
$g = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/groups/${Id}?`$select=displayName" -Method GET -OutputType PSObject
|
|
||||||
$name = $g.displayName
|
|
||||||
}
|
|
||||||
} catch { }
|
|
||||||
$principalCache[$key] = $name
|
|
||||||
return $name
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ── Permission record builder ─────────────────────────────────────────
|
|
||||||
# Retrieves permissions for a single drive item and returns structured records.
|
|
||||||
# ForceInclude: always return records even if only inherited permissions exist (used for library root).
|
|
||||||
# Without ForceInclude: only returns records if explicit (non-inherited) grants are found.
|
|
||||||
|
|
||||||
function Get-ItemPermissionRecords {
|
|
||||||
param(
|
|
||||||
[string]$DriveId,
|
|
||||||
[string]$ItemId,
|
|
||||||
[string]$ItemName,
|
|
||||||
[string]$ItemPath,
|
|
||||||
[string]$ItemType, # 'Library', 'Folder', or 'File'
|
|
||||||
[string]$SiteName,
|
|
||||||
[string]$SiteUrl,
|
|
||||||
[string]$LibraryName,
|
|
||||||
[switch]$ForceInclude
|
|
||||||
)
|
|
||||||
|
|
||||||
$records = [System.Collections.Generic.List[object]]::new()
|
|
||||||
|
|
||||||
try {
|
|
||||||
$perms = Invoke-GraphGet -Uri "https://graph.microsoft.com/v1.0/drives/$DriveId/items/$ItemId/permissions"
|
|
||||||
} catch {
|
|
||||||
Write-Log " Error retrieving permissions for '$ItemPath': $_" -Level 'WARNING'
|
|
||||||
return $records
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $perms -or $perms.Count -eq 0) { return $records }
|
|
||||||
|
|
||||||
# Only include items with explicit direct grants (not just sharing links), unless ForceInclude
|
|
||||||
$hasExplicitGrants = $perms | Where-Object {
|
|
||||||
($_.PSObject.Properties.Name -contains 'grantedToV2' -and $_.grantedToV2) -or
|
|
||||||
($_.PSObject.Properties.Name -contains 'grantedTo' -and $_.grantedTo)
|
|
||||||
}
|
|
||||||
if (-not $ForceInclude -and -not $hasExplicitGrants) { return $records }
|
|
||||||
|
|
||||||
foreach ($perm in $perms) {
|
|
||||||
$roles = if ($perm.PSObject.Properties.Name -contains 'roles' -and $perm.roles) { $perm.roles -join ', ' } else { 'read' }
|
|
||||||
$grantedTo = 'Unknown'
|
|
||||||
$linkType = ''
|
|
||||||
|
|
||||||
# Resolve who the permission is granted to (Graph API v2 properties take priority)
|
|
||||||
if ($perm.PSObject.Properties.Name -contains 'grantedToV2' -and $perm.grantedToV2) {
|
|
||||||
$g = $perm.grantedToV2
|
|
||||||
if ($g.PSObject.Properties.Name -contains 'user' -and $g.user) { $grantedTo = Get-PrincipalName -Id $g.user.id -Type 'user' }
|
|
||||||
elseif ($g.PSObject.Properties.Name -contains 'group' -and $g.group) { $grantedTo = Get-PrincipalName -Id $g.group.id -Type 'group' }
|
|
||||||
elseif ($g.PSObject.Properties.Name -contains 'siteUser' -and $g.siteUser) { $grantedTo = $g.siteUser.displayName }
|
|
||||||
elseif ($g.PSObject.Properties.Name -contains 'siteGroup' -and $g.siteGroup) { $grantedTo = $g.siteGroup.displayName }
|
|
||||||
} elseif ($perm.PSObject.Properties.Name -contains 'grantedToIdentitiesV2' -and $perm.grantedToIdentitiesV2) {
|
|
||||||
# Multiple recipients (e.g. sharing link used by multiple people)
|
|
||||||
$names = foreach ($identity in $perm.grantedToIdentitiesV2) {
|
|
||||||
if ($identity.PSObject.Properties.Name -contains 'user' -and $identity.user) { Get-PrincipalName -Id $identity.user.id -Type 'user' }
|
|
||||||
elseif ($identity.PSObject.Properties.Name -contains 'group' -and $identity.group) { Get-PrincipalName -Id $identity.group.id -Type 'group' }
|
|
||||||
elseif ($identity.PSObject.Properties.Name -contains 'siteUser' -and $identity.siteUser) { $identity.siteUser.displayName }
|
|
||||||
else { 'Unknown' }
|
|
||||||
}
|
|
||||||
$grantedTo = ($names | Where-Object { $_ }) -join '; '
|
|
||||||
} elseif ($perm.PSObject.Properties.Name -contains 'grantedTo' -and $perm.grantedTo) {
|
|
||||||
# Fallback: older grantedTo property
|
|
||||||
$g = $perm.grantedTo
|
|
||||||
if ($g.PSObject.Properties.Name -contains 'user' -and $g.user) { $grantedTo = "$($g.user.displayName) ($($g.user.email))" }
|
|
||||||
elseif ($g.PSObject.Properties.Name -contains 'group' -and $g.group) { $grantedTo = $g.group.displayName }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Detect sharing links (anonymous, organization-wide, or specific people links)
|
|
||||||
if ($perm.PSObject.Properties.Name -contains 'link' -and $perm.link) {
|
|
||||||
$linkType = "SharingLink ($($perm.link.type), $($perm.link.scope))"
|
|
||||||
if ($grantedTo -eq 'Unknown') { $grantedTo = $linkType }
|
|
||||||
}
|
|
||||||
|
|
||||||
$records.Add([PSCustomObject]@{
|
|
||||||
SiteName = $SiteName
|
|
||||||
SiteUrl = $SiteUrl
|
|
||||||
Library = $LibraryName
|
|
||||||
ItemType = $ItemType
|
|
||||||
ItemName = $ItemName
|
|
||||||
ItemPath = $ItemPath
|
|
||||||
GrantedTo = $grantedTo
|
|
||||||
Permissions = $roles
|
|
||||||
LinkType = $linkType
|
|
||||||
ScannedAt = (Get-Date -Format 'yyyy-MM-dd HH:mm')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return $records
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ── Load site list from CSV ───────────────────────────────────────────
|
|
||||||
# Reads site URLs from the SharePoint Admin Center export CSV.
|
|
||||||
# Export location: SharePoint Admin Center > Active Sites > Export
|
|
||||||
|
|
||||||
Write-Log "Loading sites from CSV: $SitesCsvPath"
|
|
||||||
|
|
||||||
if (-not (Test-Path $SitesCsvPath)) {
|
|
||||||
Write-Log "CSV file not found: $SitesCsvPath" -Level 'ERROR'
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$csvSites = Import-Csv -Path $SitesCsvPath
|
|
||||||
$urlColumn = $csvSites[0].PSObject.Properties.Name | Where-Object { $_ -match '^url$' } | Select-Object -First 1
|
|
||||||
|
|
||||||
if (-not $urlColumn) {
|
|
||||||
Write-Log "No 'URL' column found in CSV. Available columns: $($csvSites[0].PSObject.Properties.Name -join ', ')" -Level 'ERROR'
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$siteUrls = $csvSites.$urlColumn | Where-Object {
|
|
||||||
$_ -and $_ -notmatch '/personal/' -and $_ -notin $ExcludedSites
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log "Sites loaded from CSV: $($siteUrls.Count)" -Level 'SUCCESS'
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ── Main scan loop ────────────────────────────────────────────────────
|
|
||||||
# Streams permission records directly to CSV after each site to keep memory usage low.
|
|
||||||
# The CSV file is written incrementally - do NOT open it in Excel during the scan.
|
|
||||||
|
|
||||||
$csvHeaders = 'SiteName;SiteUrl;Library;ItemType;ItemName;ItemPath;GrantedTo;Permissions;LinkType;ScannedAt'
|
|
||||||
Set-Content -Path $csvOut -Value $csvHeaders -Encoding UTF8
|
|
||||||
$recordCount = 0
|
|
||||||
|
|
||||||
function Write-RecordToCsv {
|
|
||||||
# Appends one or more permission records to the CSV file immediately.
|
|
||||||
param([object[]]$Records)
|
|
||||||
foreach ($rec in $Records) {
|
|
||||||
$line = '"{0}";"{1}";"{2}";"{3}";"{4}";"{5}";"{6}";"{7}";"{8}";"{9}"' -f `
|
|
||||||
$rec.SiteName, $rec.SiteUrl, $rec.Library, $rec.ItemType, `
|
|
||||||
$rec.ItemName, $rec.ItemPath, $rec.GrantedTo, $rec.Permissions, `
|
|
||||||
$rec.LinkType, $rec.ScannedAt
|
|
||||||
Add-Content -Path $csvOut -Value $line -Encoding UTF8
|
|
||||||
$script:recordCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$siteIndex = 0
|
|
||||||
$total = $siteUrls.Count
|
|
||||||
|
|
||||||
foreach ($siteUrl in $siteUrls) {
|
|
||||||
$siteIndex++
|
|
||||||
$pct = [math]::Round(($siteIndex / $total) * 100)
|
|
||||||
Write-Progress -Activity "SharePoint Permissions Audit" `
|
|
||||||
-Status "[$siteIndex/$total] $siteUrl | Total records: $recordCount" `
|
|
||||||
-PercentComplete $pct
|
|
||||||
|
|
||||||
# Resolve site URL to Graph site ID using the hostname:/path format
|
|
||||||
try {
|
|
||||||
$uri = [Uri]$siteUrl
|
|
||||||
$hostPart = $uri.Host
|
|
||||||
$pathPart = $uri.AbsolutePath.TrimStart('/')
|
|
||||||
if ($pathPart) {
|
|
||||||
$siteObj = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/sites/${hostPart}:/${pathPart}" -Method GET -OutputType PSObject
|
|
||||||
} else {
|
|
||||||
$siteObj = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/sites/${hostPart}" -Method GET -OutputType PSObject
|
|
||||||
}
|
|
||||||
$siteId = $siteObj.id
|
|
||||||
$siteName = if ($siteObj.PSObject.Properties.Name -contains 'displayName' -and $siteObj.displayName) { $siteObj.displayName } else { $siteObj.name }
|
|
||||||
} catch {
|
|
||||||
Write-Log "[$siteIndex/$total] Failed to resolve site ID for '$siteUrl': $_" -Level 'WARNING'
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log "[$siteIndex/$total] $siteName"
|
|
||||||
|
|
||||||
# Get all document libraries for this site (excludes system lists)
|
|
||||||
try {
|
|
||||||
$drives = Invoke-GraphGet -Uri "https://graph.microsoft.com/v1.0/sites/$siteId/drives"
|
|
||||||
$drives = $drives | Where-Object { $_.driveType -eq 'documentLibrary' }
|
|
||||||
} catch {
|
|
||||||
Write-Log " Error retrieving document libraries: $_" -Level 'WARNING'
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
$recordsBefore = $recordCount
|
|
||||||
|
|
||||||
foreach ($drive in $drives) {
|
|
||||||
$libName = $drive.name
|
|
||||||
$driveId = $drive.id
|
|
||||||
Write-Log " Library: $libName"
|
|
||||||
|
|
||||||
# Scan library root permissions (always included)
|
|
||||||
try {
|
|
||||||
$root = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/drives/$driveId/root" -Method GET -OutputType PSObject
|
|
||||||
$records = Get-ItemPermissionRecords -DriveId $driveId -ItemId $root.id `
|
|
||||||
-ItemName $libName -ItemPath "/" -ItemType 'Library' `
|
|
||||||
-SiteName $siteName -SiteUrl $siteUrl -LibraryName $libName -ForceInclude
|
|
||||||
Write-RecordToCsv -Records $records
|
|
||||||
} catch {
|
|
||||||
Write-Log " Error scanning library root '$libName': $_" -Level 'WARNING'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Scan all folders (and files if -IncludeFileLevel is set)
|
|
||||||
# Only items with unique (broken) permissions are included
|
|
||||||
try {
|
|
||||||
$items = Get-AllDriveItems -DriveId $driveId
|
|
||||||
foreach ($item in $items) {
|
|
||||||
$isFolder = $item.PSObject.Properties.Name -contains 'folder'
|
|
||||||
$isFile = $item.PSObject.Properties.Name -contains 'file'
|
|
||||||
if (-not $isFolder -and -not ($IncludeFileLevel -and $isFile)) { continue }
|
|
||||||
|
|
||||||
$itemType = if ($isFolder) { 'Folder' } else { 'File' }
|
|
||||||
$parentPath = ''
|
|
||||||
if ($item.PSObject.Properties.Name -contains 'parentReference' -and
|
|
||||||
$item.parentReference.PSObject.Properties.Name -contains 'path') {
|
|
||||||
$parentPath = $item.parentReference.path -replace '.*?/root:', ''
|
|
||||||
}
|
|
||||||
$cleanPath = "$parentPath/$($item.name)"
|
|
||||||
|
|
||||||
$records = Get-ItemPermissionRecords -DriveId $driveId -ItemId $item.id `
|
|
||||||
-ItemName $item.name -ItemPath $cleanPath -ItemType $itemType `
|
|
||||||
-SiteName $siteName -SiteUrl $siteUrl -LibraryName $libName
|
|
||||||
Write-RecordToCsv -Records $records
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Log " Error scanning items in '$libName': $_" -Level 'WARNING'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$added = $recordCount - $recordsBefore
|
|
||||||
if ($added -gt 0) {
|
|
||||||
Write-Log " -> $added new records written (total: $recordCount)" -Level 'SUCCESS'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Progress -Activity "SharePoint Permissions Audit" -Completed
|
|
||||||
Write-Log "Scan complete. $recordCount permission records found." -Level 'SUCCESS'
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ── Build Excel report from CSV ───────────────────────────────────────
|
|
||||||
# Reads the completed CSV and generates a multi-sheet Excel report.
|
|
||||||
# Sheet 1 - All Permissions : full dataset
|
|
||||||
# Sheet 2 - Summary by Site : record counts grouped per site
|
|
||||||
# Sheet 3 - Top 20 Risk Items: items with the most unique permission grants
|
|
||||||
# Sheet 4 - Sharing Links : all items shared via external/anonymous links
|
|
||||||
|
|
||||||
Write-Log "Building Excel report from CSV..."
|
|
||||||
|
|
||||||
if ($recordCount -gt 0) {
|
|
||||||
$allRecords = Import-Csv -Path $csvOut -Delimiter ';' -Encoding UTF8
|
|
||||||
|
|
||||||
# Sheet 1: Full permission list
|
|
||||||
$allRecords | Export-Excel -Path $xlsxOut -WorksheetName 'All Permissions' `
|
|
||||||
-TableName 'TblAllPermissions' -TableStyle Medium9 -AutoSize -FreezeTopRow -BoldTopRow
|
|
||||||
|
|
||||||
# Sheet 2: Summary grouped by site
|
|
||||||
$allRecords | Group-Object SiteName | Select-Object `
|
|
||||||
@{N='Site'; E={$_.Name}},
|
|
||||||
@{N='Total'; E={$_.Count}},
|
|
||||||
@{N='Libraries'; E={($_.Group.Library | Sort-Object -Unique).Count}},
|
|
||||||
@{N='Folders'; E={($_.Group | Where-Object ItemType -eq 'Folder').Count}},
|
|
||||||
@{N='Files'; E={($_.Group | Where-Object ItemType -eq 'File').Count}} |
|
|
||||||
Sort-Object Total -Descending |
|
|
||||||
Export-Excel -Path $xlsxOut -WorksheetName 'Summary by Site' `
|
|
||||||
-TableName 'TblSummary' -TableStyle Medium2 -AutoSize -FreezeTopRow -BoldTopRow -Append
|
|
||||||
|
|
||||||
# Sheet 3: Top 20 items with most unique grants (highest permission fragmentation risk)
|
|
||||||
$allRecords | Group-Object ItemPath | Select-Object `
|
|
||||||
@{N='Site'; E={($_.Group | Select-Object -First 1).SiteName}},
|
|
||||||
@{N='Library'; E={($_.Group | Select-Object -First 1).Library}},
|
|
||||||
@{N='Type'; E={($_.Group | Select-Object -First 1).ItemType}},
|
|
||||||
@{N='Item'; E={($_.Group | Select-Object -First 1).ItemName}},
|
|
||||||
@{N='Path'; E={($_.Group | Select-Object -First 1).ItemPath}},
|
|
||||||
@{N='Unique Grants'; E={$_.Count}} |
|
|
||||||
Sort-Object 'Unique Grants' -Descending | Select-Object -First 20 |
|
|
||||||
Export-Excel -Path $xlsxOut -WorksheetName 'Top 20 Risk Items' `
|
|
||||||
-TableName 'TblTopRisk' -TableStyle Medium6 -AutoSize -FreezeTopRow -BoldTopRow -Append
|
|
||||||
|
|
||||||
# Sheet 4: Sharing links (potential external exposure)
|
|
||||||
$links = @($allRecords | Where-Object { $_.LinkType -ne '' })
|
|
||||||
if ($links.Count -gt 0) {
|
|
||||||
$links | Export-Excel -Path $xlsxOut -WorksheetName 'Sharing Links' `
|
|
||||||
-TableName 'TblSharingLinks' -TableStyle Medium3 -AutoSize -FreezeTopRow -BoldTopRow -Append
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log "Excel report saved: $xlsxOut" -Level 'SUCCESS'
|
|
||||||
} else {
|
|
||||||
Write-Log "No records found - Excel report not created." -Level 'WARNING'
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
Disconnect-MgGraph | Out-Null
|
|
||||||
|
|
||||||
Write-Host "`n========================================" -ForegroundColor Cyan
|
|
||||||
Write-Host " Scan complete!" -ForegroundColor Cyan
|
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
|
||||||
Write-Host " Records : $recordCount"
|
|
||||||
Write-Host " CSV : $csvOut"
|
|
||||||
Write-Host " Excel : $xlsxOut"
|
|
||||||
Write-Host " Log : $logOut"
|
|
||||||
Write-Host "========================================`n" -ForegroundColor Cyan
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Test unauthenticated SMTP relay via Microsoft 365 using MX record and IP-based connector.
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
This script sends a test email through Microsoft 365 (Exchange Online) using a connector
|
|
||||||
that allows mail relay based on the sender's IP address. It does not require authentication
|
|
||||||
and is useful for testing printers, scanners, or any other device that cannot use modern SMTP auth.
|
|
||||||
|
|
||||||
.AUTHOR
|
|
||||||
Generic / Public version (sanitized for open-source use)
|
|
||||||
|
|
||||||
.NOTES
|
|
||||||
Requires that your public IP is allowed in an Exchange Online connector.
|
|
||||||
Use the MX hostname (e.g. yourdomain-nl.mail.protection.outlook.com) as the SMTP endpoint.
|
|
||||||
|
|
||||||
# CHANGELOG
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
Date | Version | Author | Description
|
|
||||||
------------|---------|----------------|------------------------------------------
|
|
||||||
2025-07-18 | 1.0.0 | Public / GPT | Initial release for generic SMTP relay test
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
#>
|
|
||||||
|
|
||||||
# Variables (edit to match your domain and connector setup)
|
|
||||||
$from = "relay@yourdomain.com"
|
|
||||||
$to = "relay@yourdomain.com"
|
|
||||||
$smtpServer = "yourdomain-nl.mail.protection.outlook.com" # Replace with your actual MX record
|
|
||||||
$smtpPort = 25
|
|
||||||
$subject = "SMTP Relay Test via Microsoft 365 Connector"
|
|
||||||
$body = "This message was sent without authentication using an IP-allowed connector."
|
|
||||||
|
|
||||||
# Create email message
|
|
||||||
$message = New-Object system.net.mail.mailmessage
|
|
||||||
$message.From = $from
|
|
||||||
$message.To.Add($to)
|
|
||||||
$message.Subject = $subject
|
|
||||||
$message.Body = $body
|
|
||||||
$message.IsBodyHtml = $false
|
|
||||||
|
|
||||||
# Configure SMTP client (unauthenticated, no TLS)
|
|
||||||
$smtp = New-Object Net.Mail.SmtpClient($smtpServer, $smtpPort)
|
|
||||||
$smtp.EnableSsl = $false
|
|
||||||
$smtp.DeliveryMethod = "Network"
|
|
||||||
$smtp.UseDefaultCredentials = $true
|
|
||||||
|
|
||||||
Write-Host "⏳ Attempting SMTP relay via $smtpServer..."
|
|
||||||
|
|
||||||
try {
|
|
||||||
$smtp.Send($message)
|
|
||||||
Write-Host "`n✅ Message successfully sent via connector."
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host "`n❌ Failed to send mail:"
|
|
||||||
Write-Host $_.Exception.Message
|
|
||||||
if ($_.Exception.InnerException) {
|
|
||||||
Write-Host "`nDetails:"
|
|
||||||
Write-Host $_.Exception.InnerException.Message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Test script to verify SMTP (Microsoft 365) connectivity using an app password.
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
This script attempts to send a test message using Authenticated SMTP with Microsoft 365.
|
|
||||||
Useful for diagnosing SMTP authentication issues with accounts that have MFA enabled.
|
|
||||||
|
|
||||||
.AUTHOR
|
|
||||||
Generic / Public version (original author removed for portability)
|
|
||||||
|
|
||||||
.NOTES
|
|
||||||
This script is intended for test purposes and should not be used in production automation.
|
|
||||||
|
|
||||||
# CHANGELOG
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
Date | Version | Author | Description
|
|
||||||
------------|---------|---------------|------------------------------------------
|
|
||||||
2025-07-18 | 1.0.0 | Public/GPT | Initial version - generic SMTP test script
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
#>
|
|
||||||
|
|
||||||
Clear-Host
|
|
||||||
|
|
||||||
# SMTP settings
|
|
||||||
$smtpServer = "smtp.office365.com"
|
|
||||||
$smtpPort = 587
|
|
||||||
$from = "youruser@yourdomain.com"
|
|
||||||
$to = "youruser@yourdomain.com"
|
|
||||||
$subject = "SMTP Test"
|
|
||||||
$body = "This is a test message sent via smtp.office365.com with an app password."
|
|
||||||
|
|
||||||
# Secure password prompt
|
|
||||||
$securePassword = Read-Host "Enter your app password" -AsSecureString
|
|
||||||
$credential = New-Object System.Management.Automation.PSCredential($from, $securePassword)
|
|
||||||
|
|
||||||
# Create email
|
|
||||||
$mail = New-Object system.net.mail.mailmessage
|
|
||||||
$mail.From = $from
|
|
||||||
$mail.To.Add($to)
|
|
||||||
$mail.Subject = $subject
|
|
||||||
$mail.Body = $body
|
|
||||||
$mail.IsBodyHtml = $false
|
|
||||||
|
|
||||||
# Create SMTP client
|
|
||||||
$smtp = New-Object Net.Mail.SmtpClient($smtpServer, $smtpPort)
|
|
||||||
$smtp.EnableSsl = $true
|
|
||||||
$smtp.Credentials = $credential
|
|
||||||
$smtp.Timeout = 10000
|
|
||||||
|
|
||||||
Write-Host "⏳ Sending mail via ${smtpServer}:${smtpPort} as ${from}..."
|
|
||||||
|
|
||||||
try {
|
|
||||||
$smtp.Send($mail)
|
|
||||||
Write-Host "`n✅ Message sent successfully to $to"
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host "`n❌ Failed to send message:"
|
|
||||||
Write-Host $_.Exception.Message
|
|
||||||
if ($_.Exception.InnerException) {
|
|
||||||
Write-Host "`nDetails:"
|
|
||||||
Write-Host $_.Exception.InnerException.Message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#set -x
|
|
||||||
|
|
||||||
############################################################################################
|
|
||||||
##
|
|
||||||
## Script to generate Admin Password from Serial Number
|
|
||||||
##
|
|
||||||
###########################################
|
|
||||||
|
|
||||||
## Copyright (c) 2020 Microsoft Corp. All rights reserved.
|
|
||||||
## Scripts are not supported under any Microsoft standard support program or service. The scripts are provided AS IS without warranty of any kind.
|
|
||||||
## Microsoft disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a
|
|
||||||
## particular purpose. The entire risk arising out of the use or performance of the scripts and documentation remains with you. In no event shall
|
|
||||||
## Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever
|
|
||||||
## (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary
|
|
||||||
## loss) arising out of the use of or inability to use the sample scripts or documentation, even if Microsoft has been advised of the possibility
|
|
||||||
## of such damages.
|
|
||||||
## Feedback: neiljohn@microsoft.com
|
|
||||||
|
|
||||||
##
|
|
||||||
## Notes
|
|
||||||
##
|
|
||||||
## This script is to support createAdminAccount.sh the cipher used in that script must match the cipher used here to generate the correct password
|
|
||||||
## i.e. ABCDEF000009 becomes S0xNTk9QNDQ0NDQzCg==
|
|
||||||
##
|
|
||||||
## WARNING: It is strongly recommended to change the cipher on line 45 before deploying into production
|
|
||||||
|
|
||||||
echo -ne "Enter device serial number :"
|
|
||||||
read serial
|
|
||||||
password=`echo $serial | tr '[A-Z]' '[K-ZA-J]' | tr 0-9 4-90-3 | base64`
|
|
||||||
echo "Password: $password"
|
|
||||||
@ -1,190 +0,0 @@
|
|||||||
<#PSScriptInfo
|
|
||||||
|
|
||||||
.VERSION 1.3
|
|
||||||
|
|
||||||
.GUID ebf446a3-3362-4774-83c0-b7299410b63f
|
|
||||||
|
|
||||||
.AUTHOR Michael Niehaus
|
|
||||||
|
|
||||||
.COMPANYNAME Microsoft
|
|
||||||
|
|
||||||
.COPYRIGHT
|
|
||||||
|
|
||||||
.TAGS Windows AutoPilot
|
|
||||||
|
|
||||||
.LICENSEURI
|
|
||||||
|
|
||||||
.PROJECTURI
|
|
||||||
|
|
||||||
.ICONURI
|
|
||||||
|
|
||||||
.EXTERNALMODULEDEPENDENCIES
|
|
||||||
|
|
||||||
.REQUIREDSCRIPTS
|
|
||||||
|
|
||||||
.EXTERNALSCRIPTDEPENDENCIES
|
|
||||||
|
|
||||||
.RELEASENOTES
|
|
||||||
Version 1.0: Original published version.
|
|
||||||
Version 1.1: Added -Append switch.
|
|
||||||
Version 1.2: Added -Credential switch.
|
|
||||||
Version 1.3: Added -Partner switch.
|
|
||||||
|
|
||||||
#>
|
|
||||||
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Retrieves the Windows AutoPilot deployment details from one or more computers
|
|
||||||
.DESCRIPTION
|
|
||||||
This script uses WMI to retrieve properties needed by the Microsoft Store for Business to support Windows AutoPilot deployment.
|
|
||||||
.PARAMETER Name
|
|
||||||
The names of the computers. These can be provided via the pipeline (property name Name or one of the available aliases, DNSHostName, ComputerName, and Computer).
|
|
||||||
.PARAMETER OutputFile
|
|
||||||
The name of the CSV file to be created with the details for the computers. If not specified, the details will be returned to the PowerShell
|
|
||||||
pipeline.
|
|
||||||
.PARAMETER Append
|
|
||||||
Switch to specify that new computer details should be appended to the specified output file, instead of overwriting the existing file.
|
|
||||||
.PARAMETER Credential
|
|
||||||
Credentials that should be used when connecting to a remote computer (not supported when gathering details from the local computer).
|
|
||||||
.PARAMETER Partner
|
|
||||||
Switch to specify that the created CSV file should use the schema for Partner Center (using serial number, make, and model).
|
|
||||||
.EXAMPLE
|
|
||||||
.\Get-WindowsAutoPilotInfo.ps1 -ComputerName MYCOMPUTER -OutputFile .\MyComputer.csv
|
|
||||||
.EXAMPLE
|
|
||||||
.\Get-WindowsAutoPilotInfo.ps1 -ComputerName MYCOMPUTER -OutputFile .\MyComputer.csv -Append
|
|
||||||
.EXAMPLE
|
|
||||||
.\Get-WindowsAutoPilotInfo.ps1 -ComputerName MYCOMPUTER1,MYCOMPUTER2 -OutputFile .\MyComputers.csv
|
|
||||||
.EXAMPLE
|
|
||||||
Get-ADComputer -Filter * | .\GetWindowsAutoPilotInfo.ps1 -OutputFile .\MyComputers.csv
|
|
||||||
.EXAMPLE
|
|
||||||
Get-CMCollectionMember -CollectionName "All Systems" | .\GetWindowsAutoPilotInfo.ps1 -OutputFile .\MyComputers.csv
|
|
||||||
.EXAMPLE
|
|
||||||
.\Get-WindowsAutoPilotInfo.ps1 -ComputerName MYCOMPUTER1,MYCOMPUTER2 -OutputFile .\MyComputers.csv -Partner
|
|
||||||
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$False,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True,Position=0)][alias("DNSHostName","ComputerName","Computer")] [String[]] $Name = @($env:ComputerName),
|
|
||||||
[Parameter(Mandatory=$False)] [String] $OutputFile = "",
|
|
||||||
[Parameter(Mandatory=$False)] [Switch] $Append = $false,
|
|
||||||
[Parameter(Mandatory=$False)] [System.Management.Automation.PSCredential] $Credential = $null,
|
|
||||||
[Parameter(Mandatory=$False)] [Switch] $Partner = $false,
|
|
||||||
[Parameter(Mandatory=$False)] [Switch] $Force = $false
|
|
||||||
)
|
|
||||||
|
|
||||||
Begin
|
|
||||||
{
|
|
||||||
# Initialize empty list
|
|
||||||
$computers = @()
|
|
||||||
}
|
|
||||||
|
|
||||||
Process
|
|
||||||
{
|
|
||||||
foreach ($comp in $Name)
|
|
||||||
{
|
|
||||||
$bad = $false
|
|
||||||
|
|
||||||
# Get the common properties.
|
|
||||||
Write-Verbose "Checking $comp"
|
|
||||||
$serial = (Get-WmiObject -ComputerName $comp -Credential $Credential -Class Win32_BIOS).SerialNumber
|
|
||||||
|
|
||||||
# Get the hash (if available)
|
|
||||||
$devDetail = (Get-WMIObject -ComputerName $comp -Credential $Credential -Namespace root/cimv2/mdm/dmmap -Class MDM_DevDetail_Ext01 -Filter "InstanceID='Ext' AND ParentID='./DevDetail'")
|
|
||||||
if ($devDetail -and (-not $Force))
|
|
||||||
{
|
|
||||||
$hash = $devDetail.DeviceHardwareData
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$bad = $true
|
|
||||||
$hash = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# If the hash isn't available, get the make and model
|
|
||||||
if ($bad -or $Force)
|
|
||||||
{
|
|
||||||
$cs = Get-WmiObject -ComputerName $comp -Credential $Credential -Class Win32_ComputerSystem
|
|
||||||
$make = $cs.Manufacturer.Trim()
|
|
||||||
$model = $cs.Model.Trim()
|
|
||||||
if ($Partner)
|
|
||||||
{
|
|
||||||
$bad = $false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$make = ""
|
|
||||||
$model = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Getting the PKID is generally problematic for anyone other than OEMs, so let's skip it here
|
|
||||||
$product = ""
|
|
||||||
|
|
||||||
# Depending on the format requested, create the necessary object
|
|
||||||
if ($Partner)
|
|
||||||
{
|
|
||||||
# Create a pipeline object
|
|
||||||
$c = New-Object psobject -Property @{
|
|
||||||
"Device Serial Number" = $serial
|
|
||||||
"Windows Product ID" = $product
|
|
||||||
"Hardware Hash" = $hash
|
|
||||||
"Manufacturer name" = $make
|
|
||||||
"Device model" = $model
|
|
||||||
}
|
|
||||||
# From spec:
|
|
||||||
# "Manufacturer Name" = $make
|
|
||||||
# "Device Name" = $model
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
# Create a pipeline object
|
|
||||||
$c = New-Object psobject -Property @{
|
|
||||||
"Device Serial Number" = $serial
|
|
||||||
"Windows Product ID" = $product
|
|
||||||
"Hardware Hash" = $hash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Write the object to the pipeline or array
|
|
||||||
if ($bad)
|
|
||||||
{
|
|
||||||
# Report an error when the hash isn't available
|
|
||||||
Write-Error -Message "Unable to retrieve device hardware data (hash) from computer $comp" -Category DeviceError
|
|
||||||
}
|
|
||||||
elseif ($OutputFile -eq "")
|
|
||||||
{
|
|
||||||
$c
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$computers += $c
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
End
|
|
||||||
{
|
|
||||||
if ($OutputFile -ne "")
|
|
||||||
{
|
|
||||||
if ($Append)
|
|
||||||
{
|
|
||||||
if (Test-Path $OutputFile)
|
|
||||||
{
|
|
||||||
$computers += Import-CSV -Path $OutputFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($Partner)
|
|
||||||
{
|
|
||||||
$computers | Select "Device Serial Number", "Windows Product ID", "Hardware Hash", "Manufacturer name", "Device model" | ConvertTo-CSV -NoTypeInformation | % {$_ -replace '"',''} | Out-File $OutputFile
|
|
||||||
# From spec:
|
|
||||||
# $computers | Select "Device Serial Number", "Windows Product ID", "Hardware Hash", "Manufacturer Name", "Device Name" | ConvertTo-CSV -NoTypeInformation | % {$_ -replace '"',''} | Out-File $OutputFile
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$computers | Select "Device Serial Number", "Windows Product ID", "Hardware Hash" | ConvertTo-CSV -NoTypeInformation | % {$_ -replace '"',''} | Out-File $OutputFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
# URL to the application's installer
|
|
||||||
$downloadUrl = "https://pinotage.centrastage.net/csm/profile/downloadAgent/key-site"
|
|
||||||
# Path where the installer will be saved
|
|
||||||
$downloadPath = "C:\Windows\Temp\agentsetup.exe"
|
|
||||||
|
|
||||||
# Path to the uninstaller of the current application
|
|
||||||
$uninstallPath = "C:\Program Files (x86)\CentraStage\uninst.exe"
|
|
||||||
# Optional arguments for the uninstallation
|
|
||||||
$uninstallArgs = "/uninstall /quiet"
|
|
||||||
|
|
||||||
# Optional arguments for the installation
|
|
||||||
$installArgs = "/silent"
|
|
||||||
|
|
||||||
# Download the installer
|
|
||||||
Write-Output "Downloading the installer..."
|
|
||||||
Invoke-WebRequest -Uri $downloadUrl -OutFile $downloadPath
|
|
||||||
|
|
||||||
Write-Output "The installer has been downloaded to $downloadPath."
|
|
||||||
|
|
||||||
# Start the uninstallation
|
|
||||||
$uninstallProcess = Start-Process -FilePath $uninstallPath -ArgumentList $uninstallArgs -PassThru
|
|
||||||
|
|
||||||
# Wait until the uninstallation is completed
|
|
||||||
Write-Output "Waiting for the uninstallation to complete..."
|
|
||||||
Wait-Process -Id $uninstallProcess.Id
|
|
||||||
|
|
||||||
Write-Output "The uninstallation is complete."
|
|
||||||
|
|
||||||
# Start the installation
|
|
||||||
$installProcess = Start-Process -FilePath $downloadPath -ArgumentList $installArgs -PassThru
|
|
||||||
|
|
||||||
# Wait until the installation is completed
|
|
||||||
Write-Output "Waiting for the installation to complete..."
|
|
||||||
Wait-Process -Id $installProcess.Id
|
|
||||||
|
|
||||||
Write-Output "The installation is complete."
|
|
||||||
|
|
||||||
# Remove the downloaded installer
|
|
||||||
Write-Output "Removing the downloaded installer..."
|
|
||||||
Remove-Item -Path $downloadPath -Force
|
|
||||||
|
|
||||||
Write-Output "The downloaded installer has been removed."
|
|
||||||
|
|
||||||
|
|
||||||
Write-Output "De installatie is voltooid."
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
# Define the reboot time
|
|
||||||
$rebootTime = "21:00"
|
|
||||||
|
|
||||||
# Get the current date and time
|
|
||||||
$currentDate = Get-Date
|
|
||||||
|
|
||||||
# Parse the reboot time into a DateTime object for today
|
|
||||||
$rebootDateTime = [datetime]::ParseExact("$($currentDate.ToString('yyyy-MM-dd')) $rebootTime", "yyyy-MM-dd HH:mm", $null)
|
|
||||||
|
|
||||||
# Check if the reboot time is in the past for today; if so, schedule it for tomorrow
|
|
||||||
if ($rebootDateTime -lt $currentDate) {
|
|
||||||
$rebootDateTime = $rebootDateTime.AddDays(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Format the DateTime object for the task scheduler
|
|
||||||
$taskTime = $rebootDateTime.ToString("HH:mm")
|
|
||||||
|
|
||||||
# Create a scheduled task action for a forced reboot
|
|
||||||
$taskName = "ForcedReboot"
|
|
||||||
$action = New-ScheduledTaskAction -Execute "shutdown.exe" -Argument "/r /f /t 0"
|
|
||||||
|
|
||||||
# Create a trigger for the specified time
|
|
||||||
$trigger = New-ScheduledTaskTrigger -Once -At $rebootDateTime
|
|
||||||
|
|
||||||
# Configure settings to allow the task to run when no user is logged in
|
|
||||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable `
|
|
||||||
-RunOnlyIfIdle:$false -WakeToRun
|
|
||||||
|
|
||||||
# Set the task to run with the highest privileges and even when no user is logged in
|
|
||||||
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
|
||||||
|
|
||||||
# Register the task
|
|
||||||
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Force
|
|
||||||
|
|
||||||
Write-Host "Scheduled reboot at $rebootDateTime, even if no user is logged in."
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
# This script is designed for use in Datto RMM (CentraStage).
|
|
||||||
# Purpose: Uninstall all "Microsoft Visual C++ 2010 ... Redistributable" (x86/x64), version independent.
|
|
||||||
#
|
|
||||||
# Example run / expected output:
|
|
||||||
# Uninstall Microsoft Visual C++ 2010
|
|
||||||
# Found: Microsoft Visual C++ 2010 x64 Redistributable - 10.0.40219, Microsoft Visual C++ 2010 x86 Redistributable - 10.0.40219
|
|
||||||
# Uninstall (MSI) Microsoft Visual C++ 2010 x64 Redistributable - 10.0.40219 -> msiexec /X{1D8E6291-B0D5-35EC-8441-6616F567A0F7} /quiet /norestart
|
|
||||||
# Uninstall (MSI) Microsoft Visual C++ 2010 x86 Redistributable - 10.0.40219 -> msiexec /X{F0C3E5D1-1ADE-321E-8167-68EF0DE699A5} /quiet /norestart
|
|
||||||
# Completed: all target items successfully removed (or not present).
|
|
||||||
#
|
|
||||||
# Usage in Datto RMM:
|
|
||||||
# - Create a new Component → Script (PowerShell).
|
|
||||||
# - Paste this script in the code editor.
|
|
||||||
# - Run As: System
|
|
||||||
# - Architecture: 64-bit
|
|
||||||
# - Test on one device before wide rollout.
|
|
||||||
# - Deploy via Job or Policy at the Site level to target all devices.
|
|
||||||
#
|
|
||||||
# Exit codes:
|
|
||||||
# 0 = OK (nothing found or everything removed successfully)
|
|
||||||
# 1 = Failures occurred
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
$fails = 0
|
|
||||||
|
|
||||||
function Invoke-Uninstall {
|
|
||||||
param(
|
|
||||||
[string]$CmdLine,
|
|
||||||
[string]$AppName
|
|
||||||
)
|
|
||||||
|
|
||||||
if (-not $CmdLine) {
|
|
||||||
Write-Warning ("No UninstallString found for " + $AppName)
|
|
||||||
$script:fails++
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# Normalize to exe + args
|
|
||||||
$exe = $null
|
|
||||||
$args = ""
|
|
||||||
if ($CmdLine -match '^\s*"(.+?)"\s*(.*)$') {
|
|
||||||
$exe = $Matches[1]
|
|
||||||
$args = $Matches[2]
|
|
||||||
} elseif ($CmdLine -match '^\s*(\S+)\s*(.*)$') {
|
|
||||||
$exe = $Matches[1]
|
|
||||||
$args = $Matches[2]
|
|
||||||
}
|
|
||||||
|
|
||||||
# MSI: force silent uninstall
|
|
||||||
if ($exe -match '(?i)msiexec\.exe') {
|
|
||||||
if ($args -match '(?i)/I\s*{([0-9A-F\-]+)}') {
|
|
||||||
$guid = $Matches[1]
|
|
||||||
$args = "/x {" + $guid + "} /quiet /norestart"
|
|
||||||
} else {
|
|
||||||
if ($args -notmatch '(?i)/x') { $args = "/x " + $args }
|
|
||||||
if ($args -notmatch '(?i)/quiet|/qn') { $args = $args + " /quiet" }
|
|
||||||
if ($args -notmatch '(?i)/norestart') { $args = $args + " /norestart" }
|
|
||||||
}
|
|
||||||
Write-Output ("Uninstall (MSI) " + $AppName + " -> msiexec " + $args)
|
|
||||||
} else {
|
|
||||||
# Non-MSI: try to enforce silent switches
|
|
||||||
if ($args -notmatch '(?i)/quiet|/qn|/silent|/s') { $args = $args + " /quiet" }
|
|
||||||
if ($args -notmatch '(?i)/norestart') { $args = $args + " /norestart" }
|
|
||||||
Write-Output ("Uninstall (EXE) " + $AppName + " -> " + $exe + " " + $args)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$p = Start-Process -FilePath $exe -ArgumentList $args -Wait -PassThru -WindowStyle Hidden
|
|
||||||
$code = $p.ExitCode
|
|
||||||
# Common msiexec codes: 0=success, 3010=reboot required, 1605=not installed
|
|
||||||
if ($exe -match '(?i)msiexec\.exe') {
|
|
||||||
if ($code -in 0,3010,1605) {
|
|
||||||
if ($code -eq 3010) { Write-Output ("Note: reboot required (3010) for " + $AppName) }
|
|
||||||
elseif ($code -eq 1605) { Write-Output ("Not installed (1605) for " + $AppName + ", skipping.") }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ($code -eq 0) { return }
|
|
||||||
}
|
|
||||||
Write-Warning ("Uninstall failed for " + $AppName + " with exit code " + $code)
|
|
||||||
$script:fails++
|
|
||||||
} catch {
|
|
||||||
$msg = $_.Exception.Message
|
|
||||||
Write-Warning ("Error while uninstalling " + $AppName + ": " + $msg)
|
|
||||||
$script:fails++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Pre-check: look for target apps
|
|
||||||
$uninstallKeys = @(
|
|
||||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
|
||||||
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
|
||||||
)
|
|
||||||
|
|
||||||
$targets = foreach ($key in $uninstallKeys) {
|
|
||||||
Get-ItemProperty -Path $key -ErrorAction SilentlyContinue |
|
|
||||||
Where-Object {
|
|
||||||
$_.DisplayName -and ($_.DisplayName -like "Microsoft Visual C++ 2010*Redistributable*")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $targets) {
|
|
||||||
Write-Output "No Microsoft Visual C++ 2010 Redistributables found. Nothing to do."
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
$foundList = ($targets.DisplayName | Sort-Object -Unique) -join ", "
|
|
||||||
Write-Output ("Found: " + $foundList)
|
|
||||||
|
|
||||||
# Uninstall each item
|
|
||||||
foreach ($app in $targets) {
|
|
||||||
$name = $app.DisplayName
|
|
||||||
$cmd = if ($app.QuietUninstallString) { $app.QuietUninstallString } else { $app.UninstallString }
|
|
||||||
Invoke-Uninstall -CmdLine $cmd -AppName $name
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($fails -gt 0) {
|
|
||||||
Write-Output ("Completed with errors: " + $fails + " item(s) failed to uninstall.")
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
Write-Output "Completed: all target items successfully removed (or not present)."
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
#UUID list
|
|
||||||
#Maakt een Excel lijst met UUID en serienummer
|
|
||||||
cls
|
|
||||||
$UUID = (Get-WmiObject Win32_ComputerSystemProduct).UUID
|
|
||||||
$Serial = (Get-WmiObject Win32_BIOS).SerialNumber
|
|
||||||
$a = New-object PSobject
|
|
||||||
$a | Add-Member NoteProperty "UUID" $UUID
|
|
||||||
$a | Add-Member NoteProperty "Serial" $Serial
|
|
||||||
$a | Format-Table
|
|
||||||
$a | Export-Csv -Path list.csv -Append
|
|
||||||
Reference in New Issue
Block a user