diff --git a/.github/workflows/security-validation.yml b/.github/workflows/security-validation.yml
new file mode 100644
index 0000000..ff4a06b
--- /dev/null
+++ b/.github/workflows/security-validation.yml
@@ -0,0 +1,77 @@
+name: Security validation
+
+on:
+ pull_request:
+ push:
+ branches: [master]
+
+permissions:
+ contents: read
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install shell runtimes
+ run: sudo apt-get update && sudo apt-get install -y shellcheck zsh
+
+ - name: Parse Bash scripts
+ shell: bash
+ run: |
+ while IFS= read -r -d '' script; do
+ first_line="$(head -n 1 "$script")"
+ if [[ "$first_line" == *bash* || "$first_line" == "#!/bin/sh" ]]; then
+ bash -n "$script"
+ fi
+ done < <(find . -type f -name '*.sh' -print0)
+
+ - name: Parse Zsh scripts
+ shell: bash
+ run: |
+ while IFS= read -r -d '' script; do
+ if head -n 1 "$script" | grep -q zsh; then
+ zsh -n "$script"
+ fi
+ done < <(find . -type f -name '*.sh' -print0)
+
+ - name: Run ShellCheck
+ shell: bash
+ run: |
+ mapfile -d '' scripts < <(
+ while IFS= read -r -d '' script; do
+ first_line="$(head -n 1 "$script")"
+ if [[ "$first_line" == *bash* || "$first_line" == "#!/bin/sh" ]]; then
+ printf '%s\0' "$script"
+ fi
+ done < <(find . -type f -name '*.sh' -print0)
+ )
+ shellcheck --severity=error "${scripts[@]}"
+
+ - name: Parse Python and run tests
+ run: |
+ python -m compileall -q .
+ python -m unittest discover -s tests -v
+
+ - name: Parse JavaScript
+ run: node --check tampermonkey/edx-download-transcripts.js
+
+ - name: Parse PowerShell
+ shell: pwsh
+ run: |
+ $failed = $false
+ Get-ChildItem -Recurse -Filter *.ps1 | ForEach-Object {
+ $tokens = $null
+ $errors = $null
+ [System.Management.Automation.Language.Parser]::ParseFile(
+ $_.FullName,
+ [ref]$tokens,
+ [ref]$errors
+ ) | Out-Null
+ if ($errors.Count) {
+ $failed = $true
+ $errors | ForEach-Object { Write-Error $_.Message }
+ }
+ }
+ if ($failed) { exit 1 }
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ed8ebf5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+__pycache__
\ No newline at end of file
diff --git a/Monitor-ADGroupChanges.ps1 b/Monitor-ADGroupChanges.ps1
index 9a40dec..bf6dbc2 100644
--- a/Monitor-ADGroupChanges.ps1
+++ b/Monitor-ADGroupChanges.ps1
@@ -1,128 +1,107 @@
-# Monitor-ADGroupMemberChanges
-#
-# 1. Get members.
-# 2. Compare to previous members.
-# 3. Output changes.
-#
-# Credit to https://github.com/lazywinadmin/Monitor-ADGroupMembership
-# Portions Copyright (c) 2015 Francois-Xavier Cat
+[CmdletBinding()]
+param(
+ [Parameter()]
+ [string]$GroupName = "Domain Admins",
-# The MIT License (MIT)
+ [Parameter()]
+ [string]$StateDirectory = (Join-Path $env:ProgramData "ScriptSecurity\ADGroupMonitor")
+)
-# Copyright (c) 2020 Benjamin Hunter
-# Copyright (c) 2015 Francois-Xavier Cat
+$ErrorActionPreference = "Stop"
+Import-Module ActiveDirectory -ErrorAction Stop
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
+function Protect-CsvValue {
+ param([AllowNull()][object]$Value)
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-
-#######################################
-# Simple version
-
-
-$item = "Domain Admins"
-$ScriptName = $MyInvocation.MyCommand
-$ScriptPath = (Split-Path -Path ((Get-Variable -Name MyInvocation).Value).MyCommand.Path)
-$ScriptPathOutput = $ScriptPath + "\Output"
-
-$MemberObjs = Get-ADGroupMember -Identity $item -Recursive -ErrorAction Stop
-[Array]$Members = $MemberObjs | Where-Object {$_.objectClass -eq "user" } | Get-ADUser -Properties PasswordExpired | Select-Object -Property *,@{ Name = 'DN'; Expression = { $_.DistinguishedName } }
-$Members += $MemberObjs | Where-Object {$_.objectClass -eq "computer" } | Get-ADComputer -Properties PasswordExpired | Select-Object -Property *,@{ Name = 'DN'; Expression = { $_.DistinguishedName } }
-
-# Load previous membership
-$ImportCSV = Import-Csv -Path (Join-Path -Path $ScriptPathOutput -ChildPath $StateFile) -ErrorAction Stop -ErrorVariable ErrorProcessImportCSV
-
-# Write twice - time stamped and reference file for next comparison.
-$Members | Export-Csv -Path (Join-Path -Path $ScriptPathOutput -ChildPath ($StateFile + (Get-Date -Format FileDateTimeUniversal)) -NoTypeInformation -Encoding Unicode
-$Members | Export-Csv -Path (Join-Path -Path $ScriptPathOutput -ChildPath $StateFile) -NoTypeInformation -Encoding Unicode
-
-$Changes = Compare-Object -DifferenceObject $ImportCSV -ReferenceObject $Members -ErrorAction Stop -ErrorVariable ErrorProcessCompareObject -Property Name, SamAccountName, DN |
-Select-Object -Property @{ Name = "DateTime"; Expression = { Get-Date -Format "yyyyMMdd-hh:mm:ss" } }, @{
- Name = 'State'; expression = {
- if ($_.SideIndicator -eq "=>") { "Removed" }
- else { "Added" }
+ $text = [string]$Value
+ if ($text -match '^[=+\-@]') {
+ return "'$text"
}
-}, DisplayName, Name, SamAccountName, DN | Where-Object { $_.name -notlike "*no user or group*" }
-
-Write-Output $Changes
-
-
-
-#######################################
-# Work in progress
-# Based on https://github.com/lazywinadmin/Monitor-ADGroupMembership
-
-
-
+ return $text
+}
-$item = "Domain Admins"
+function Set-RestrictedDirectoryAcl {
+ param([Parameter(Mandatory)][string]$Path)
-$ScriptName = $MyInvocation.MyCommand
-$ScriptPath = (Split-Path -Path ((Get-Variable -Name MyInvocation).Value).MyCommand.Path)
-$ScriptPathOutput = $ScriptPath + "\Output"
+ $acl = Get-Acl -LiteralPath $Path
+ $acl.SetAccessRuleProtection($true, $false)
+ foreach ($rule in @($acl.Access)) {
+ [void]$acl.RemoveAccessRuleAll($rule)
+ }
-if (-not(Test-Path -Path $ScriptPathOutput))
-{
- Write-Verbose -Message "[$ScriptName][Begin] Creating the Output Folder : $ScriptPathOutput"
- New-Item -Path $ScriptPathOutput -ItemType Directory | Out-Null
+ $identities = @(
+ "NT AUTHORITY\SYSTEM",
+ "BUILTIN\Administrators",
+ [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
+ ) | Select-Object -Unique
+
+ foreach ($identity in $identities) {
+ $rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
+ $identity,
+ [System.Security.AccessControl.FileSystemRights]::FullControl,
+ [System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
+ [System.Security.AccessControl.PropagationFlags]::None,
+ [System.Security.AccessControl.AccessControlType]::Allow
+ )
+ $acl.AddAccessRule($rule)
+ }
+ Set-Acl -LiteralPath $Path -AclObject $acl
}
-$GroupName = Get-ADGroup @GroupSplatting -Properties * -ErrorAction Continue -ErrorVariable ErrorProcessGetADGroup
-Write-Verbose -Message "[$ScriptName][Process] Extracting Domain Name from $($GroupName.CanonicalName)"
-$DomainName = ($GroupName.CanonicalName -split '/')[0]
-$RealGroupName = $GroupName.Name
-
-$MemberObjs = Get-ADGroupMember -Identity $item -Recursive -ErrorAction Stop
-[Array]$Members = $MemberObjs | Where-Object {$_.objectClass -eq "user" } | Get-ADUser -Properties PasswordExpired | Select-Object -Property *,@{ Name = 'DN'; Expression = { $_.DistinguishedName } }
-$Members += $MemberObjs | Where-Object {$_.objectClass -eq "computer" } | Get-ADComputer -Properties PasswordExpired | Select-Object -Property *,@{ Name = 'DN'; Expression = { $_.DistinguishedName } }
+function ConvertTo-StateRecord {
+ param([Parameter(Mandatory)]$DirectoryObject)
-# GroupName Membership File
-# if the file doesn't exist, assume we don't have a record to refer to
-$StateFile = "$($DomainName)_$($RealGroupName)-membership.csv"
-
-if (-not (Test-Path -Path (Join-Path -Path $ScriptPathOutput -ChildPath $StateFile)))
-{
- Write-Verbose -Message "[$ScriptName][Process] $item - The following file did not exist: $StateFile"
- Write-Verbose -Message "[$ScriptName][Process] $item - Exporting the current membership information into the file: $StateFile"
+ [pscustomobject]@{
+ Name = Protect-CsvValue $DirectoryObject.Name
+ SamAccountName = Protect-CsvValue $DirectoryObject.SamAccountName
+ DN = Protect-CsvValue $DirectoryObject.DistinguishedName
+ ObjectClass = Protect-CsvValue $DirectoryObject.ObjectClass
+ }
+}
- $Members | Export-Csv -Path (Join-Path -Path $ScriptPathOutput -ChildPath $StateFile) -NoTypeInformation -Encoding Unicode
+if (-not (Test-Path -LiteralPath $StateDirectory)) {
+ New-Item -Path $StateDirectory -ItemType Directory -Force | Out-Null
}
-else
-{
- Write-Verbose -Message "[$ScriptName][Process] $item - The following file exists: $StateFile"
+$StateDirectory = (Resolve-Path -LiteralPath $StateDirectory).Path
+Set-RestrictedDirectoryAcl -Path $StateDirectory
+
+$group = Get-ADGroup -Identity $GroupName -ErrorAction Stop
+$safeGroupName = $group.SamAccountName -replace '[^A-Za-z0-9_.-]', '_'
+$stateFile = Join-Path $StateDirectory "$safeGroupName-membership.csv"
+
+$members = @(
+ Get-ADGroupMember -Identity $group -Recursive -ErrorAction Stop |
+ Where-Object { $_.ObjectClass -in @("user", "computer", "group") } |
+ ForEach-Object { ConvertTo-StateRecord $_ } |
+ Sort-Object ObjectClass, SamAccountName, DN
+)
+
+if (Test-Path -LiteralPath $stateFile) {
+ $previous = @(Import-Csv -LiteralPath $stateFile)
+ $changes = Compare-Object `
+ -ReferenceObject $previous `
+ -DifferenceObject $members `
+ -Property Name, SamAccountName, DN, ObjectClass |
+ ForEach-Object {
+ [pscustomobject]@{
+ DateTime = Get-Date -Format "yyyy-MM-ddTHH:mm:ssK"
+ State = if ($_.SideIndicator -eq "=>") { "Added" } else { "Removed" }
+ Name = $_.Name
+ SamAccountName = $_.SamAccountName
+ DN = $_.DN
+ ObjectClass = $_.ObjectClass
+ }
+ }
+ $changes
}
-# GroupName Membership File is compared with the current GroupName Membership
-Write-Verbose -Message "[$ScriptName][Process] $item - Comparing Current and Before"
-
-$ImportCSV = Import-Csv -Path (Join-Path -Path $ScriptPathOutput -ChildPath $StateFile) -ErrorAction Stop -ErrorVariable ErrorProcessImportCSV
-
-$Changes = Compare-Object -DifferenceObject $ImportCSV -ReferenceObject $Members -ErrorAction Stop -ErrorVariable ErrorProcessCompareObject -Property Name, SamAccountName, DN |
-Select-Object -Property @{ Name = "DateTime"; Expression = { Get-Date -Format "yyyyMMdd-hh:mm:ss" } }, @{
- Name = 'State'; expression = {
- if ($_.SideIndicator -eq "=>") { "Removed" }
- else { "Added" }
+$temporaryFile = Join-Path $StateDirectory ([System.IO.Path]::GetRandomFileName())
+try {
+ $members | Export-Csv -LiteralPath $temporaryFile -NoTypeInformation -Encoding Unicode
+ Move-Item -LiteralPath $temporaryFile -Destination $stateFile -Force
+}
+finally {
+ if (Test-Path -LiteralPath $temporaryFile) {
+ Remove-Item -LiteralPath $temporaryFile -Force
}
-}, DisplayName, Name, SamAccountName, DN | Where-Object { $_.name -notlike "*no user or group*" }
-
-Write-Verbose -Message "[$ScriptName][Process] $item - Compare Block Done!"
-
-
-
-
-
-
+}
diff --git a/README.md b/README.md
index 52812df..3d14c4d 100644
--- a/README.md
+++ b/README.md
@@ -10,10 +10,11 @@ scripts that delete files or modify system configuration.
- `π§ WIP` - Incomplete, experimental, or known to contain unfinished behavior.
- `π Elevated` - Requires root, administrator, or other privileged access.
- `π§ͺ Example` - Demonstration code that needs customization before use.
+- `Retired` - Kept as a fail-closed stub for discoverability; it no longer performs its former operation.
## Git and GitHub
-- `β οΈ` [`git-delete-merged-branches.sh`](git-delete-merged-branches.sh) - Opens a list of branches already merged into `main` in Neovim, then deletes the selected branches.
+- `β οΈ` [`git-delete-merged-branches.sh`](git-delete-merged-branches.sh) - Uses a private temporary file to edit and delete selected branches already merged into `main` or `master`.
- [`git-pull-recursive.sh`](git-pull-recursive.sh) - Recursively finds Git repositories under a directory and fetches and pulls each one.
- [`git-remotes-recursive.sh`](git-remotes-recursive.sh) - Recursively lists Git repositories and their fetch remotes.
- [`git-status-directories.sh`](git-status-directories.sh) - Recursively reports Git repositories with uncommitted changes.
@@ -26,7 +27,7 @@ scripts that delete files or modify system configuration.
## GitLab
- [`gitlab-check-image-publish-time.sh`](gitlab-check-image-publish-time.sh) - Reports how long ago a GitLab container registry image tag was published.
-- `π§` [`gitlab-clone-group.sh`](gitlab-clone-group.sh) - Clones projects from a GitLab group and attempts to process its subgroups; currently a work in progress.
+- `Retired` [`gitlab-clone-group.sh`](gitlab-clone-group.sh) - Retired unsafe prototype; use `python/clone-gitlab-group.py`.
- [`gitlab-clone-projects-recursive.sh`](gitlab-clone-projects-recursive.sh) - Exports or reads GitLab project metadata and clones projects into their namespace directory structure.
- [`gitlab-get-project-id-from-current-repo.sh`](gitlab-get-project-id-from-current-repo.sh) - Resolves the current repository's GitLab project ID by matching its remote URL.
- [`gitlab-list-registry-images.sh`](gitlab-list-registry-images.sh) - Lists container registry tags for the current GitLab repository and reports each tag's age.
@@ -37,49 +38,49 @@ scripts that delete files or modify system configuration.
- `β οΈ` `π` [`clean-storage-safe.sh`](clean-storage-safe.sh) - Interactively reviews and cleans package caches, build artifacts, logs, temporary files, user caches, and trash.
- [`diagnose-disk-space-ubuntu.sh`](diagnose-disk-space-ubuntu.sh) - Performs a read-only Ubuntu disk-usage audit and prints possible cleanup commands.
-- `β οΈ` `π` [`expand_root_volume.sh`](expand_root_volume.sh) - Expands partition 2 and its ext4 root filesystem with `growpart` and `resize2fs`.
+- `β οΈ` `π` [`expand_root_volume.sh`](expand_root_volume.sh) - Identifies and confirms a directly mounted ext4 root partition before resizing it.
- [`find-backup-files.sh`](find-backup-files.sh) - Creates a report of recently modified documents, code, configuration, media, database, archive, and large files.
-- `β οΈ` [`rm-recursive-.gradle.sh`](rm-recursive-.gradle.sh) - Recursively deletes every `.gradle` directory below the current directory.
-- `β οΈ` [`rm-recursive-build.sh`](rm-recursive-build.sh) - Recursively deletes every `build` directory below the current directory.
-- `β οΈ` [`rm-recursive-node_modules.sh`](rm-recursive-node_modules.sh) - Recursively deletes every `node_modules` directory below the current directory.
-- `β οΈ` [`rm-recursive-postgres-data.sh`](rm-recursive-postgres-data.sh) - Recursively deletes every `postgres-data` directory below the current directory.
+- `β οΈ` [`rm-recursive-.gradle.sh`](rm-recursive-.gradle.sh) - Previews and confirms symlink-safe removal of `.gradle` directories below a selected root.
+- `β οΈ` [`rm-recursive-build.sh`](rm-recursive-build.sh) - Previews and confirms symlink-safe removal of `build` directories below a selected root.
+- `β οΈ` [`rm-recursive-node_modules.sh`](rm-recursive-node_modules.sh) - Previews and confirms symlink-safe removal of `node_modules` directories below a selected root.
+- `β οΈ` [`rm-recursive-postgres-data.sh`](rm-recursive-postgres-data.sh) - Previews and confirms symlink-safe removal of `postgres-data` directories below a selected root.
- `π` [`storage_diagnose.sh`](storage_diagnose.sh) - Collects disk, partition, LVM, inode, large-file, ZFS, and snapshot diagnostics into a log in `/tmp`.
- `β οΈ` `π` [`update-apt.sh`](update-apt.sh) - Updates, upgrades, cleans, and removes unused packages on apt-based systems.
-- [`zfsdash.py`](zfsdash.py) - Serves a small web dashboard for ZFS pool status, scrub progress, and resilver progress.
+- [`zfsdash.py`](zfsdash.py) - Serves a localhost-only, cached web dashboard for ZFS pool status, scrub progress, and resilver progress.
## Docker, Cloud, and Deployment
- [`docker-debug-container.sh`](docker-debug-container.sh) - Starts a Docker image interactively with `/bin/sh` as its entrypoint and removes the container afterward.
- [`docker-list-tags-remote.sh`](docker-list-tags-remote.sh) - Lists Docker Hub tags for an image, optionally filtering them by text.
-- `β οΈ` `π` `π§ͺ` [`example-deploy.sh`](example-deploy.sh) - Example installer that clones or updates HeavyScript to its latest tag and creates a command wrapper.
+- `Retired` [`example-deploy.sh`](example-deploy.sh) - Retired root installer that trusted mutable upstream tags.
- `β οΈ` [`gcp-subnets-enable-flow-logs.sh`](gcp-subnets-enable-flow-logs.sh) - Enables VPC Flow Logs on every subnet in the active Google Cloud project.
## Networking
- [`get-ip-from-domain-names.sh`](get-ip-from-domain-names.sh) - Reads domain names from a file and prints the IP addresses returned by `dig`.
-- [`http-wait.sh`](http-wait.sh) - Polls an HTTP host until it becomes reachable.
+- [`http-wait.sh`](http-wait.sh) - Polls a complete HTTP or HTTPS URL until it becomes reachable.
- [`ssh-wait.sh`](ssh-wait.sh) - Retries an SSH connection until the target becomes reachable.
## Development Utilities
- [`combine-md.sh`](combine-md.sh) - Combines Markdown files in the current directory into `combined.md`, adding a heading for each source file.
-- `π§ͺ` [`generate-pdf-from-tex.ps1`](generate-pdf-from-tex.ps1) - Runs `pdflatex` in a TeX Live Docker container to generate a PDF from `week_8.tex`.
+- [`generate-pdf-from-tex.ps1`](generate-pdf-from-tex.ps1) - Runs `pdflatex` in a restricted, digest-pinned TeX Live container; shell escape requires explicit opt-in.
- `π§ͺ` [`is-command-in-path.sh`](is-command-in-path.sh) - Demonstrates checking whether a configured command, currently `cargo`, exists in `PATH`.
- [`python-run-on-change.sh`](python-run-on-change.sh) - Uses `fswatch` to rerun a Python file whenever it changes.
-- [`Watch-Command.ps1`](Watch-Command.ps1) - Defines a PowerShell function that repeatedly runs a command at a configurable interval.
+- [`Watch-Command.ps1`](Watch-Command.ps1) - Repeatedly invokes a trusted PowerShell script block at a configurable interval.
## Platform Setup
- [`bash-3-version-check-mac.sh`](bash-3-version-check-mac.sh) - Prints the version of macOS's system Bash.
- [`bash-5-version-check-mac.sh`](bash-5-version-check-mac.sh) - Prints the version of Bash installed at `/usr/local/bin/bash`.
- [`java_home.sh`](java_home.sh) - Defines macOS shell functions for switching `JAVA_HOME` between common JDK versions.
-- [`macos/ruby/chruby_local.sh`](macos/ruby/chruby_local.sh) - Installs `chruby` under the current user's home directory and configures shell startup files.
-- [`macos/ruby/ruby-install_local.sh`](macos/ruby/ruby-install_local.sh) - Installs `ruby-install` locally and uses it to install the latest stable Ruby.
-- `β οΈ` `π` [`setup-kali.sh`](setup-kali.sh) - Bootstraps a Kali Linux installation with packages, tools, shell configuration, Ghidra, and security utilities.
+- `Retired` [`macos/ruby/chruby_local.sh`](macos/ruby/chruby_local.sh) - Retired unverified chruby installer.
+- `Retired` [`macos/ruby/ruby-install_local.sh`](macos/ruby/ruby-install_local.sh) - Retired unverified ruby-install bootstrapper.
+- `Retired` [`setup-kali.sh`](setup-kali.sh) - Retired root bootstrapper for obsolete, unverified third-party software.
## Windows and Active Directory
-- `π§` [`Monitor-ADGroupChanges.ps1`](Monitor-ADGroupChanges.ps1) - Compares current Active Directory group membership with saved CSV state and reports additions and removals; includes work-in-progress code.
+- [`Monitor-ADGroupChanges.ps1`](Monitor-ADGroupChanges.ps1) - Atomically tracks Active Directory group membership in an ACL-restricted, spreadsheet-safe CSV state file.
## Browser Tools
diff --git a/SECURITY_ASSESSMENT.md b/SECURITY_ASSESSMENT.md
new file mode 100644
index 0000000..8ba756b
--- /dev/null
+++ b/SECURITY_ASSESSMENT.md
@@ -0,0 +1,263 @@
+# Security Assessment
+
+Date: 2026-06-08
+
+## Scope and Method
+
+Reviewed all 48 script-like files in this repository:
+
+- 40 shell scripts
+- 3 PowerShell scripts
+- 3 Python scripts
+- 1 JavaScript userscript
+- 1 HTML/JavaScript utility
+
+The review covered command injection, path traversal, unsafe deletion, privilege
+boundaries, credential exposure, network exposure, supply-chain integrity,
+temporary-file handling, browser injection, and sensitive-data handling.
+
+Static validation performed:
+
+- Python AST parsing: passed for all Python files.
+- PowerShell parsing: `Monitor-ADGroupChanges.ps1` failed at line 50.
+- JavaScript syntax checking: passed.
+- Secret-pattern scan: no committed private keys or recognizable access tokens found.
+- Shell syntax/static analysis could not be run because ShellCheck is unavailable
+ and Windows Subsystem for Linux has no installed distribution.
+
+## Findings
+
+### High: Remote code is loaded over plaintext HTTP
+
+File: `tampermonkey/edx-download-transcripts.js:10`
+
+The userscript loads `jquery-latest.js` over HTTP. A network attacker can replace
+the response and execute arbitrary JavaScript in the context of every matching
+edX page. Using the mutable `latest` target also makes the dependency
+non-reproducible.
+
+Recommendation: remove jQuery if possible. Otherwise pin an exact HTTPS asset
+and verify the userscript manager's integrity mechanism, if supported.
+
+### High: Root setup executes unverified third-party content
+
+File: `setup-kali.sh:105-125`, `setup-kali.sh:137-138`
+
+The script runs as root, downloads a Ghidra archive without checksum or signature
+verification, clones a mutable Git repository, and installs its requirements.
+It also installs unpinned Python packages as root. Compromise of a download,
+repository, dependency, or account can become root code execution.
+
+Recommendation: pin immutable versions/commits, verify release signatures or
+checksums, install Python tools in an unprivileged isolated environment, and
+avoid running repository-controlled installation code as root.
+
+### High: TeX compilation explicitly permits command execution
+
+File: `generate-pdf-from-tex.ps1:1`
+
+`pdflatex -shell-escape` allows the TeX document to execute commands. The current
+directory is mounted writable into an unpinned `texlive/texlive:latest` image.
+Compiling an untrusted `week_8.tex` can modify repository files and run commands
+inside the container; container/runtime weaknesses could increase the impact.
+
+Recommendation: remove `-shell-escape` unless required, pin the image by digest,
+mount source read-only, and write output to a separate directory.
+
+### High: Recursive deletion follows directory symlinks
+
+Files:
+
+- `rm-recursive-.gradle.sh:6-12`
+- `rm-recursive-build.sh:5-11`
+- `rm-recursive-node_modules.sh:5-11`
+- `rm-recursive-postgres-data.sh:5-11`
+
+`[ -d "$file" ]` follows symlinks, and the recursive functions descend through
+them. A symlink below the starting directory can redirect traversal outside the
+tree, where a matching directory may then be deleted.
+
+Recommendation: use `find` without `-L`, constrain results to the resolved
+starting directory, reject symbolic links explicitly, and add confirmation or
+dry-run behavior.
+
+### Medium: ZFS status is exposed without authentication
+
+File: `zfsdash.py:6`, `zfsdash.py:394-416`
+
+The server binds to `0.0.0.0` and exposes pool names, device paths, errors, and
+status to any reachable client. Every API request launches `zpool`, so a remote
+client can also repeatedly consume process and I/O resources.
+
+Recommendation: bind to `127.0.0.1` by default. Put authentication, TLS, request
+limits, and caching in front of it if remote access is required.
+
+### Medium: Stored DOM injection through filenames
+
+File: `file-manager.html:282-291`
+
+Stored filenames are interpolated into `innerHTML`, including text and an HTML
+attribute. A crafted filename can inject markup and script-capable event
+handlers. Because records persist in IndexedDB, the injection is persistent for
+the page's origin.
+
+Recommendation: construct elements with DOM APIs, assign filenames with
+`textContent`, and set `href` and `download` properties directly.
+
+### Medium: Predictable privileged temporary files
+
+Files:
+
+- `git-delete-merged-branches.sh:2`
+- `storage_diagnose.sh:4-7`
+
+Both scripts create predictable files under `/tmp` without `mktemp` or exclusive
+creation. When run by a privileged user, a local attacker can pre-create a
+symlink and cause truncation or appending to another file. The storage report
+also contains sensitive filesystem, UUID, path, and host information and will
+normally be created with permissive default mode bits.
+
+Recommendation: use `mktemp`, set `umask 077`, install cleanup traps, and avoid
+running the branch helper with elevated privileges.
+
+### Medium: Root installer tracks mutable upstream state
+
+File: `example-deploy.sh:24-36`, `example-deploy.sh:63-100`
+
+This root-only installer trusts the latest reachable Git tag, forcibly checks it
+out, and exposes the downloaded executable through root's `PATH`. Tags are
+mutable and no commit/signature allowlist is enforced.
+
+Recommendation: pin a reviewed commit or signed release and verify it before
+installing the executable.
+
+### Medium: AD data is exported without spreadsheet neutralization
+
+File: `Monitor-ADGroupChanges.ps1:43-51`, `Monitor-ADGroupChanges.ps1:90-120`
+
+AD-controlled values are exported to CSV. If an attribute begins with `=`, `+`,
+`-`, or `@`, spreadsheet software may interpret it as a formula when an
+administrator opens the report. The output directory also has no explicit
+restrictive ACL despite containing detailed account and distinguished-name data.
+
+Recommendation: neutralize formula-leading cells and create the output directory
+with an ACL limited to the monitoring identity and intended readers.
+
+### Low: GitHub token is placed in a process argument
+
+File: `github-latest-tag.sh:65-73`
+
+The authorization header, including `GITHUB_TOKEN`, is passed in curl's command
+line. Depending on operating-system process visibility, another local user may
+be able to observe it.
+
+Recommendation: use a protected curl configuration or credential mechanism that
+does not expose the token in process arguments.
+
+### Low: Sensitive inventory reports use default permissions
+
+File: `find-backup-files.sh:7`, `find-backup-files.sh:57-68`
+
+The report lists SSH files, database files, archives, and other sensitive paths,
+but no restrictive `umask` is set.
+
+Recommendation: set `umask 077` before creating the report.
+
+### Low: Dynamic command execution is unrestricted by design
+
+File: `Watch-Command.ps1:8-24`
+
+`Invoke-Expression` executes the supplied string as PowerShell code. This is
+expected for an interactive watch helper, but it is unsafe if callers pass data
+from another user, file, service, or automation boundary.
+
+Recommendation: accept a `[scriptblock]` and invoke it with `&`, and document
+that the input must be trusted.
+
+### Low: SSH target can be interpreted as an option
+
+File: `ssh-wait.sh:12-17`
+
+A target beginning with `-` may be interpreted by `ssh` as another option. This
+can become dangerous if the target originates outside the invoking user's trust
+boundary, especially with options such as `ProxyCommand`.
+
+Recommendation: validate the target as `user@host`/host syntax and reject values
+beginning with `-`.
+
+## Correctness Defects Affecting Security Operations
+
+- `Monitor-ADGroupChanges.ps1:50` has a missing closing parenthesis and does not
+ parse.
+- `Monitor-ADGroupChanges.ps1:47` uses `$StateFile` before it is initialized in
+ the first implementation block.
+- `gitlab-clone-group.sh:59` has an unterminated quote and is not executable as
+ written.
+- `docker-debug-container.sh:1` uses `"$*"`, collapsing all arguments into one
+ Docker argument.
+- Several GitLab/GCP scripts use unquoted expansions. Current API naming rules
+ reduce direct injection risk, but spaces, globbing, and malformed responses can
+ alter behavior.
+
+## Per-Script Disposition
+
+| Script | Risk | Assessment |
+|---|---:|---|
+| `bash-3-version-check-mac.sh` | None | No material security issue. |
+| `bash-5-version-check-mac.sh` | None | No material security issue. |
+| `clean-storage-safe.sh` | Low | Destructive by purpose, but fixed `PATH`, quoting, prompts, and non-following `find` defaults are good controls. Running the whole script with `sudo` changes `HOME` behavior and blast radius; prefer per-command elevation. |
+| `combine-md.sh` | Low | Overwrites `combined.md` in the current directory by design; avoid privileged or attacker-controlled directories. |
+| `diagnose-disk-space-ubuntu.sh` | None | Read-only; destructive commands are printed only. |
+| `docker-debug-container.sh` | Low | No injection found; argument collapsing is a correctness issue. Container image trust remains the caller's responsibility. |
+| `docker-list-tags-remote.sh` | Low | Uses a deprecated endpoint and fragile text parsing; no direct code-execution path found. |
+| `example-deploy.sh` | Medium | Root install trusts mutable upstream tags and repository content. |
+| `expand_root_volume.sh` | Medium | Ignores the detected root device and always modifies `/dev/vda2`, even after confirming a different device. This can corrupt the wrong partition. |
+| `file-manager.html` | Medium | Persistent DOM injection through stored filenames. |
+| `find-backup-files.sh` | Low | Sensitive path inventory is written with default permissions. |
+| `gcp-subnets-enable-flow-logs.sh` | Low | Privileged cloud-wide configuration change lacks confirmation and quotes; no shell code injection found under normal GCP naming rules. |
+| `generate-pdf-from-tex.ps1` | High | Untrusted TeX can execute commands; writable bind mount and mutable image increase risk. |
+| `get-ip-from-domain-names.sh` | None | Input is quoted; no material security issue. |
+| `git-delete-merged-branches.sh` | Medium | Predictable `/tmp` file enables symlink attacks under elevated execution. |
+| `git-pull-recursive.sh` | Low | Pulls and merges untrusted remote content across many repositories; no automatic code execution in this script. |
+| `git-remotes-recursive.sh` | None | Read-only repository inspection. |
+| `git-status-directories.sh` | None | Read-only repository inspection; missing argument validation is operational. |
+| `github-latest-release.sh` | None | Quoted HTTPS API request; no material issue found. |
+| `github-latest-tag.sh` | Low | Token may be visible in process arguments; prefix is treated as a regex rather than a literal. |
+| `gitlab-check-image-publish-time.sh` | Low | Unquoted API path components and multiple registry IDs can produce unintended requests; identifiers come from trusted CLI/API contexts. |
+| `gitlab-clone-group.sh` | Low | Unquoted expansions and malformed recursion are unsafe operationally; script also has a syntax error. |
+| `gitlab-clone-projects-recursive.sh` | Low | Unquoted clone URL/path and trusted-JSON assumptions can mis-handle malformed project data. Validate paths remain below `BASE_DIR`. |
+| `gitlab-get-project-id-from-current-repo.sh` | None | Uses structured `jq` arguments and URL encoding; no material issue found. |
+| `gitlab-list-registry-images.sh` | Low | Unquoted identifiers and word splitting can target unintended tags. |
+| `http-wait.sh` | Low | Permits cleartext HTTP and arbitrary destinations by design; do not use it as a security/identity check. |
+| `hub-sync-recursive.sh` | Low | Broadly mutates local branches based on remotes; remote names are quoted at execution. |
+| `is-command-in-path.sh` | None | No material security issue. |
+| `java_home.sh` | Low | Unquoted version argument can be split into extra tool arguments if exposed to untrusted input. |
+| `Monitor-ADGroupChanges.ps1` | Medium | CSV formula/ACL risk; currently fails to parse. |
+| `python-run-on-change.sh` | Low | Repeatedly executes the watched Python file; safe only when that file and its directory are trusted. |
+| `rm-recursive-.gradle.sh` | High | Recursive deletion can traverse directory symlinks outside the starting tree. |
+| `rm-recursive-build.sh` | High | Recursive deletion can traverse directory symlinks outside the starting tree. |
+| `rm-recursive-node_modules.sh` | High | Recursive deletion can traverse directory symlinks outside the starting tree. |
+| `rm-recursive-postgres-data.sh` | High | Recursive deletion can traverse directory symlinks outside the starting tree. |
+| `setup-kali.sh` | High | Root execution of unverified archives, repositories, and package installation. |
+| `ssh-wait.sh` | Low | SSH target option injection is possible if target input is untrusted. |
+| `storage_diagnose.sh` | Medium | Predictable privileged `/tmp` report and sensitive output permissions. |
+| `tag.sh` | Low | Pushes all local tags, not only the new tag; compromised local tags could be published. |
+| `update-apt.sh` | Low | Performs broad unattended package changes as root; repository trust and package-manager signatures are the primary controls. |
+| `utils.sh` | None | No material security issue. |
+| `Watch-Command.ps1` | Low | Arbitrary expression execution by design; only trusted callers should supply commands. |
+| `zfsdash.py` | Medium | Unauthenticated all-interface status disclosure and remotely triggerable subprocess load. |
+| `macos/ruby/chruby_local.sh` | Medium | Downloads and installs an archive without checksum/signature verification. |
+| `macos/ruby/ruby-install_local.sh` | Medium | Downloads installer code without verification, then uses it to fetch/build Ruby. |
+| `python/clone-gitlab-group.py` | Low | Destination paths are derived from API fields without containment checks; GitLab path rules normally constrain them. `subprocess.run` does not use a shell. |
+| `python/list-gitlab-projects.py` | None | Token is read from the environment and passed through the library; no material issue found. |
+| `tampermonkey/edx-download-transcripts.js` | High | Plaintext, mutable remote script dependency permits browser-context code injection. |
+
+## Priority Order
+
+1. Remove the HTTP userscript dependency.
+2. Replace the four recursive deletion implementations with symlink-safe traversal.
+3. Harden or retire `setup-kali.sh`.
+4. Remove `-shell-escape` and pin the TeX image.
+5. Bind `zfsdash.py` to localhost and add access controls for remote use.
+6. Replace filename `innerHTML` rendering in `file-manager.html`.
+7. Replace predictable `/tmp` files and restrict report permissions.
diff --git a/Watch-Command.ps1 b/Watch-Command.ps1
index f3e7f37..3d2eb3f 100644
--- a/Watch-Command.ps1
+++ b/Watch-Command.ps1
@@ -1,15 +1,13 @@
-# Watch-Command.ps1 -Command "Get-Process" -Interval 2 -Clear
-# Add to PowerShell profile: Set-Alias -Name watch -Value Watch-Command
-# Add to PowerShell profile: Set-Alias -Name w -Value Watch-Command
-#
-# Open your PowerShell profile: notepad $profile
-# copy and paste the following code into your PowerShell profile
-
function Watch-Command {
+ [CmdletBinding()]
param (
- [string]$Command,
+ [Parameter(Mandatory)]
+ [scriptblock]$Command,
+
+ [ValidateRange(1, 86400)]
[int]$Interval = 2,
- [switch]$Clear = $false
+
+ [switch]$Clear
)
while ($true) {
@@ -19,8 +17,7 @@ function Watch-Command {
Get-Date
"Command: $Command"
"Interval: $Interval"
- "Clear: $Clear"
- Invoke-Expression $Command
+ & $Command
Start-Sleep -Seconds $Interval
}
}
diff --git a/clean-storage-safe.sh b/clean-storage-safe.sh
index e9cf6ae..eefe92b 100755
--- a/clean-storage-safe.sh
+++ b/clean-storage-safe.sh
@@ -28,6 +28,14 @@ set -Eeuo pipefail
PATH=/usr/sbin:/usr/bin:/sbin:/bin
+if [[ $EUID -eq 0 && -n "${SUDO_USER:-}" && "$SUDO_USER" != "root" ]]; then
+ USER_HOME="$(getent passwd "$SUDO_USER" | cut -d: -f6)"
+ if [[ -n "$USER_HOME" && -d "$USER_HOME" ]]; then
+ HOME="$USER_HOME"
+ export HOME
+ fi
+fi
+
DIVIDER="===================================================================="
say() {
diff --git a/docker-debug-container.sh b/docker-debug-container.sh
index 97943f7..50195ae 100755
--- a/docker-debug-container.sh
+++ b/docker-debug-container.sh
@@ -1 +1,4 @@
-docker run -it --entrypoint /bin/sh --rm "$*"
+#!/usr/bin/env bash
+set -Eeuo pipefail
+[[ $# -gt 0 ]] || { echo "Usage: $0 IMAGE [docker-run-args...]" >&2; exit 2; }
+docker run -it --entrypoint /bin/sh --rm "$@"
diff --git a/example-deploy.sh b/example-deploy.sh
index 1d86c4f..a154642 100644
--- a/example-deploy.sh
+++ b/example-deploy.sh
@@ -1,122 +1,5 @@
-#!/bin/bash
-
-# colors
-reset='\033[0m'
-red='\033[0;31m'
-yellow='\033[1;33m'
-green='\033[0;32m'
-blue='\033[0;34m'
-
-
-# Check user's permissions
-if [[ $(id -u) != 0 ]]; then
- echo -e "${red}This script must be run as root.${reset}" >&2
- exit 1
-fi
-
-# Check if user has a home
-if [[ -z "$HOME" || $HOME == "/nonexistent" ]]; then
- echo -e "${red}This script requires a home directory.${reset}" >&2
- exit 1
-fi
-
-
-update_repo() {
- local script_dir="$1"
- cd "$script_dir" || return 1
- git reset --hard &>/dev/null
- git fetch --tags &>/dev/null
- echo
- echo -e "${blue}Checking out the latest release...${reset}"
- if ! git checkout --force "$(git describe --tags "$(git rev-list --tags --max-count=1)")" &>/dev/null; then
- echo "${red}Failed to check out the latest release.${reset}"
- return 1
- else
- echo -e "${green}Successfully checked out the latest release.${reset}"
- return 0
- fi
-}
-
-
-# Define variables
-script_name='heavyscript'
-script_dir="$HOME/heavy_script"
-bin_dir="$HOME/bin"
-script_wrapper="$bin_dir/$script_name"
-
-main() {
- # Check if the script repository already exists
- if [[ -d "$script_dir" ]]; then
- echo -e "${yellow}The ${blue}$script_name${yellow} repository already exists.${reset}"
- if [[ -d "$script_dir/.git" ]]; then
- echo -e "${blue}Reinstalling $script_name repository...${reset}"
- if ! update_repo "$script_dir"; then
- echo -e "${red}Failed to reinstall the repository${reset}"
- exit 1
- else
- echo -e "${green}Successfully reinstalled the repository${reset}"
- fi
- else
- # Convert the directory into a git repository
- echo -e "${blue}Converting it into a git repository...${reset}"
- cd "$script_dir" || exit 1
- git init
- git remote add origin "https://github.com/Heavybullets8/heavy_script.git"
- if ! update_repo "$script_dir"; then
- echo "${red}Failed to convert to git repository${reset}"
- exit 1
- else
- echo -e "${green}Successfully converted to git repository${reset}"
- fi
- fi
- else
- # Clone the script repository
- echo -e "${blue}Cloning $script_name repository...${reset}"
- cd "$HOME" || exit 1
- if ! git clone "https://github.com/Heavybullets8/heavy_script.git" &>/dev/null; then
- echo -e "${red}Failed to clone the repository${reset}"
- exit 1
- else
- echo -e "${green}Successfully cloned the repository${reset}"
- fi
-
- cd "$script_dir" || exit 1
- if ! update_repo "$script_dir"; then
- exit 1
- fi
- fi
-
- echo
-
- # Create the bin directory if it does not exist
- if [[ ! -d "$bin_dir" ]]; then
- echo -e "${blue}Creating $bin_dir directory...${reset}"
- mkdir "$bin_dir"
- fi
-
- # Create symlink inside bin, to the script
- echo -e "${blue}Creating $script_wrapper wrapper...${reset}"
- ln -sf "$script_dir/bin/$script_name" "$script_wrapper"
- chmod +x "$script_dir/bin/$script_name"
-
- echo
-
- # Add $HOME/bin to PATH in .bashrc and .zshrc
- for rc_file in .bashrc .zshrc; do
- if [[ ! -f "$HOME/$rc_file" ]]; then
- echo -e "${blue}Creating $HOME/$rc_file file...${reset}"
- touch "$HOME/$rc_file"
- fi
-
- if ! grep -q "$bin_dir" "$HOME/$rc_file"; then
- echo -e "${blue}Adding $bin_dir to $rc_file...${reset}"
- echo "export PATH=$bin_dir:\$PATH" >> "$HOME/$rc_file"
- fi
- done
-
- echo
- echo -e "${green}Successfully installed ${blue}$script_name${reset}"
- echo
-}
-
-main
+#!/usr/bin/env bash
+echo "example-deploy.sh has been retired." >&2
+echo "It installed mutable upstream Git tags as root without signature verification." >&2
+echo "Install a reviewed, signed release through a dedicated deployment process." >&2
+exit 1
diff --git a/expand_root_volume.sh b/expand_root_volume.sh
index a52e678..498a067 100755
--- a/expand_root_volume.sh
+++ b/expand_root_volume.sh
@@ -1,44 +1,39 @@
-#!/bin/bash
-# expand_root_volume.sh - Safely resize root partition and ext4 filesystem
-# 2025-08-04
+#!/usr/bin/env bash
+# Safely resize a directly mounted ext4 root partition and filesystem.
+set -Eeuo pipefail
-set -euo pipefail
-
-echo "π Checking required tools..."
-if ! command -v growpart &>/dev/null; then
- echo "Installing growpart (cloud-guest-utils)..."
- sudo apt update
- sudo apt install -y cloud-guest-utils
-fi
-
-ROOT_MOUNT_DEVICE=$(findmnt -n -o SOURCE /)
-ROOT_FS_TYPE=$(findmnt -n -o FSTYPE /)
+ROOT_PART="$(findmnt -n -o SOURCE /)"
+ROOT_FS_TYPE="$(findmnt -n -o FSTYPE /)"
if [[ "$ROOT_FS_TYPE" != "ext4" ]]; then
- echo "β Root filesystem is not ext4 (found: $ROOT_FS_TYPE). Aborting."
+ echo "Root filesystem is not ext4 (found: $ROOT_FS_TYPE)." >&2
exit 1
fi
-
-if [[ "$ROOT_MOUNT_DEVICE" != /dev/vda2 ]]; then
- echo "β οΈ Warning: root is not mounted on /dev/vda2 (found: $ROOT_MOUNT_DEVICE)."
- read -rp "Do you want to proceed with $ROOT_MOUNT_DEVICE? (y/n): " confirm
- if [[ "$confirm" != "y" ]]; then
- echo "Aborting."
- exit 1
- fi
+if [[ "$ROOT_PART" != /dev/* || "$(lsblk -ndo TYPE "$ROOT_PART")" != "part" ]]; then
+ echo "Root must be mounted directly from a disk partition; found: $ROOT_PART" >&2
+ exit 1
fi
-ROOT_DEV="/dev/vda"
-PART_NUM="2"
-PART="${ROOT_DEV}${PART_NUM}"
-
-echo "β Detected root on $PART with ext4 filesystem."
+PARENT_NAME="$(lsblk -ndo PKNAME "$ROOT_PART")"
+PART_NUM="$(lsblk -ndo PARTN "$ROOT_PART")"
+if [[ -z "$PARENT_NAME" || ! "$PART_NUM" =~ ^[0-9]+$ ]]; then
+ echo "Unable to identify the parent disk and partition number for $ROOT_PART." >&2
+ exit 1
+fi
+ROOT_DEV="/dev/$PARENT_NAME"
+
+echo "Root partition: $ROOT_PART"
+echo "Parent disk: $ROOT_DEV"
+echo "Partition: $PART_NUM"
+lsblk -f "$ROOT_DEV"
+read -r -p "Grow $ROOT_PART to available disk space? [y/N]: " reply
+[[ "$reply" =~ ^[Yy]([Ee][Ss])?$ ]] || exit 0
+
+if ! command -v growpart >/dev/null 2>&1; then
+ sudo apt-get update
+ sudo apt-get install -y cloud-guest-utils
+fi
-echo "π§± Expanding partition $PART..."
sudo growpart "$ROOT_DEV" "$PART_NUM"
-
-echo "π Resizing ext4 filesystem on $PART..."
-sudo resize2fs "$PART"
-
-echo "β Expansion complete. Final disk usage:"
+sudo resize2fs "$ROOT_PART"
df -h /
diff --git a/file-manager.html b/file-manager.html
index e04ca89..8fc15fc 100644
--- a/file-manager.html
+++ b/file-manager.html
@@ -129,7 +129,7 @@
Browser File Vault
Upload File
Drag & drop a file here
-
β or β
+
-- or --
@@ -246,7 +246,7 @@
Stored Files
// 5. Load and Display Files
function loadFiles() {
const listElement = document.getElementById('fileList');
- listElement.innerHTML = ''; // Clear current list
+ listElement.replaceChildren();
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
@@ -263,7 +263,10 @@