From 34ec391b229dc9e3492670606aa60a44d3c4c1b7 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 8 Jun 2026 12:12:39 -0500 Subject: [PATCH 1/2] docs: add script security assessment --- SECURITY_ASSESSMENT.md | 263 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 SECURITY_ASSESSMENT.md 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. From 2bd5a78f25716a0eafd16039d02215db07c700c5 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 9 Jun 2026 21:31:03 -0500 Subject: [PATCH 2/2] Add security validation CI and script hardening Introduce a Security validation GitHub Actions workflow and tests, add a .gitignore for __pycache__, and harden many scripts across the repo. Changes include: atomic, ACL-restricted state handling and CSV safety for Monitor-ADGroupChanges.ps1; safer Watch-Command.ps1 using a scriptblock param; stricter shell scripts (set -Eeuo pipefail, input validation, umask, safer tmp files) and retire/disable unsafe installers (example-deploy.sh, gitlab-clone-group.sh). Improve utilities (docker-debug-container, expand_root_volume, gcp-subnets-enable-flow-logs, git-delete-merged-branches, github-latest-tag, gitlab image/tag checks), tighten container invocation for TeX PDF generation (digest-pinned image, reduced privileges), JS/HTML DOM updates in file-manager, and add tests/test_security_regressions.py. Overall focus: automated parsing/testing, input validation, least-privilege execution, safer file handling, and retiring unsafe prototypes. --- .github/workflows/security-validation.yml | 77 ++++++++ .gitignore | 1 + Monitor-ADGroupChanges.ps1 | 203 +++++++++------------ README.md | 33 ++-- Watch-Command.ps1 | 19 +- clean-storage-safe.sh | 8 + docker-debug-container.sh | 5 +- example-deploy.sh | 127 +------------ expand_root_volume.sh | 63 +++---- file-manager.html | 48 +++-- find-backup-files.sh | 2 + gcp-subnets-enable-flow-logs.sh | 40 +++- generate-pdf-from-tex.ps1 | 52 +++++- git-delete-merged-branches.sh | 21 ++- git-status-directories.sh | 2 +- github-latest-tag.sh | 21 ++- gitlab-check-image-publish-time.sh | 59 +++--- gitlab-clone-group.sh | 64 +------ gitlab-clone-projects-recursive.sh | 159 ++++------------ gitlab-get-project-id-from-current-repo.sh | 4 +- gitlab-list-registry-images.sh | 30 +-- http-wait.sh | 27 ++- java_home.sh | 3 +- macos/ruby/chruby_local.sh | 23 +-- macos/ruby/ruby-install_local.sh | 16 +- python/clone-gitlab-group.py | 27 ++- rm-recursive-.gradle.sh | 59 ++++-- rm-recursive-build.sh | 57 ++++-- rm-recursive-node_modules.sh | 57 ++++-- rm-recursive-postgres-data.sh | 57 ++++-- setup-kali.sh | 187 +------------------ ssh-wait.sh | 31 ++-- storage_diagnose.sh | 3 +- tag.sh | 3 +- tampermonkey/edx-download-transcripts.js | 119 +++++------- tests/test_security_regressions.py | 87 +++++++++ update-apt.sh | 24 ++- zfsdash.py | 58 ++++-- 38 files changed, 912 insertions(+), 964 deletions(-) create mode 100644 .github/workflows/security-validation.yml create mode 100644 .gitignore create mode 100644 tests/test_security_regressions.py 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/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 @@

Stored Files

