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/`
|
||||
- `Test-SMTPRelay-M365.ps1`
|
||||
- `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.
|
||||
```bash
|
||||
git remote set-url origin https://forgejo.oskamp.info/ivooskamp/Random_scripts.git
|
||||
```
|
||||
|
||||
@ -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