cursor.continue(); } else { if (!hasFiles) { - listElement.innerHTML = '
  • No files stored yet.
  • '; + const emptyItem = document.createElement('li'); + emptyItem.style.cssText = 'text-align:center; padding:1rem; color:#888;'; + emptyItem.textContent = 'No files stored yet.'; + listElement.appendChild(emptyItem); } } }; @@ -279,17 +282,34 @@

    Stored Files

    const sizeStr = formatSize(fileRecord.size); const dateStr = fileRecord.created.toLocaleDateString() + ' ' + fileRecord.created.toLocaleTimeString(); - li.innerHTML = ` -
    - ${fileRecord.name} - ${sizeStr} β€’ ${dateStr} -
    -
    - Download - -
    - `; - + const info = document.createElement('div'); + info.className = 'file-info'; + + const name = document.createElement('span'); + name.className = 'file-name'; + name.textContent = fileRecord.name; + + const meta = document.createElement('span'); + meta.className = 'file-meta'; + meta.textContent = `${sizeStr} - ${dateStr}`; + info.append(name, meta); + + const actions = document.createElement('div'); + actions.className = 'actions'; + + const downloadLink = document.createElement('a'); + downloadLink.href = fileUrl; + downloadLink.download = fileRecord.name; + downloadLink.className = 'btn-download'; + downloadLink.textContent = 'Download'; + + const deleteButton = document.createElement('button'); + deleteButton.type = 'button'; + deleteButton.className = 'btn-danger'; + deleteButton.textContent = 'Delete'; + deleteButton.addEventListener('click', () => deleteFile(id)); + actions.append(downloadLink, deleteButton); + li.append(info, actions); container.appendChild(li); } diff --git a/find-backup-files.sh b/find-backup-files.sh index d4d8269..b527d14 100755 --- a/find-backup-files.sh +++ b/find-backup-files.sh @@ -3,6 +3,8 @@ # Script to identify files that might need to be backed up on Ubuntu # Usage: find-backup-files.sh [days_modified] +umask 077 + DAYS=${1:-7} OUTPUT_FILE="backup-candidates-$(date +%Y%m%d-%H%M%S).txt" diff --git a/gcp-subnets-enable-flow-logs.sh b/gcp-subnets-enable-flow-logs.sh index 1fac506..36f7be8 100755 --- a/gcp-subnets-enable-flow-logs.sh +++ b/gcp-subnets-enable-flow-logs.sh @@ -1,14 +1,34 @@ -#!/bin/sh +#!/usr/bin/env bash +set -Eeuo pipefail -# Currently working in project: -PROJECT=$(gcloud config get-value project) -echo "Working in project: $PROJECT" +ASSUME_YES=false +[[ "${1:-}" == "--yes" ]] && ASSUME_YES=true -# List all the subnets in the project -# gcloud compute networks subnets list --format="table(name,region)" +PROJECT="$(gcloud config get-value project 2>/dev/null)" +if [[ -z "$PROJECT" || "$PROJECT" == "(unset)" ]]; then + echo "No active Google Cloud project is configured." >&2 + exit 1 +fi -# List all subnets and their respective regions, then enable flow logs -gcloud compute networks subnets list --format="csv[no-heading](name,region)" | while IFS=, read -r name region; do - echo "Enabling Flow Logs for subnet $name in $region..." - gcloud compute networks subnets update $name --region=$region --enable-flow-logs +mapfile -t SUBNETS < <(gcloud compute networks subnets list --format="csv[no-heading](name,region)") +if [[ ${#SUBNETS[@]} -eq 0 ]]; then + echo "No subnets found in project $PROJECT." + exit 0 +fi + +echo "Project: $PROJECT" +printf 'Subnets to update:\n' +printf ' %s\n' "${SUBNETS[@]}" +if ! $ASSUME_YES; then + read -r -p "Enable VPC Flow Logs on every listed subnet? [y/N]: " reply + [[ "$reply" =~ ^[Yy]([Ee][Ss])?$ ]] || exit 0 +fi + +for subnet in "${SUBNETS[@]}"; do + IFS=, read -r name region <<< "$subnet" + [[ -n "$name" && -n "$region" ]] || { + echo "Malformed subnet record: $subnet" >&2 + exit 1 + } + gcloud compute networks subnets update "$name" --region="$region" --enable-flow-logs done diff --git a/generate-pdf-from-tex.ps1 b/generate-pdf-from-tex.ps1 index c1af7e7..3c024a5 100644 --- a/generate-pdf-from-tex.ps1 +++ b/generate-pdf-from-tex.ps1 @@ -1,3 +1,49 @@ -docker run -i --rm --name latex -v ${PWD}:/usr/src/app -w /usr/src/app texlive/texlive:latest pdflatex -shell-escape week_8.tex -# Example .tex in mcso-ala-matlab/week_8/week_8.tex -# ${PWD} for powershell, $PWD for bash +[CmdletBinding()] +param( + [Parameter()] + [string]$InputFile = "week_8.tex", + + [Parameter()] + [string]$OutputDirectory = "latex-output", + + [Parameter()] + [switch]$AllowShellEscape, + + [Parameter()] + [string]$Image = $env:TEXLIVE_IMAGE +) + +$ErrorActionPreference = "Stop" + +if ([string]::IsNullOrWhiteSpace($Image) -or $Image -notmatch '@sha256:[0-9a-fA-F]{64}$') { + throw "Set -Image or TEXLIVE_IMAGE to a TeX Live image pinned by sha256 digest." +} + +$inputPath = (Resolve-Path -LiteralPath $InputFile).Path +if ([System.IO.Path]::GetExtension($inputPath) -ne ".tex") { + throw "InputFile must be a .tex file." +} + +$sourceDirectory = Split-Path -Parent $inputPath +$inputName = Split-Path -Leaf $inputPath +$outputPath = [System.IO.Path]::GetFullPath((Join-Path $PWD $OutputDirectory)) +[System.IO.Directory]::CreateDirectory($outputPath) | Out-Null + +$pdflatexArgs = @("-interaction=nonstopmode", "-halt-on-error", "-output-directory=/output") +if ($AllowShellEscape) { + Write-Warning "Shell escape allows the TeX document to execute commands inside the container." + $pdflatexArgs += "-shell-escape" +} +else { + $pdflatexArgs += "-no-shell-escape" +} +$pdflatexArgs += $inputName + +docker run --rm ` + --network none ` + --read-only ` + --tmpfs /tmp:rw,noexec,nosuid,size=256m ` + --mount "type=bind,source=$sourceDirectory,target=/input,readonly" ` + --mount "type=bind,source=$outputPath,target=/output" ` + --workdir /input ` + $Image pdflatex @pdflatexArgs diff --git a/git-delete-merged-branches.sh b/git-delete-merged-branches.sh index f6c4bd8..6a9de35 100755 --- a/git-delete-merged-branches.sh +++ b/git-delete-merged-branches.sh @@ -1,3 +1,20 @@ -#!/bin/sh -git branch --merged | grep -v "^\(\* \)\?main$" > /tmp/merged-branches && nvim /tmp/merged-branches && xargs git branch -d "$branch_file" || true + +if [[ ! -s "$branch_file" ]]; then + echo "No merged branches found." + exit 0 +fi + +"${EDITOR:-nvim}" "$branch_file" +while IFS= read -r branch; do + [[ -n "$branch" && "$branch" != -* ]] || continue + git branch -d -- "$branch" +done < "$branch_file" diff --git a/git-status-directories.sh b/git-status-directories.sh index 4eebab0..e09c894 100755 --- a/git-status-directories.sh +++ b/git-status-directories.sh @@ -27,4 +27,4 @@ list_git_status_recursive() { } # Start the recursive search from the current directory -list_git_status_recursive $1 +list_git_status_recursive "${1:-.}" diff --git a/github-latest-tag.sh b/github-latest-tag.sh index 8c3600f..0767cfb 100755 --- a/github-latest-tag.sh +++ b/github-latest-tag.sh @@ -55,6 +55,10 @@ if [[ -z "$OWNER" || -z "$REPO" ]]; then echo "Usage: $0 [--prefix ] [--porcelain]" exit 1 fi +if [[ ! "$OWNER" =~ ^[A-Za-z0-9_.-]+$ || ! "$REPO" =~ ^[A-Za-z0-9_.-]+$ ]]; then + echo "Owner and repository contain unsupported characters." >&2 + exit 1 +fi BASE_URL="https://api.github.com/repos/${OWNER}/${REPO}/tags" PER_PAGE=100 @@ -62,15 +66,18 @@ PAGE=1 all_tags="" # --- Authentication (optional) --- -AUTH_HEADER=() -if [[ -n "${GITHUB_TOKEN:-}" ]]; then - AUTH_HEADER=(-H "Authorization: token ${GITHUB_TOKEN}") -fi - # --- Fetch all pages of tags --- while :; do API_URL="${BASE_URL}?per_page=${PER_PAGE}&page=${PAGE}" - response=$(curl -sSL "${AUTH_HEADER[@]}" "$API_URL") + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + response=$( + printf 'header = "Authorization: Bearer %s"\nurl = "%s"\nsilent\nshow-error\nfail\nlocation\n' \ + "$GITHUB_TOKEN" "$API_URL" | + curl --config - + ) + else + response=$(curl --fail --silent --show-error --location "$API_URL") + fi count=$(echo "$response" | jq 'length') if [[ "$count" -eq 0 ]]; then @@ -93,7 +100,7 @@ fi # --- Apply prefix filter if provided --- if [[ -n "$PREFIX" ]]; then - tags=$(echo "$tags" | grep "^${PREFIX}" || true) + tags=$(while IFS= read -r tag; do [[ "$tag" == "$PREFIX"* ]] && printf '%s\n' "$tag"; done <<< "$tags") if [[ -z "$tags" ]]; then echo "❌ No tags found for ${OWNER}/${REPO} with prefix '${PREFIX}'" exit 1 diff --git a/gitlab-check-image-publish-time.sh b/gitlab-check-image-publish-time.sh index 32f25c9..e0e5e76 100755 --- a/gitlab-check-image-publish-time.sh +++ b/gitlab-check-image-publish-time.sh @@ -1,39 +1,30 @@ -#!/bin/sh - -# Example usage: -# PROJECT_ID="1234" -# TARGET_TAG="tag-name" -# check-image-publish-time.sh "$PROJECT_ID" "$TARGET_TAG" - -# set -x - -PROJECT_ID="$1" -TARGET_TAG="$2" -TAG_FOUND="false" -REGISTRY_ID=$(glab api "projects/$PROJECT_ID/registry/repositories" | jq '.[] | .id') -TAG_JSON=$(glab api projects/$PROJECT_ID/registry/repositories/$REGISTRY_ID/tags/$TARGET_TAG) - -# Check if the tag exists and get its creation timestamp -CREATED_AT=$(echo "$TAG_JSON" | jq -r ". | .created_at") - -if [ -n "$CREATED_AT" ]; then - TAG_FOUND="true" +#!/usr/bin/env bash +set -Eeuo pipefail + +PROJECT_ID="${1:-}" +TARGET_TAG="${2:-}" +[[ "$PROJECT_ID" =~ ^[0-9]+$ && -n "$TARGET_TAG" && "$TARGET_TAG" != -* ]] || { + echo "Usage: $0 PROJECT_ID TAG" >&2 + exit 2 +} + +mapfile -t REGISTRY_IDS < <(glab api "projects/$PROJECT_ID/registry/repositories" | jq -er '.[].id') +if [[ ${#REGISTRY_IDS[@]} -ne 1 || ! "${REGISTRY_IDS[0]}" =~ ^[0-9]+$ ]]; then + echo "Expected exactly one container registry repository." >&2 + exit 1 fi -# Display the result -if [ "$TAG_FOUND" = "true" ]; then - # echo "Found tag '${TARGET_TAG}' in the container registry." - - # Calculate the time difference between the current time and the image creation time - CURRENT_TIME=$(date -u +%s) - # CREATED_TIME=$(date -u -d "$CREATED_AT" +%s) # doesn't work on mac - CREATED_TIME=$(date -u -jf "%Y-%m-%dT%H:%M:%S" "${CREATED_AT%Z}" +%s 2> /dev/null) - TIME_DIFF=$((CURRENT_TIME - CREATED_TIME)) - - # Convert the time difference to a human-readable format - TIME_DIFF_HUMAN=$(printf '%dd %dh %dm %ds' $((TIME_DIFF/86400)) $((TIME_DIFF%86400/3600)) $((TIME_DIFF%3600/60)) $((TIME_DIFF%60))) +ENCODED_TAG="$(jq -rn --arg value "$TARGET_TAG" '$value|@uri')" +TAG_JSON="$(glab api "projects/$PROJECT_ID/registry/repositories/${REGISTRY_IDS[0]}/tags/$ENCODED_TAG")" +CREATED_AT="$(jq -er '.created_at' <<< "$TAG_JSON")" - echo "Tag ${TARGET_TAG} published ${TIME_DIFF_HUMAN} ago." +if date --version >/dev/null 2>&1; then + CREATED_TIME="$(date -u -d "$CREATED_AT" +%s)" else - echo "Tag ${TARGET_TAG} not found in the container registry." + CREATED_TIME="$(date -u -jf "%Y-%m-%dT%H:%M:%S" "${CREATED_AT%Z}" +%s)" fi +CURRENT_TIME="$(date -u +%s)" +TIME_DIFF=$((CURRENT_TIME - CREATED_TIME)) +printf 'Tag %s published %dd %dh %dm %ds ago.\n' "$TARGET_TAG" \ + $((TIME_DIFF / 86400)) $((TIME_DIFF % 86400 / 3600)) \ + $((TIME_DIFF % 3600 / 60)) $((TIME_DIFF % 60)) diff --git a/gitlab-clone-group.sh b/gitlab-clone-group.sh index 2a65974..01b5c12 100755 --- a/gitlab-clone-group.sh +++ b/gitlab-clone-group.sh @@ -1,60 +1,4 @@ -#! /bin/zsh - -#echo "TODO: this script is WIP" -#exit 1 - -usage() { - echo "Usage: $0 " -} - -# set -x - -if [ -z "$1" ]; then # if $1 is empty - usage - exit 1 -fi - -# Remove if using group ID instead of path -# if [ ! -d "$1" ]; then # if $1 directory does not exist - # echo "Creating directory: $1" - # mkdir -p "$1" -# fi - -# glab api groups/$1/projects | jq -r '.[].web_url' | xargs -n1 -I {} echo "git clone {} $1" -# glab api groups/$1/projects | jq -r '.[].web_url' | xargs -n1 -I {} git clone --recurse-submodules {} $1 - - -# jq explanation: -# -c # compact output -# .[] # for each item in array -echo "Fetching projects for group: $1" -PROJECTS=$(glab api groups/$1/projects | jq -c ".[]") -echo "PROJECTS=$PROJECTS" - -# echo $PROJECTS | jq -r '.[].web_url' | xargs -n1 -I {} echo "git clone --recurse-submodules {} $1" - -echo $PROJECTS | while IFS= read -r PROJECT; do - # get the project.name from jq - PROJECT_NAME=$(echo $PROJECT | jq -r '.name') - echo "PROJECT_NAME: $PROJECT_NAME" - URL=$(echo $PROJECT | jq -r '.web_url') - DIRECTORY=$1/$PROJECT_NAME - echo " git clone $URL $DIRECTORY" - git clone --recurse-submodules $URL $DIRECTORY -done - -echo "Done cloning projects for group: $1" - -SUBGROUPS=$(glab api groups/$1/subgroups | jq -c ".[]") - -echo $SUBGROUPS | while IFS= read -r GROUP; do - echo "TODO looks like this loop breaks when newline are in the string" - echo "GROUP: $GROUP" - # GROUP_NAME=$(echo $GROUP | jq -r '.name') - # echo "GROUP_NAME: $GROUP_NAME" - echo "clone subgroups" - gitlab-clone-group.sh $GROUP -done - -echo "Done cloning subgroups for group: $1 -exit 0 +#!/usr/bin/env bash +echo "gitlab-clone-group.sh has been retired because it was incomplete and unsafe." >&2 +echo "Use python/clone-gitlab-group.py instead." >&2 +exit 1 diff --git a/gitlab-clone-projects-recursive.sh b/gitlab-clone-projects-recursive.sh index 304a095..2ef1d2b 100755 --- a/gitlab-clone-projects-recursive.sh +++ b/gitlab-clone-projects-recursive.sh @@ -1,143 +1,56 @@ -#! /bin/zsh -# -# Get the projects json: -# gitlab-clone-projects-recursive.sh . -d --get-projects-file -# -# List projects by path: -# cat GitLab-Projects-2024-05-29T0308.json | jq -sr ".[].path_with_namespace" | sort | moar -# -# Filter for paths that have "security" or "asve" in them. Outputs the paths: -# cat GitLab-Projects-2024-05-13T1454.json | jq -sc "map(select(.path_with_namespace | test(\"(security|asve)\"))) | sort_by(.path_with_namespace) | .[].path_with_namespace" | moar -# cat GitLab-Projects-2024-05-13T1454.json | jq -sc "map(select(.path_with_namespace | test(\"(security|asve)\"))) | sort_by(.path_with_namespace) | .[]" > filtered-projects.json -# gitlab-clone-projects-recursive.sh . -d -f filtered-projects.json +#!/usr/bin/env bash +set -Eeuo pipefail usage() { - echo "Usage: ./gitlab-clone-projects-recursive.sh [-d|--dry-run] [-f|--file GITLAB_PROJECTS_FILE] [--get-projects-file"] + echo "Usage: $0 BASE_DIR [--dry-run] [--file PROJECTS_FILE] [--get-projects-file]" } -should_skip() { - local REPO=$1 - local SKIP_STRINGS_VAR=$2 - - # The (@P) flag is used in zsh to indirectly reference an array variable. This should allow you to pass the name of the array variable as a string to the function and then reference the array indirectly inside the function. - for SKIP_STRING in "${(@P)SKIP_STRINGS_VAR}"; do - if [[ $REPO == *"$SKIP_STRING"* ]]; then - echo "Skipping $REPO because it contains: $SKIP_STRING" - return 0 # Skip - fi - done - return 1 # Do not skip -} - -# should_skip() { -# local REPO=$1 -# local -n SKIP_STRINGS=$2 # error in zsh: bad option: -n -# -# for SKIP_STRING in "${SKIP_STRINGS[@]}"; do -# if [[ $REPO == *"$SKIP_STRING"* ]]; then -# echo "Skipping $REPO because it contains: $SKIP_STRING" -# return 0 # Skip -# fi -# done -# return 1 # Do not skip -# } - -# set -x - -if [ -z "$1" ]; then # if $1 is empty - usage - exit 1 -fi - -if [ ! -d "$1" ]; then # if $1 directory does not exist - echo "Directory $1 does not exist." - exit 1 -fi - -# BASE_DIR="./tmp" -BASE_DIR=$1 +[[ $# -gt 0 ]] || { usage >&2; exit 2; } +BASE_DIR="$(realpath -e -- "$1")" +shift DRY_RUN=false +GET_PROJECTS_FILE=false +GITLAB_PROJECTS_FILE="" -POSITIONAL_ARGS=() while [[ $# -gt 0 ]]; do - case $1 in - -d|--dry-run) - DRY_RUN=true - shift # past value - ;; + case "$1" in + -d|--dry-run) DRY_RUN=true; shift ;; -f|--file) + [[ $# -ge 2 ]] || { usage >&2; exit 2; } GITLAB_PROJECTS_FILE="$2" - shift # past argument - shift # past value - ;; - --get-projects-file) - GET_PROJECTS_FILE=true - shift # past argument - ;; - -*|--*) - usage - exit 1 - ;; - *) - POSITIONAL_ARGS+=("$1") # save positional arg - shift # past argument + shift 2 ;; + --get-projects-file) GET_PROJECTS_FILE=true; shift ;; + -h|--help) usage; exit 0 ;; + *) usage >&2; exit 2 ;; esac done -set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters -if [ "$DRY_RUN" = true ]; then - echo "Dry run enabled" -fi - -echo "BASE_DIR: $BASE_DIR" -echo "GITLAB_HOST: $GITLAB_HOST" - -SKIP_STRINGS=("substring1" - "substring2") - -if [ -z "$GITLAB_PROJECTS_FILE" ]; then - GITLAB_PROJECTS_FILE="GitLab-Projects-$GITLAB_HOST_$(date -u +%Y-%m-%dT%H%M).json" - echo "Getting projects from GitLab API..." - glab api projects --paginate | jq -s "add" | jq -c "sort_by(.path_with_namespace) | .[]" > $GITLAB_PROJECTS_FILE +GITLAB_HOST="${GITLAB_HOST:-gitlab.com}" +if [[ -z "$GITLAB_PROJECTS_FILE" ]]; then + safe_host="${GITLAB_HOST//[^A-Za-z0-9_.-]/_}" + GITLAB_PROJECTS_FILE="GitLab-Projects-${safe_host}_$(date -u +%Y-%m-%dT%H%M).json" + glab api projects --paginate | + jq -s 'add | sort_by(.path_with_namespace) | .[]' -c > "$GITLAB_PROJECTS_FILE" else - echo "GITLAB_PROJECTS_FILE set to $GITLAB_PROJECTS_FILE" -fi - -if [ "$GET_PROJECTS_FILE" = true ]; then - echo "Exiting after getting projects file." - exit 0 + GITLAB_PROJECTS_FILE="$(realpath -e -- "$GITLAB_PROJECTS_FILE")" fi -echo "Found $(cat $GITLAB_PROJECTS_FILE| wc -l | awk '{print $1}') projects from GitLab." +$GET_PROJECTS_FILE && exit 0 -cat $GITLAB_PROJECTS_FILE | while IFS= read -r LINE; do - CLEAN_LINE=$(echo $LINE | tr -d '\r\n') - # CLEAN_LINE=$(echo $LINE | sed 's/\n//g' | sed 's/\r//g') - - REPO_PATH=$BASE_DIR/$(echo $CLEAN_LINE | jq -r .path_with_namespace) - REPO_URL=$(echo $CLEAN_LINE | jq -r .web_url) - - if [ -z "$REPO_PATH" ] || [ -z "$REPO_URL" ]; then - echo "ERROR: jq couldn't parse:" - echo "ERROR: LINE=$LINE" - echo "ERROR: REPO_PATH=$REPO_PATH" - echo "ERROR: REPO_URL=$REPO_URL" - echo "ERROR: Skipping" - continue - fi - - if [ "$DRY_RUN" = true ]; then - echo "Would have cloned: $REPO_URL to: $REPO_PATH" - continue +while IFS= read -r project; do + namespace="$(jq -er '.path_with_namespace | select(type == "string" and length > 0)' <<< "$project")" + repo_url="$(jq -er '.web_url | select(type == "string" and startswith("https://"))' <<< "$project")" + repo_path="$(realpath -m -- "$BASE_DIR/$namespace")" + if [[ "$repo_path" != "$BASE_DIR/"* ]]; then + echo "Refusing project path outside base directory: $namespace" >&2 + exit 1 fi - # Check if REPO_PATH includes any of the skip strings and skip it if so - if should_skip "$REPO_PATH" SKIP_STRINGS; then - continue + if $DRY_RUN; then + printf 'Would clone %s to %s\n' "$repo_url" "$repo_path" + else + mkdir -p -- "$(dirname -- "$repo_path")" + git clone --recurse-submodules -- "$repo_url" "$repo_path" fi - - echo $REPO_PATH $REPO_URL - git clone --recurse-submodules $REPO_URL $REPO_PATH - echo -done +done < "$GITLAB_PROJECTS_FILE" diff --git a/gitlab-get-project-id-from-current-repo.sh b/gitlab-get-project-id-from-current-repo.sh index 13ce95e..7e8148c 100755 --- a/gitlab-get-project-id-from-current-repo.sh +++ b/gitlab-get-project-id-from-current-repo.sh @@ -18,8 +18,8 @@ PROJECT_ID=$(echo "$PROJECTS_JSON" | jq --arg REPO_REMOTE_URL "$REPO_REMOTE_URL" # PROJECT_ID=$(echo "$PROJECTS_JSON" | jq '.[] | .id') # Check if a matching project was found -if [ -n "$PROJECT_ID" ]; then - echo $PROJECT_ID +if [[ "$PROJECT_ID" =~ ^[0-9]+$ ]]; then + echo "$PROJECT_ID" else # echo "No matching project found." exit 1 diff --git a/gitlab-list-registry-images.sh b/gitlab-list-registry-images.sh index 5970281..2fe12e1 100755 --- a/gitlab-list-registry-images.sh +++ b/gitlab-list-registry-images.sh @@ -1,16 +1,22 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ID="$("$SCRIPT_DIR/gitlab-get-project-id-from-current-repo.sh")" +[[ "$PROJECT_ID" =~ ^[0-9]+$ ]] || { echo "Invalid project ID." >&2; exit 1; } -PROJECT_ID=$(gitlab-get-project-id-from-current-repo.sh) -REGISTRY_ID=$(glab api "projects/$PROJECT_ID/registry/repositories" | jq '.[] | .id') -TAGS=$(glab api "projects/$PROJECT_ID/registry/repositories/$REGISTRY_ID/tags" | jq -r '.[] | .name') +mapfile -t REGISTRY_IDS < <(glab api "projects/$PROJECT_ID/registry/repositories" | jq -er '.[].id') +if [[ ${#REGISTRY_IDS[@]} -ne 1 || ! "${REGISTRY_IDS[0]}" =~ ^[0-9]+$ ]]; then + echo "Expected exactly one container registry repository." >&2 + exit 1 +fi -echo "Found tags:" -echo $TAGS - -for tag in $TAGS; do - # Remove quotes using parameter expansion - tag="${tag%\"}" - tag="${tag#\"}" - - gitlab-check-image-publish-time.sh $PROJECT_ID $tag +mapfile -t TAGS < <( + glab api "projects/$PROJECT_ID/registry/repositories/${REGISTRY_IDS[0]}/tags" | + jq -er '.[].name' +) +printf 'Found tags:\n' +printf ' %s\n' "${TAGS[@]}" +for tag in "${TAGS[@]}"; do + "$SCRIPT_DIR/gitlab-check-image-publish-time.sh" "$PROJECT_ID" "$tag" done diff --git a/http-wait.sh b/http-wait.sh index befc4e1..59ce33b 100755 --- a/http-wait.sh +++ b/http-wait.sh @@ -1,23 +1,34 @@ #!/bin/zsh -# Script to wait until an HTTP server is reachable on port 80 -# Usage: http-wait.sh hostname [timeout_seconds] +# Wait until an HTTP or HTTPS URL is reachable. +# Usage: http-wait.sh URL [timeout_seconds] if [ $# -eq 0 ]; then - echo "Usage: $0 hostname [timeout_seconds]" - echo "Example: $0 example.com 5" + echo "Usage: $0 URL [timeout_seconds]" + echo "Example: $0 https://example.com/health 5" exit 1 fi -TARGET=$1 +URL=$1 TIMEOUT=${2:-5} -URL="http://$TARGET" + +if [[ "$URL" != http://* && "$URL" != https://* ]]; then + print -u2 "URL must start with http:// or https://" + exit 1 +fi +if [[ "$TIMEOUT" != <-> || "$TIMEOUT" -lt 1 ]]; then + print -u2 "Timeout must be a positive integer" + exit 1 +fi +if [[ "$URL" == http://* ]]; then + print -u2 "Warning: plain HTTP does not authenticate or encrypt the endpoint." +fi echo "Waiting for HTTP connection to $URL..." -until curl -s --connect-timeout $TIMEOUT --max-time $TIMEOUT "$URL" >/dev/null 2>&1; do +until curl --fail --silent --show-error --connect-timeout "$TIMEOUT" --max-time "$TIMEOUT" "$URL" >/dev/null 2>&1; do print "$(date): Not reachable, retrying in $TIMEOUT seconds..." - sleep $TIMEOUT + sleep "$TIMEOUT" done print "$(date): HTTP server is up! Connection to $URL successful." diff --git a/java_home.sh b/java_home.sh index 3de4e6c..ecde82e 100755 --- a/java_home.sh +++ b/java_home.sh @@ -3,7 +3,8 @@ # Set java_home to the provided version. # Args: $1 = version number (21, 17, etc) set_java_home() { - export JAVA_HOME=$(/usr/libexec/java_home -v $1) + [[ "$1" =~ ^[0-9]+$ ]] || { echo "Java version must be numeric." >&2; return 2; } + export JAVA_HOME=$(/usr/libexec/java_home -v "$1") echo "JAVA_HOME=$JAVA_HOME" } diff --git a/macos/ruby/chruby_local.sh b/macos/ruby/chruby_local.sh index bc9dcb7..c554dde 100755 --- a/macos/ruby/chruby_local.sh +++ b/macos/ruby/chruby_local.sh @@ -1,19 +1,4 @@ -## -# A non-root installation of the latest Ruby with chruby - -VERSION="0.3.9" - -# Install chruby (https://github.com/postmodern/chruby#readme) -mkdir -p $HOME/src -cd $HOME/src -wget -O chruby-$VERSION.tar.gz https://github.com/postmodern/chruby/archive/v$VERSION.tar.gz -tar -xzvf chruby-$VERSION.tar.gz -cd chruby-$VERSION/ -PREFIX=$HOME/.chruby make install - -# Add the source lines to your ~/.bashrc or ~/.zshrc -echo "source ~/.chruby/share/chruby/chruby.sh" >> ~/.bashrc -echo "source ~/.chruby/share/chruby/auto.sh" >> ~/.bashrc - -# Then set whatever Ruby version you want as default -echo "ruby-2.3" > ~/.ruby-version +#!/usr/bin/env bash +echo "chruby_local.sh has been retired." >&2 +echo "Use a currently supported Ruby version manager and verify its signed release." >&2 +exit 1 diff --git a/macos/ruby/ruby-install_local.sh b/macos/ruby/ruby-install_local.sh index 68be32c..fe1d4c5 100755 --- a/macos/ruby/ruby-install_local.sh +++ b/macos/ruby/ruby-install_local.sh @@ -1,12 +1,4 @@ -# Install ruby-install (https://github.com/postmodern/ruby-install#readme) -mkdir -p $HOME/src -cd $HOME/src -wget -O ruby-install-0.6.0.tar.gz https://github.com/postmodern/ruby-install/archive/v0.6.0.tar.gz -tar -xzvf ruby-install-0.6.0.tar.gz -cd ruby-install-0.6.0/ -PREFIX=$HOME/.ruby-install make install - -# Install latest stable Ruby -$HOME/.ruby-install/bin/ruby-install --latest --no-install-deps ruby - -# Now restart your terminal so chruby can detect your shiny new Ruby +#!/usr/bin/env bash +echo "ruby-install_local.sh has been retired." >&2 +echo "Use a currently supported Ruby version manager and verify its signed release." >&2 +exit 1 diff --git a/python/clone-gitlab-group.py b/python/clone-gitlab-group.py index 87cfcb3..3e5d398 100644 --- a/python/clone-gitlab-group.py +++ b/python/clone-gitlab-group.py @@ -3,6 +3,15 @@ import gitlab import os import subprocess +from pathlib import Path + + +def contained_path(parent_dir, child_name): + parent = Path(parent_dir).resolve() + destination = (parent / child_name).resolve() + if destination == parent or parent not in destination.parents: + raise ValueError(f'Unsafe destination path: {child_name}') + return str(destination) def clone_repo(repo_url, destination_path): @@ -10,7 +19,10 @@ def clone_repo(repo_url, destination_path): if not os.path.exists(destination_path): try: print(f'Cloning {repo_url} into {destination_path}') - subprocess.run(['git', 'clone', repo_url, destination_path]) + subprocess.run( + ['git', 'clone', '--', repo_url, destination_path], + check=True, + ) except subprocess.CalledProcessError as e: print(f'An error occurred: {e}') else: @@ -20,13 +32,13 @@ def clone_repo(repo_url, destination_path): def clone_project(project, parent_dir): print(f'Cloning project {project.path} in {parent_dir}') project_web_url = project.web_url - project_path = os.path.join(parent_dir, project.path) + project_path = contained_path(parent_dir, project.path) clone_repo(project_web_url, project_path) def clone_wiki(project, parent_dir): print(f'Cloning wiki for project {project.path} in {parent_dir}') - wiki_path = os.path.join(parent_dir, f"{project.path}.wiki") + wiki_path = contained_path(parent_dir, f"{project.path}.wiki") wiki_web_git_url = project.web_url + '.wiki.git' # if project.wikis.list() is empty, the wiki does not exist @@ -46,7 +58,7 @@ def process_group(group_id, parent_dir, gitlab_client): group = gitlab_client.groups.get(group_id) # Make sure the directory structure matches the group structure - group_dir = os.path.join(parent_dir, group.path) + group_dir = contained_path(parent_dir, group.path) if not os.path.exists(group_dir): print(f'Creating directory {group_dir}') os.makedirs(group_dir) @@ -80,8 +92,14 @@ def main(): gitlab_token = os.environ.get('GITLAB_TOKEN') gitlab_host = os.environ.get('GITLAB_HOST', 'https://gitlab.com') + if not gitlab_token: + print('GITLAB_TOKEN is required', file=sys.stderr) + sys.exit(1) if not gitlab_host.startswith('http'): gitlab_host = f'https://{gitlab_host}' + if not gitlab_host.startswith('https://'): + print('GITLAB_HOST must use HTTPS', file=sys.stderr) + sys.exit(1) gitlab_client = gitlab.Gitlab(gitlab_host, private_token=gitlab_token) @@ -89,6 +107,7 @@ def main(): process_group(gitlab_group_id, os.getcwd(), gitlab_client) except Exception as e: print(f'An error occurred: {e}') + sys.exit(1) if __name__ == '__main__': diff --git a/rm-recursive-.gradle.sh b/rm-recursive-.gradle.sh index f02a928..6b5f87b 100755 --- a/rm-recursive-.gradle.sh +++ b/rm-recursive-.gradle.sh @@ -1,22 +1,47 @@ -#!/bin/bash +#!/usr/bin/env bash +set -Eeuo pipefail -# Define a function to delete node_modules directories -function delete_node_modules { - shopt -s dotglob - for file in "$1"/*; do - if [ -d "$file" ]; then - if [ "$(basename "$file")" = ".gradle" ]; then - echo "Deleting $file" - rm -rf "$file" - else - delete_node_modules "$file" - fi - fi - done - shopt -u dotglob +TARGET_NAME=".gradle" +ASSUME_YES=false +ROOT="." + +usage() { + echo "Usage: $0 [--yes] [root]" } +while [[ $# -gt 0 ]]; do + case "$1" in + --yes) ASSUME_YES=true ;; + -h|--help) usage; exit 0 ;; + -*) usage >&2; exit 2 ;; + *) ROOT="$1" ;; + esac + shift +done + +ROOT="$(realpath -e -- "$ROOT")" +if [[ "$ROOT" == "/" ]]; then + echo "Refusing to search from the filesystem root." >&2 + exit 1 +fi + +mapfile -d '' MATCHES < <(find -P "$ROOT" -type d -name "$TARGET_NAME" -prune -print0) +if [[ ${#MATCHES[@]} -eq 0 ]]; then + echo "No $TARGET_NAME directories found under $ROOT." + exit 0 +fi -# Call the delete_node_modules function with the current directory as the argument -delete_node_modules "." +printf 'Directories to delete:\n' +printf ' %s\n' "${MATCHES[@]}" +if ! $ASSUME_YES; then + read -r -p "Delete these directories? [y/N]: " reply + [[ "$reply" =~ ^[Yy]([Ee][Ss])?$ ]] || exit 0 +fi +for path in "${MATCHES[@]}"; do + [[ "$path" == "$ROOT/"* && ! -L "$path" ]] || { + echo "Refusing unsafe path: $path" >&2 + exit 1 + } + rm -rf -- "$path" +done diff --git a/rm-recursive-build.sh b/rm-recursive-build.sh index 5b16dfb..2e1d259 100755 --- a/rm-recursive-build.sh +++ b/rm-recursive-build.sh @@ -1,20 +1,47 @@ -#!/bin/bash +#!/usr/bin/env bash +set -Eeuo pipefail -# Define a function to delete node_modules directories -function delete_node_modules { - for file in "$1"/*; do - if [ -d "$file" ]; then - if [ "$(basename "$file")" = "build" ]; then - echo "Deleting $file" - rm -rf "$file" - else - delete_node_modules "$file" - fi - fi - done +TARGET_NAME="build" +ASSUME_YES=false +ROOT="." + +usage() { + echo "Usage: $0 [--yes] [root]" } +while [[ $# -gt 0 ]]; do + case "$1" in + --yes) ASSUME_YES=true ;; + -h|--help) usage; exit 0 ;; + -*) usage >&2; exit 2 ;; + *) ROOT="$1" ;; + esac + shift +done + +ROOT="$(realpath -e -- "$ROOT")" +if [[ "$ROOT" == "/" ]]; then + echo "Refusing to search from the filesystem root." >&2 + exit 1 +fi + +mapfile -d '' MATCHES < <(find -P "$ROOT" -type d -name "$TARGET_NAME" -prune -print0) +if [[ ${#MATCHES[@]} -eq 0 ]]; then + echo "No $TARGET_NAME directories found under $ROOT." + exit 0 +fi -# Call the delete_node_modules function with the current directory as the argument -delete_node_modules "." +printf 'Directories to delete:\n' +printf ' %s\n' "${MATCHES[@]}" +if ! $ASSUME_YES; then + read -r -p "Delete these directories? [y/N]: " reply + [[ "$reply" =~ ^[Yy]([Ee][Ss])?$ ]] || exit 0 +fi +for path in "${MATCHES[@]}"; do + [[ "$path" == "$ROOT/"* && ! -L "$path" ]] || { + echo "Refusing unsafe path: $path" >&2 + exit 1 + } + rm -rf -- "$path" +done diff --git a/rm-recursive-node_modules.sh b/rm-recursive-node_modules.sh index 06f19c5..1e80c6f 100755 --- a/rm-recursive-node_modules.sh +++ b/rm-recursive-node_modules.sh @@ -1,20 +1,47 @@ -#!/bin/bash +#!/usr/bin/env bash +set -Eeuo pipefail -# Define a function to delete node_modules directories -function delete_node_modules { - for file in "$1"/*; do - if [ -d "$file" ]; then - if [ "$(basename "$file")" = "node_modules" ]; then - echo "Deleting $file" - rm -rf "$file" - else - delete_node_modules "$file" - fi - fi - done +TARGET_NAME="node_modules" +ASSUME_YES=false +ROOT="." + +usage() { + echo "Usage: $0 [--yes] [root]" } +while [[ $# -gt 0 ]]; do + case "$1" in + --yes) ASSUME_YES=true ;; + -h|--help) usage; exit 0 ;; + -*) usage >&2; exit 2 ;; + *) ROOT="$1" ;; + esac + shift +done + +ROOT="$(realpath -e -- "$ROOT")" +if [[ "$ROOT" == "/" ]]; then + echo "Refusing to search from the filesystem root." >&2 + exit 1 +fi + +mapfile -d '' MATCHES < <(find -P "$ROOT" -type d -name "$TARGET_NAME" -prune -print0) +if [[ ${#MATCHES[@]} -eq 0 ]]; then + echo "No $TARGET_NAME directories found under $ROOT." + exit 0 +fi -# Call the delete_node_modules function with the current directory as the argument -delete_node_modules "." +printf 'Directories to delete:\n' +printf ' %s\n' "${MATCHES[@]}" +if ! $ASSUME_YES; then + read -r -p "Delete these directories? [y/N]: " reply + [[ "$reply" =~ ^[Yy]([Ee][Ss])?$ ]] || exit 0 +fi +for path in "${MATCHES[@]}"; do + [[ "$path" == "$ROOT/"* && ! -L "$path" ]] || { + echo "Refusing unsafe path: $path" >&2 + exit 1 + } + rm -rf -- "$path" +done diff --git a/rm-recursive-postgres-data.sh b/rm-recursive-postgres-data.sh index 11d2979..99ac5ff 100755 --- a/rm-recursive-postgres-data.sh +++ b/rm-recursive-postgres-data.sh @@ -1,20 +1,47 @@ -#!/bin/bash +#!/usr/bin/env bash +set -Eeuo pipefail -# Define a function to delete node_modules directories -function delete_node_modules { - for file in "$1"/*; do - if [ -d "$file" ]; then - if [ "$(basename "$file")" = "postgres-data" ]; then - echo "Deleting $file" - rm -rf "$file" - else - delete_node_modules "$file" - fi - fi - done +TARGET_NAME="postgres-data" +ASSUME_YES=false +ROOT="." + +usage() { + echo "Usage: $0 [--yes] [root]" } +while [[ $# -gt 0 ]]; do + case "$1" in + --yes) ASSUME_YES=true ;; + -h|--help) usage; exit 0 ;; + -*) usage >&2; exit 2 ;; + *) ROOT="$1" ;; + esac + shift +done + +ROOT="$(realpath -e -- "$ROOT")" +if [[ "$ROOT" == "/" ]]; then + echo "Refusing to search from the filesystem root." >&2 + exit 1 +fi + +mapfile -d '' MATCHES < <(find -P "$ROOT" -type d -name "$TARGET_NAME" -prune -print0) +if [[ ${#MATCHES[@]} -eq 0 ]]; then + echo "No $TARGET_NAME directories found under $ROOT." + exit 0 +fi -# Call the delete_node_modules function with the current directory as the argument -delete_node_modules "." +printf 'Directories to delete:\n' +printf ' %s\n' "${MATCHES[@]}" +if ! $ASSUME_YES; then + read -r -p "Delete these directories? [y/N]: " reply + [[ "$reply" =~ ^[Yy]([Ee][Ss])?$ ]] || exit 0 +fi +for path in "${MATCHES[@]}"; do + [[ "$path" == "$ROOT/"* && ! -L "$path" ]] || { + echo "Refusing unsafe path: $path" >&2 + exit 1 + } + rm -rf -- "$path" +done diff --git a/setup-kali.sh b/setup-kali.sh index 3348a58..a12dca4 100644 --- a/setup-kali.sh +++ b/setup-kali.sh @@ -1,182 +1,5 @@ -#!/bin/bash - -# Setup Kali from installation -# Tested 2020-05-25 with Kali 2020.2 installer, x64, in VirtualBox. - -# $ git clone https://github.com/benhunter/scripts; chmod +x ./scripts/setup-kali.sh; ./scripts/setup-kali.sh - -# Update repo in place -# https://stackoverflow.com/questions/1125968/how-do-i-force-git-pull-to-overwrite-local-files -# git reset --hard HEAD; git pull - -# Pause for debugging if needed: -# read -p "Press Enter key to continue." # TODO remove - - -# Prompt for sudo if not root. -if [[ $EUID != 0 ]]; then - echo $? - sudo "$0" "$@" - exit $? -fi - -echo "Running as root." - -CWD=$(pwd) # store working directory to cleanly return to it later -echo '$SUDO_USER' $SUDO_USER -HOME_DIR=$(eval echo ~`logname`) # Home directory of the user running the script. -echo '$HOME_DIR' $HOME_DIR - -# Get path to script that is running. -# https://stackoverflow.com/questions/59895/how-to-get-the-source-directory-of-a-bash-script-from-within-the-script-itself -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -echo '$SCRIPT_DIR' $SCRIPT_DIR -# echo '~' ~ - -# update-apt.sh must be in the same directory -if [[ -e $SCRIPT_DIR/update-apt.sh ]]; then - echo "Running update-apt.sh" - chmod +x $SCRIPT_DIR/update-apt.sh - $SCRIPT_DIR/update-apt.sh -else - echo "Could not find update-apt.sh. Exiting." - exit -fi - -# Install more apt packages -echo "Installing apt packages..." -# VirtualBox guest additions are auto-installed? -apt -y install kali-linux-everything # https://tools.kali.org/kali-metapackages - -# More packages. -# Htop, tree, gobuster -# Python pip3, pip for virtual environments -# ssss - Shamir's secret sharing scheme -# ExifTool https://github.com/exiftool/exiftool -# Hex editor for GNOME https://wiki.gnome.org/Apps/Ghex -apt -y install htop tree gobuster python3-venv python-pip ssss libimage-exiftool-perl ghex jq powerline fonts-powerline joplin - -# Install special software - -# Snap (for VSCode) -echo "Installing and enabling snap..." -apt -y install snapd # Install snapcraft.io store -# Additionally, enable and start both the snapd and the snapd.apparmor services with the following command: -systemctl enable --now snapd apparmor -# To test your system, install the hello-world snap and make sure it runs correctly: -# $ snap install hello-world -# $ hello-world 6.3 from Canonicalβœ“ installed -# $ hello-world -# Hello World! -# Install Snap Store App -# $ sudo snap install snap-store - -# Add snap to path and update .bash_profile -# https://github.com/thoughtbot/til/blob/master/bash/bash_profile_vs_bashrc.md -# if [[ -e ~/.bash_profile ]]; then -echo "Updating ~/.bash_profile..." -echo 'export PATH=$PATH:/snap/bin' >> $HOME_DIR/.bash_profile -chown $SUDO_USER:$SUDO_USER $HOME_DIR/.bash_profile -# fi - -# Visual Studio Code / VSCode -# TODO check out VSCodium https://vscodium.com/ -# https://snapcraft.io/docs/installing-snap-on-kali -echo "Installing VSCode..." -snap install --classic code -# To execute: -# snap run code -# code # if '/snap/bin' is in $PATH - -# TODO How to add a shortcut to the start menu? - -# Install Zsteg -# https://0xrick.github.io/lists/stego/ -# sudo gem install zsteg - -# Ghidra -cd $HOME_DIR/Downloads -# curl -s https://api.github.com/repos/NationalSecurityAgency/ghidra/tags | grep -m1 zip | cut -d '"' -f 4 | wget -qi - -# GHIDRA_GITHUB=`curl -s https://api.github.com/repos/NationalSecurityAgency/ghidra/tags` -# GHIDRA_ZIP=`echo $GHIDRA_GITHUB | jq '.[0].name'` -# GHIDRA_ZIP_URL=`echo $GHIDRA_GITHUB | jq '.[0].zipball_url'` -# wget $GHIDRA_ZIP_URL - -GHIDRA_VERSION=9.1.2 -GHIDRA_ZIP=ghidra_9.1.2_PUBLIC_20200212.zip -wget "https://ghidra-sre.org/$GHIDRA_ZIP" -chown $SUDO_USER:$SUDO_USER $GHIDRA_ZIP -unzip $GHIDRA_ZIP -chown -R $SUDO_USER:$SUDO_USER ghidra_"$GHIDRA_VERSION"_PUBLIC -mv ghidra_"$GHIDRA_VERSION"_PUBLIC /opt/ -cd $HOME_DIR - -# Download git repos - -# RSA CTF Tool -mkdir $HOME_DIR/GitHub -cd $HOME_DIR/GitHub -git clone https://github.com/Ganapati/RsaCtfTool -if [[ -d ./RsaCtfTool ]]; then - cd ./RsaCtfTool - python3 -m venv --system-site-packages venv - source ./venv/bin/activate - sudo apt -y install libmpc-dev # and libmpfr-dev ? - pip install -r requirements.txt - # SageMath package was removed from kali apt... - deactivate # exit virtual environment - - chown -R $SUDO_USER:$SUDO_USER $HOME_DIR/GitHub - - cd $HOME_DIR -else - echo "FAILED: git clone https://github.com/Ganapati/RsaCtfTool" - exit -fi - -# Install Python Packages -pip3 install pwntools -# Output: -# WARNING: The scripts asm, checksec, common, constgrep, cyclic, debug, disablenx, disasm, elfdiff, elfpa -# tch, errno, hex, main, phd, pwn, pwnstrip, scramble, shellcraft, template, unhex and update are installed -# in '/home/kali/.local/bin' which is not on PATH. -# Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script -# -location. - -# config anything else -echo 'alias ll="ls -lahF"' >> $HOME_DIR/.bash_aliases -echo 'alias tt="tree -lahfs"' >> $HOME_DIR/.bash_aliases -chown $SUDO_USER:$SUDO_USER $HOME_DIR/.bash_aliases - -# Unpack RockYou.txt wordlist -gunzip /usr/share/wordlists/rockyou.txt.gz -# TODO check owner of rockyou - -# Firefox Addons - -# sshd - -# Powerline for Bash -# apt -y install powerline fonts-powerline # already executed -echo >> $HOME_DIR/.bashrc -echo '# Powerline' >> $HOME_DIR/.bashrc -echo '# config goes in ~/.confg/powerline/config.json' >> $HOME_DIR/.bashrc -echo 'if [ -f `which powerline-daemon` ]; then' >> $HOME_DIR/.bashrc -echo ' powerline-daemon -q' >> $HOME_DIR/.bashrc -echo ' POWERLINE_BASH_CONTINUATION=1' >> $HOME_DIR/.bashrc -echo ' POWERLINE_BASH_SELECT=1' >> $HOME_DIR/.bashrc -echo ' . /usr/share/powerline/bindings/bash/powerline.sh' >> $HOME_DIR/.bashrc -echo 'fi' >> $HOME_DIR/.bashrc -echo >> $HOME_DIR/.bashrc - -# Powerline for tmux -echo 'source "/usr/share/powerline/bindings/tmux/powerline.conf"' >> $HOME_DIR/.tmux.conf -echo >> $HOME_DIR/.tmux.conf -chown $SUDO_USER:$SUDO_USER $HOME_DIR/.tmux.conf -echo >> $HOME_DIR/.bash_profile -echo '. ~/.bashrc' >> $HOME_DIR/.bash_profile -echo >> $HOME_DIR/.bash_profile - -# Cleanup -cd $CWD # Go back to the directory where the script started. -echo "Please reboot (snapshot if needed)..." +#!/usr/bin/env bash +echo "setup-kali.sh has been retired." >&2 +echo "It installed obsolete, unverified third-party software as root." >&2 +echo "Use Kali's supported package repositories and verify each external tool independently." >&2 +exit 1 diff --git a/ssh-wait.sh b/ssh-wait.sh index 7a8a870..4200d7d 100755 --- a/ssh-wait.sh +++ b/ssh-wait.sh @@ -1,22 +1,25 @@ -#!/bin/zsh +#!/usr/bin/env zsh +set -eu -# Script to wait until an SSH server is reachable -# Usage: ssh-wait.sh user@hostname [timeout_seconds] - -if [ $# -eq 0 ]; then - echo "Usage: $0 user@hostname [timeout_seconds]" - echo "Example: $0 user@example.com 5" - exit 1 +if (( $# == 0 )); then + print -u2 "Usage: $0 user@hostname [timeout_seconds]" + exit 1 fi TARGET=$1 TIMEOUT=${2:-5} +if [[ "$TARGET" == -* || "$TARGET" == *[[:space:]]* ]]; then + print -u2 "Invalid SSH target." + exit 1 +fi +if [[ "$TIMEOUT" != <-> || "$TIMEOUT" -lt 1 ]]; then + print -u2 "Timeout must be a positive integer." + exit 1 +fi -echo "Waiting for SSH connection to $TARGET..." - -until ssh -o ConnectTimeout=$TIMEOUT "$TARGET" true 2>/dev/null; do - print "$(date): Not reachable, retrying in $TIMEOUT seconds..." - sleep $TIMEOUT +print "Waiting for SSH connection to $TARGET..." +until ssh -o BatchMode=yes -o NumberOfPasswordPrompts=0 -o ConnectTimeout="$TIMEOUT" -- "$TARGET" true 2>/dev/null; do + print "$(date): Not reachable, retrying in $TIMEOUT seconds..." + sleep "$TIMEOUT" done - print "$(date): SSH is up! Connection to $TARGET successful." diff --git a/storage_diagnose.sh b/storage_diagnose.sh index e1e5efc..81eda6d 100755 --- a/storage_diagnose.sh +++ b/storage_diagnose.sh @@ -1,7 +1,8 @@ #!/bin/bash # storage_diagnose.sh - Collects comprehensive storage diagnostics on Ubuntu -OUTPUT="/tmp/storage_report_$(hostname)_$(date +%Y%m%d_%H%M%S).log" +umask 077 +OUTPUT="$(mktemp "${TMPDIR:-/tmp}/storage_report_$(hostname).XXXXXX.log")" echo "Saving storage diagnostics to: $OUTPUT" exec > >(tee -a "$OUTPUT") 2>&1 diff --git a/tag.sh b/tag.sh index 1dd8885..712be4a 100644 --- a/tag.sh +++ b/tag.sh @@ -45,8 +45,7 @@ echo "Bumping $VERSION_BUMP version to $NEW_TAG" # Create a new tag and push it to the remote repository git tag "$NEW_TAG" -git push --tags -git push +git push origin "$NEW_TAG" exit 0 diff --git a/tampermonkey/edx-download-transcripts.js b/tampermonkey/edx-download-transcripts.js index 467c000..5272139 100644 --- a/tampermonkey/edx-download-transcripts.js +++ b/tampermonkey/edx-download-transcripts.js @@ -1,88 +1,67 @@ // ==UserScript== // @name Download Transcripts // @namespace https://www.github.com/benhunter/scripts/tampermonkey -// @version 2024-05-03 +// @version 2026-06-08 // @description Download video transcripts from edX. // @author Ben // @match https://courses.edx.org/* // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant none -// @require http://code.jquery.com/jquery-latest.js -// @require https://cdn.jsdelivr.net/gh/CoeJoder/waitForKeyElements.js@v1.2/waitForKeyElements.js - // ==/UserScript== -function handleClick() { - var sm = $('.subtitles-menu'); - const title = $("h3.hd").text(); - const filename = `${title}.txt`; - download(filename, sm.text()); -} - -function addFeatures() { - $('body').append(''); - $("#dltranscript").css("position", "fixed").css("top", 0).css("left", 0); - $('#dltranscript').click(handleClick); -} - -function download(filename, text) { - var element = document.createElement('a'); - element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); - element.setAttribute('download', filename); - - element.style.display = 'none'; - document.body.appendChild(element); +(() => { + 'use strict'; - element.click(); + const BUTTON_ID = 'dltranscript'; - document.body.removeChild(element); -} + function safeFilename(value) { + const cleaned = value.replace(/[<>:"/\\|?*\u0000-\u001f]/g, '_').trim(); + return `${cleaned || 'transcript'}.txt`; + } -// Just a backup in case the reference repo goes down. -function waitForKeyElements_(selectorOrFunction, callback, waitOnce, interval, maxIntervals) { - console.log("debug looking for "); // + selectorOrFunction); - if (typeof waitOnce === "undefined") { - waitOnce = true; - } - if (typeof interval === "undefined") { - interval = 300; - } - if (typeof maxIntervals === "undefined") { - maxIntervals = -1; - } - if (typeof waitForKeyElements.namespace === "undefined") { - waitForKeyElements.namespace = Date.now().toString(); - } - var targetNodes = (typeof selectorOrFunction === "function") - ? selectorOrFunction() - : document.querySelectorAll(selectorOrFunction); + function download(filename, text) { + const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const element = document.createElement('a'); + element.href = url; + element.download = filename; + element.hidden = true; + document.body.appendChild(element); + element.click(); + element.remove(); + URL.revokeObjectURL(url); + } - var targetsFound = targetNodes && targetNodes.length > 0; - if (targetsFound) { - console.log("found something"); - targetNodes.forEach(function(targetNode) { - var attrAlreadyFound = `data-userscript-${waitForKeyElements.namespace}-alreadyFound`; - var alreadyFound = targetNode.getAttribute(attrAlreadyFound) || false; - if (!alreadyFound) { - console.log("callback"); - var cancelFound = callback(targetNode); - if (cancelFound) { - targetsFound = false; - } - else { - targetNode.setAttribute(attrAlreadyFound, true); - } - } - }); + function handleClick() { + const subtitles = document.querySelector('.subtitles-menu'); + if (!subtitles) { + return; } + const title = document.querySelector('h3.hd')?.textContent || 'transcript'; + download(safeFilename(title), subtitles.textContent || ''); + } - if (maxIntervals !== 0 && !(targetsFound && waitOnce)) { - maxIntervals -= 1; - setTimeout(function() { - waitForKeyElements(selectorOrFunction, callback, waitOnce, interval, maxIntervals); - }, interval); + function addFeatures() { + if (document.getElementById(BUTTON_ID) || !document.querySelector('.subtitles')) { + return; } -} + const button = document.createElement('button'); + button.id = BUTTON_ID; + button.type = 'button'; + button.textContent = 'Download Transcript'; + Object.assign(button.style, { + position: 'fixed', + top: '0', + left: '0', + zIndex: '2147483647' + }); + button.addEventListener('click', handleClick); + document.body.appendChild(button); + } -'use strict'; -waitForKeyElements (".subtitles", addFeatures); + addFeatures(); + new MutationObserver(addFeatures).observe(document.documentElement, { + childList: true, + subtree: true + }); +})(); diff --git a/tests/test_security_regressions.py b/tests/test_security_regressions.py new file mode 100644 index 0000000..7cae095 --- /dev/null +++ b/tests/test_security_regressions.py @@ -0,0 +1,87 @@ +import importlib.util +import os +from pathlib import Path +import subprocess +import tempfile +import unittest +from unittest import mock + + +ROOT = Path(__file__).resolve().parents[1] + + +def load_zfsdash(): + spec = importlib.util.spec_from_file_location("zfsdash", ROOT / "zfsdash.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class StaticSecurityTests(unittest.TestCase): + def test_userscript_has_no_remote_dependencies(self): + text = (ROOT / "tampermonkey/edx-download-transcripts.js").read_text(encoding="utf-8-sig") + self.assertNotIn("@require", text) + self.assertNotIn("http://", text) + + def test_file_manager_does_not_interpolate_records_into_html(self): + text = (ROOT / "file-manager.html").read_text(encoding="utf-8-sig") + self.assertNotIn("li.innerHTML", text) + self.assertNotIn("${fileRecord.name}", text) + + def test_zfs_dashboard_is_local_only(self): + module = load_zfsdash() + self.assertEqual(module.HOST, "127.0.0.1") + + def test_retired_installers_fail_closed(self): + for relative_path in ( + "setup-kali.sh", + "example-deploy.sh", + "macos/ruby/ruby-install_local.sh", + "macos/ruby/chruby_local.sh", + "gitlab-clone-group.sh", + ): + text = (ROOT / relative_path).read_text(encoding="utf-8-sig") + self.assertIn("retired", text) + self.assertIn("exit 1", text) + + +class ZfsCacheTests(unittest.TestCase): + def test_status_is_cached(self): + module = load_zfsdash() + module._status_cache.update(at=0.0, output="") + completed = subprocess.CompletedProcess([], 0, "pool output\n", "") + with mock.patch.object(module.subprocess, "run", return_value=completed) as run: + self.assertEqual(module.get_zpool_status(), "pool output") + self.assertEqual(module.get_zpool_status(), "pool output") + run.assert_called_once() + + +@unittest.skipUnless(os.name == "posix", "requires POSIX shell and symlinks") +class RecursiveDeletionTests(unittest.TestCase): + def test_symlink_does_not_escape_selected_root(self): + with tempfile.TemporaryDirectory() as workspace: + workspace = Path(workspace) + root = workspace / "root" + outside = workspace / "outside" + inside_target = root / "project" / "node_modules" + outside_target = outside / "node_modules" + inside_target.mkdir(parents=True) + outside_target.mkdir(parents=True) + (root / "external-link").symlink_to(outside, target_is_directory=True) + + subprocess.run( + ["bash", str(ROOT / "rm-recursive-node_modules.sh"), "--yes", str(root)], + check=True, + ) + + self.assertFalse(inside_target.exists()) + self.assertTrue(outside_target.exists()) + + def test_filesystem_root_is_rejected(self): + result = subprocess.run( + ["bash", str(ROOT / "rm-recursive-node_modules.sh"), "--yes", "/"], + capture_output=True, + text=True, + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn("Refusing", result.stderr) diff --git a/update-apt.sh b/update-apt.sh index b923499..12b7453 100755 --- a/update-apt.sh +++ b/update-apt.sh @@ -1,16 +1,22 @@ -#!/bin/bash -# Update systems using apt package manager. Ubuntu, Kali. +#!/usr/bin/env bash +set -Eeuo pipefail -# Prompt for sudo if not root. -if [ $EUID != 0 ]; then - echo $? - sudo "$0" "$@" - exit $? +ASSUME_YES=false +[[ "${1:-}" == "--yes" ]] && ASSUME_YES=true + +if [[ $EUID -ne 0 ]]; then + exec sudo -- "$0" "$@" +fi + +if ! $ASSUME_YES; then + echo "This will update package indexes, upgrade packages, run dist-upgrade," + echo "clean the package cache, and remove unused packages." + read -r -p "Continue? [y/N]: " reply + [[ "$reply" =~ ^[Yy]([Ee][Ss])?$ ]] || exit 0 fi -echo "Updating..." apt-get update apt-get -y upgrade -apt-get dist-upgrade +apt-get -y dist-upgrade apt-get clean apt-get -y autoremove diff --git a/zfsdash.py b/zfsdash.py index 59f0565..f5eb961 100755 --- a/zfsdash.py +++ b/zfsdash.py @@ -1,20 +1,35 @@ #!/usr/bin/env python3 import re import subprocess -from http.server import BaseHTTPRequestHandler, HTTPServer +import threading +import time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -HOST = "0.0.0.0" +HOST = "127.0.0.1" PORT = 8787 +CACHE_SECONDS = 2.0 +_status_lock = threading.Lock() +_status_cache = {"at": 0.0, "output": ""} def get_zpool_status() -> str: - p = subprocess.run( - ["/sbin/zpool", "status", "-v"], - capture_output=True, - text=True, - timeout=8, - ) - out = p.stdout if p.returncode == 0 else (p.stdout + "\n" + p.stderr) - return out.rstrip("\n") + now = time.monotonic() + with _status_lock: + if now - _status_cache["at"] < CACHE_SECONDS: + return _status_cache["output"] + try: + p = subprocess.run( + ["/sbin/zpool", "status", "-v"], + capture_output=True, + text=True, + timeout=8, + check=False, + ) + out = p.stdout if p.returncode == 0 else (p.stdout + "\n" + p.stderr) + except (OSError, subprocess.TimeoutExpired) as exc: + out = f"Unable to read zpool status: {exc}" + output = out.rstrip("\n") + _status_cache.update(at=time.monotonic(), output=output) + return output def _extract_pool_blocks(zpool_output: str) -> list[dict]: """ @@ -291,6 +306,13 @@ def parse_scrub_info(zpool_output: str) -> dict: el.textContent = poolName ? `pool: ${poolName}` : ""; } +function setHeadline(el, state, detail) { + const pill = document.createElement("span"); + pill.className = "pill"; + pill.textContent = state; + el.replaceChildren(pill, document.createTextNode(` ${detail}`)); +} + function formatResilverWidget(r) { if (!r) { setPool(resilverPool, null); @@ -302,7 +324,7 @@ def parse_scrub_info(zpool_output: str) -> dict: if (r.active) { const eta = r.eta ? r.eta : "In progress"; - resilverHeadline.innerHTML = `ACTIVE   ${eta}`; + setHeadline(resilverHeadline, "ACTIVE", eta); const pct = (typeof r.pct_done === "number") ? `${r.pct_done.toFixed(1)}% done` : null; const parts = []; @@ -310,7 +332,7 @@ def parse_scrub_info(zpool_output: str) -> dict: if (r.state) parts.push(r.state); resilverDetail.textContent = parts.join(" β€” "); } else { - resilverHeadline.innerHTML = `IDLE   No resilver in progress`; + setHeadline(resilverHeadline, "IDLE", "No resilver in progress"); resilverDetail.textContent = r.state ? r.state : ""; } } @@ -326,7 +348,7 @@ def parse_scrub_info(zpool_output: str) -> dict: if (s.active) { const eta = s.eta ? s.eta : "No ETA"; - scrubHeadline.innerHTML = `ACTIVE   ${eta}`; + setHeadline(scrubHeadline, "ACTIVE", eta); const pct = (typeof s.pct_done === "number") ? `${s.pct_done.toFixed(2)}% done` : null; const parts = []; @@ -334,7 +356,7 @@ def parse_scrub_info(zpool_output: str) -> dict: if (s.state) parts.push(s.state); scrubDetail.textContent = parts.join(" β€” "); } else { - scrubHeadline.innerHTML = `IDLE   Last scrub result`; + setHeadline(scrubHeadline, "IDLE", "Last scrub result"); const parts = []; if (s.result) parts.push(s.result); if (s.duration) parts.push(`duration ${s.duration}`); @@ -388,6 +410,10 @@ def _send(self, code: int, body: bytes, content_type: str): self.send_header("Content-Type", content_type) self.send_header("Content-Length", str(len(body))) self.send_header("Cache-Control", "no-store") + self.send_header("Content-Security-Policy", "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'") + self.send_header("X-Content-Type-Options", "nosniff") + self.send_header("X-Frame-Options", "DENY") + self.send_header("Referrer-Policy", "no-referrer") self.end_headers() self.wfile.write(body) @@ -397,7 +423,7 @@ def do_GET(self): return if self.path.startswith("/api/status"): - import time, json + import json output = get_zpool_status() payload = json.dumps({ "ts": int(time.time()), @@ -411,7 +437,7 @@ def do_GET(self): self._send(404, b"Not Found", "text/plain; charset=utf-8") def main(): - httpd = HTTPServer((HOST, PORT), Handler) + httpd = ThreadingHTTPServer((HOST, PORT), Handler) print(f"Serving on http://{HOST}:{PORT}") httpd.serve_forever()