Compare commits

..

43 Commits

Author SHA1 Message Date
Carlos Polop
1473fedcbf Fix linPEAS module section path matching 2026-01-21 15:21:50 +01:00
Carlos Polop
f8f4250b81 Add stronger winPEAS/linPEAS tests 2026-01-21 15:14:08 +01:00
Carlos Polop
1fb419fa0c Reduce CI warnings (actions versions, outputs, ruleset) 2026-01-21 13:52:20 +01:00
Carlos Polop
651dc9cd7d Force-copy TestPlatform framework for MSTest 2026-01-21 12:53:49 +01:00
Carlos Polop
0808fb7f1b Ensure MSTest adapter finds TestPlatform framework 2026-01-21 12:24:56 +01:00
Carlos Polop
c332fab519 Fix MSTest adapter dependency load 2026-01-21 11:55:35 +01:00
Carlos Polop
577dcc9964 Fix MSTest framework copy-local for CI 2026-01-21 11:27:00 +01:00
Carlos Polop
b591f3d524 Fix winPEAS argument parsing tests 2026-01-21 10:57:32 +01:00
Carlos Polop
b3ac8c6d22 Stabilize winPEAS MSBuild in CI 2026-01-21 01:47:37 +01:00
Carlos Polop
83580fcd8a Re-enable winPEAS tests and add linPEAS builder checks 2026-01-21 01:15:38 +01:00
Carlos Polop
ab3a5899de Gate codex fixer job on PR author 2026-01-21 01:04:06 +01:00
Carlos Polop
0fac664048 Fix winPEAS build break in Program.Main 2026-01-21 00:32:09 +01:00
SirBroccoli
db30e3bd7d Fix Browser_profiles module ID casing (#576) 2026-01-20 23:54:30 +01:00
SirBroccoli
7ad87a85e6 Use PAT for fixer pushes and limit to one attempt (#575)
* Test CI failure flow

* Use PAT for fixer pushes and run only once per PR
2026-01-20 23:54:19 +01:00
Carlos Polop
b24694f00b Gate codex triage on successful PR tests 2026-01-20 23:23:42 +01:00
Carlos Polop
e777c81eba Run Codex triage after PR tests succeed 2026-01-20 23:23:42 +01:00
SirBroccoli
21a86bc365 Auto-merge PR #573 (Codex) 2026-01-20 22:17:38 +00:00
SirBroccoli
ac7cb9c73c Auto-merge PR #572 (Codex) 2026-01-20 22:17:02 +00:00
SirBroccoli
d054715fbd Merge pull request #564 from peass-ng/fix/issue-410-printnightmare
Check PrintNightmare PointAndPrint policy
2026-01-20 23:06:58 +01:00
SirBroccoli
b4c1043a93 Merge branch 'master' into fix/issue-410-printnightmare 2026-01-20 23:06:29 +01:00
Carlos Polop
1b8706aac6 Handle missing PointAndPrint values in PS1 2026-01-20 23:04:35 +01:00
SirBroccoli
3371be7bd6 Merge pull request #557 from peass-ng/fix/issue-474-service-timeout
Add timeout to service enumeration
2026-01-20 23:02:35 +01:00
SirBroccoli
2344f5b106 Auto-merge PR #570 (Codex) 2026-01-20 17:25:25 +00:00
SirBroccoli
485f91d46c Auto-merge PR #569 (Codex) 2026-01-20 17:25:00 +00:00
SirBroccoli
018e8866e6 Auto-merge PR #568 (Codex) 2026-01-20 17:24:04 +00:00
SirBroccoli
d707317278 Auto-merge PR #567 (Codex) 2026-01-20 17:23:33 +00:00
SirBroccoli
f4ef371afc Auto-merge PR #566 (Codex) 2026-01-20 17:22:47 +00:00
SirBroccoli
61f6282b5f Auto-merge PR #565 (Codex) 2026-01-20 17:22:35 +00:00
codex-action
a363541d77 Fix CI failures for PR #564 2026-01-20 17:09:07 +00:00
Carlos Polop
6fc41c9a23 Add PrintNightmare PointAndPrint policy check 2026-01-20 18:03:55 +01:00
SirBroccoli
170a4b2c70 Auto-merge PR #563 (Codex) 2026-01-20 17:03:45 +00:00
codex-action
710709834a Fix CI failures for PR #557 2026-01-20 17:03:40 +00:00
SirBroccoli
21b2bac892 Auto-merge PR #562 (Codex) 2026-01-20 17:03:14 +00:00
SirBroccoli
5fdb99b38e Auto-merge PR #561 (Codex) 2026-01-20 17:02:17 +00:00
SirBroccoli
787bc8fa8a Auto-merge PR #560 (Codex) 2026-01-20 17:01:24 +00:00
SirBroccoli
c5401bd33d Auto-merge PR #559 (Codex) 2026-01-20 17:00:50 +00:00
SirBroccoli
bd18d96837 Auto-merge PR #558 (Codex) 2026-01-20 17:00:10 +00:00
SirBroccoli
ede5960b7c Auto-merge PR #556 (Codex) 2026-01-20 16:59:18 +00:00
Carlos Polop
c54f483648 Add timeout to service enumeration in extra checks 2026-01-20 17:58:36 +01:00
SirBroccoli
e533bf3ba5 Auto-merge PR #555 (Codex) 2026-01-20 16:58:29 +00:00
SirBroccoli
66c3d4e342 Merge pull request #554 from peass-ng/fix-pr-failure-dispatch-context-2
Fix pr failure dispatch context 2
2026-01-20 17:46:27 +01:00
Carlos Polop
21a967acb5 fix urls 2026-01-20 13:59:32 +01:00
SirBroccoli
89a55bde9b Auto-merge PR #553 (Codex) 2026-01-18 23:11:38 +00:00
39 changed files with 609 additions and 102 deletions

View File

@@ -26,7 +26,7 @@ jobs:
steps:
# checkout
- name: Checkout
uses: actions/checkout@master
uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
@@ -36,11 +36,11 @@ jobs:
# Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v1.0.2
uses: microsoft/setup-msbuild@v2
# Setup NuGet
- name: Setup NuGet.exe
uses: nuget/setup-nuget@v1
uses: nuget/setup-nuget@v2
# Restore the packages for testing
- name: Restore the application
@@ -48,23 +48,23 @@ jobs:
# build
- name: run MSBuild
run: msbuild $env:Solution_Path
run: msbuild $env:Solution_Path /p:Configuration=$env:Configuration /p:UseSharedCompilation=false
# Execute all unit tests in the solution
#- name: Execute unit tests
# run: dotnet test $env:Solution_Path
- name: Execute unit tests
run: dotnet test $env:Solution_Path --configuration $env:Configuration
# Build & update all versions
- name: Build all versions
run: |
echo "build x64"
msbuild -m $env:Solution_Path /t:Rebuild /p:Configuration=$env:Configuration /p:Platform="x64"
msbuild -m $env:Solution_Path /t:Rebuild /p:Configuration=$env:Configuration /p:Platform="x64" /p:UseSharedCompilation=false
echo "build x86"
msbuild -m $env:Solution_Path /t:Rebuild /p:Configuration=$env:Configuration /p:Platform="x86"
msbuild -m $env:Solution_Path /t:Rebuild /p:Configuration=$env:Configuration /p:Platform="x86" /p:UseSharedCompilation=false
echo "build Any CPU"
msbuild -m $env:Solution_Path /t:Rebuild /p:Configuration=$env:Configuration /p:Platform="Any CPU"
msbuild -m $env:Solution_Path /t:Rebuild /p:Configuration=$env:Configuration /p:Platform="Any CPU" /p:UseSharedCompilation=false
- name: Execute winPEAS -h
shell: pwsh
@@ -220,6 +220,7 @@ jobs:
- uses: actions/setup-go@v6
with:
go-version: '1.23'
cache: false
- run: go version
# Build linpeas
@@ -230,6 +231,9 @@ jobs:
python3 -m builder.linpeas_builder --all --output linpeas_fat.sh
python3 -m builder.linpeas_builder --all-no-fat --output linpeas.sh
python3 -m builder.linpeas_builder --small --output linpeas_small.sh
- name: Run linPEAS builder tests
run: python3 -m unittest discover -s linPEAS/tests -p "test_*.py"
# Build linpeas binaries
- name: Build linpeas binaries
@@ -362,7 +366,7 @@ jobs:
steps:
# Download repo
- uses: actions/checkout@v2
- uses: actions/checkout@v5
# Build linpeas
- name: Build macpeas
@@ -469,11 +473,11 @@ jobs:
- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%Y%m%d')"
run: echo "date=$(date +'%Y%m%d')" >> "$GITHUB_OUTPUT"
- name: Generate random
id: random_n
run: echo "::set-output name=some_rand::$(openssl rand -hex 4)"
run: echo "some_rand=$(openssl rand -hex 4)" >> "$GITHUB_OUTPUT"
# Create the release
- name: Create Release

View File

@@ -8,6 +8,8 @@ on:
paths-ignore:
- '.github/**'
workflow_dispatch:
jobs:
Build_and_test_winpeas_pr:
runs-on: windows-latest
@@ -20,7 +22,7 @@ jobs:
steps:
# checkout
- name: Checkout
uses: actions/checkout@master
uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
@@ -30,11 +32,11 @@ jobs:
# Add MSBuild to the PATH
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v1.0.2
uses: microsoft/setup-msbuild@v2
# Setup NuGet
- name: Setup NuGet.exe
uses: nuget/setup-nuget@v1
uses: nuget/setup-nuget@v2
# Restore the packages for testing
- name: Restore the application
@@ -42,19 +44,23 @@ jobs:
# build
- name: run MSBuild
run: msbuild $env:Solution_Path
run: msbuild $env:Solution_Path /p:Configuration=$env:Configuration /p:UseSharedCompilation=false
# Execute unit tests in the solution
- name: Execute unit tests
run: dotnet test $env:Solution_Path --configuration $env:Configuration
# Build all versions
- name: Build all versions
run: |
echo "build x64"
msbuild -m $env:Solution_Path /t:Rebuild /p:Configuration=$env:Configuration /p:Platform="x64"
msbuild -m $env:Solution_Path /t:Rebuild /p:Configuration=$env:Configuration /p:Platform="x64" /p:UseSharedCompilation=false
echo "build x86"
msbuild -m $env:Solution_Path /t:Rebuild /p:Configuration=$env:Configuration /p:Platform="x86"
msbuild -m $env:Solution_Path /t:Rebuild /p:Configuration=$env:Configuration /p:Platform="x86" /p:UseSharedCompilation=false
echo "build Any CPU"
msbuild -m $env:Solution_Path /t:Rebuild /p:Configuration=$env:Configuration /p:Platform="Any CPU"
msbuild -m $env:Solution_Path /t:Rebuild /p:Configuration=$env:Configuration /p:Platform="Any CPU" /p:UseSharedCompilation=false
- name: Execute winPEAS -h
shell: pwsh
@@ -105,7 +111,7 @@ jobs:
steps:
# Download repo
- uses: actions/checkout@v2
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
@@ -113,6 +119,7 @@ jobs:
- uses: actions/setup-go@v6
with:
go-version: '1.23'
cache: false
- run: go version
# Build linpeas
@@ -123,6 +130,9 @@ jobs:
python3 -m builder.linpeas_builder --all --output linpeas_fat.sh
python3 -m builder.linpeas_builder --all-no-fat --output linpeas.sh
python3 -m builder.linpeas_builder --small --output linpeas_small.sh
- name: Run linPEAS builder tests
run: python3 -m unittest discover -s linPEAS/tests -p "test_*.py"
# Run linpeas help as quick test
- name: Run linpeas help
@@ -161,7 +171,7 @@ jobs:
steps:
# Download repo
- uses: actions/checkout@v2
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}

View File

@@ -1,41 +1,94 @@
name: Codex PR Triage
on:
pull_request:
types: [opened]
workflow_run:
workflows: ["PR-tests"]
types: [completed]
jobs:
codex_triage:
if: ${{ github.event.pull_request.user.login == 'carlospolop' }}
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
should_run: ${{ steps.gate.outputs.should_run }}
pr_number: ${{ steps.gate.outputs.pr_number }}
pr_title: ${{ steps.gate.outputs.pr_title }}
pr_body: ${{ steps.gate.outputs.pr_body }}
base_ref: ${{ steps.gate.outputs.base_ref }}
head_ref: ${{ steps.gate.outputs.head_ref }}
base_sha: ${{ steps.gate.outputs.base_sha }}
head_sha: ${{ steps.gate.outputs.head_sha }}
decision: ${{ steps.parse.outputs.decision }}
message: ${{ steps.parse.outputs.message }}
steps:
- name: Resolve PR context
id: gate
env:
GH_TOKEN: ${{ github.token }}
run: |
pr_number="${{ github.event.workflow_run.pull_requests[0].number }}"
if [ -z "$pr_number" ]; then
echo "No pull request found for this workflow_run; skipping."
echo "should_run=false" >> "$GITHUB_OUTPUT"
echo "pr_number=" >> "$GITHUB_OUTPUT"
exit 0
fi
author="$(gh pr view "$pr_number" --json author --jq .author.login)"
if [ "$author" != "carlospolop" ]; then
echo "PR author is $author; skipping."
echo "should_run=false" >> "$GITHUB_OUTPUT"
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
exit 0
fi
pr_title="$(gh pr view "$pr_number" --json title --jq .title)"
pr_body="$(gh pr view "$pr_number" --json body --jq .body)"
base_ref="$(gh pr view "$pr_number" --json baseRefName --jq .baseRefName)"
head_ref="$(gh pr view "$pr_number" --json headRefName --jq .headRefName)"
base_sha="$(gh pr view "$pr_number" --json baseRefOid --jq .baseRefOid)"
head_sha="$(gh pr view "$pr_number" --json headRefOid --jq .headRefOid)"
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
echo "pr_title<<EOF" >> "$GITHUB_OUTPUT"
echo "$pr_title" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
echo "pr_body<<EOF" >> "$GITHUB_OUTPUT"
echo "$pr_body" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT"
echo "head_ref=$head_ref" >> "$GITHUB_OUTPUT"
echo "base_sha=$base_sha" >> "$GITHUB_OUTPUT"
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
- name: Checkout PR merge ref
uses: actions/checkout@v5
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
ref: refs/pull/${{ steps.gate.outputs.pr_number }}/merge
if: ${{ steps.gate.outputs.should_run == 'true' }}
- name: Pre-fetch base and head refs
if: ${{ steps.gate.outputs.should_run == 'true' }}
run: |
git fetch --no-tags origin \
${{ github.event.pull_request.base.ref }} \
+refs/pull/${{ github.event.pull_request.number }}/head
${{ steps.gate.outputs.base_ref }} \
+refs/pull/${{ steps.gate.outputs.pr_number }}/head
- name: Run Codex
id: run_codex
if: ${{ steps.gate.outputs.should_run == 'true' }}
uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
output-schema-file: .github/codex/pr-merge-schema.json
model: gpt-5.2-codex
prompt: |
You are reviewing PR #${{ github.event.pull_request.number }} for ${{ github.repository }}.
You are reviewing PR #${{ steps.gate.outputs.pr_number }} for ${{ github.repository }}.
Decide whether to merge or comment. Merge only if all of the following are true:
- Changes are simple and safe (no DoS, no long operations, no backdoors).
@@ -48,16 +101,17 @@ jobs:
Pull request title and body:
----
${{ github.event.pull_request.title }}
${{ github.event.pull_request.body }}
${{ steps.gate.outputs.pr_title }}
${{ steps.gate.outputs.pr_body }}
Review ONLY the changes introduced by the PR:
git log --oneline ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}
git log --oneline ${{ steps.gate.outputs.base_sha }}...${{ steps.gate.outputs.head_sha }}
Output JSON only, following the provided schema.
- name: Parse Codex decision
id: parse
if: ${{ steps.gate.outputs.should_run == 'true' }}
env:
CODEX_MESSAGE: ${{ steps.run_codex.outputs.final-message }}
run: |
@@ -78,7 +132,7 @@ jobs:
merge_or_comment:
runs-on: ubuntu-latest
needs: codex_triage
if: ${{ needs.codex_triage.outputs.decision != '' }}
if: ${{ github.event.workflow_run.conclusion == 'success' && needs.codex_triage.outputs.should_run == 'true' && needs.codex_triage.outputs.decision != '' }}
permissions:
contents: write
pull-requests: write
@@ -87,7 +141,7 @@ jobs:
if: ${{ needs.codex_triage.outputs.decision == 'merge' }}
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_NUMBER: ${{ needs.codex_triage.outputs.pr_number }}
run: |
gh api \
-X PUT \
@@ -100,7 +154,7 @@ jobs:
if: ${{ needs.codex_triage.outputs.decision == 'comment' }}
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_NUMBER: ${{ needs.codex_triage.outputs.pr_number }}
CODEX_MESSAGE: ${{ needs.codex_triage.outputs.message }}
with:
github-token: ${{ github.token }}

View File

@@ -6,7 +6,7 @@ on:
types: [completed]
jobs:
codex_on_failure:
resolve_pr_context:
if: >
${{ github.event.workflow_run.conclusion == 'failure' &&
github.event.workflow_run.pull_requests &&
@@ -14,11 +14,14 @@ jobs:
!startsWith(github.event.workflow_run.head_commit.message, 'Fix CI failures for PR #') }}
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
actions: read
pull-requests: read
issues: read
outputs:
number: ${{ steps.pr_context.outputs.number }}
author: ${{ steps.pr_context.outputs.author }}
head_repo: ${{ steps.pr_context.outputs.head_repo }}
head_branch: ${{ steps.pr_context.outputs.head_branch }}
should_run: ${{ steps.pr_context.outputs.should_run }}
steps:
- name: Resolve PR context
id: pr_context
@@ -35,18 +38,37 @@ jobs:
pr_head_branch=$(gh api -H "Accept: application/vnd.github+json" \
/repos/${{ github.repository }}/pulls/${PR_NUMBER} \
--jq '.head.ref')
pr_labels=$(gh api -H "Accept: application/vnd.github+json" \
/repos/${{ github.repository }}/issues/${PR_NUMBER} \
--jq '.labels[].name')
if echo "$pr_labels" | grep -q "^codex-fix-attempted$"; then
echo "codex fix already attempted for PR #${PR_NUMBER}; skipping."
should_run=false
else
should_run=true
fi
{
echo "number=${PR_NUMBER}"
echo "author=${pr_author}"
echo "head_repo=${pr_head_repo}"
echo "head_branch=${pr_head_branch}"
echo "should_run=${should_run}"
} >> "$GITHUB_OUTPUT"
codex_on_failure:
needs: resolve_pr_context
if: ${{ needs.resolve_pr_context.outputs.author == 'carlospolop' && needs.resolve_pr_context.outputs.should_run == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Comment on PR with failure info
if: ${{ steps.pr_context.outputs.author == 'carlospolop' }}
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ steps.pr_context.outputs.number }}
PR_NUMBER: ${{ needs.resolve_pr_context.outputs.number }}
RUN_URL: ${{ github.event.workflow_run.html_url }}
WORKFLOW_NAME: ${{ github.event.workflow_run.name }}
with:
@@ -61,23 +83,30 @@ jobs:
body,
});
- name: Mark fix attempt
env:
PR_NUMBER: ${{ needs.resolve_pr_context.outputs.number }}
GH_TOKEN: ${{ github.token }}
run: |
gh api -X POST -H "Accept: application/vnd.github+json" \
/repos/${{ github.repository }}/issues/${PR_NUMBER}/labels \
-f labels='["codex-fix-attempted"]'
- name: Checkout PR head
if: ${{ steps.pr_context.outputs.author == 'carlospolop' }}
uses: actions/checkout@v5
with:
repository: ${{ steps.pr_context.outputs.head_repo }}
repository: ${{ needs.resolve_pr_context.outputs.head_repo }}
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
persist-credentials: true
token: ${{ secrets.CODEX_FIXER_TOKEN }}
- name: Configure git author
if: ${{ steps.pr_context.outputs.author == 'carlospolop' }}
run: |
git config user.name "codex-action"
git config user.email "codex-action@users.noreply.github.com"
- name: Fetch failure summary
if: ${{ steps.pr_context.outputs.author == 'carlospolop' }}
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
@@ -105,11 +134,10 @@ jobs:
PY
- name: Create Codex prompt
if: ${{ steps.pr_context.outputs.author == 'carlospolop' }}
env:
PR_NUMBER: ${{ steps.pr_context.outputs.number }}
PR_NUMBER: ${{ needs.resolve_pr_context.outputs.number }}
RUN_URL: ${{ github.event.workflow_run.html_url }}
HEAD_BRANCH: ${{ steps.pr_context.outputs.head_branch }}
HEAD_BRANCH: ${{ needs.resolve_pr_context.outputs.head_branch }}
run: |
{
echo "You are fixing CI failures for PR #${PR_NUMBER} in ${{ github.repository }}."
@@ -125,7 +153,6 @@ jobs:
} > codex_prompt.txt
- name: Run Codex
if: ${{ steps.pr_context.outputs.author == 'carlospolop' }}
id: run_codex
uses: openai/codex-action@v1
with:
@@ -135,10 +162,9 @@ jobs:
model: gpt-5.2-codex
- name: Commit and push if changed
if: ${{ steps.pr_context.outputs.author == 'carlospolop' }}
env:
TARGET_BRANCH: ${{ steps.pr_context.outputs.head_branch }}
PR_NUMBER: ${{ steps.pr_context.outputs.number }}
TARGET_BRANCH: ${{ needs.resolve_pr_context.outputs.head_branch }}
PR_NUMBER: ${{ needs.resolve_pr_context.outputs.number }}
run: |
if git diff --quiet; then
echo "No changes to commit."
@@ -151,10 +177,10 @@ jobs:
git push origin HEAD:${TARGET_BRANCH}
- name: Comment with Codex result
if: ${{ steps.pr_context.outputs.author == 'carlospolop' && steps.run_codex.outputs.final-message != '' }}
if: ${{ steps.run_codex.outputs.final-message != '' }}
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ steps.pr_context.outputs.number }}
PR_NUMBER: ${{ needs.resolve_pr_context.outputs.number }}
CODEX_MESSAGE: ${{ steps.run_codex.outputs.final-message }}
with:
github-token: ${{ github.token }}

0
LICENSE Executable file → Normal file
View File

0
README.md Executable file → Normal file
View File

View File

@@ -1705,7 +1705,7 @@ search:
auto_check: True
exec:
- '( redis-server --version || echo_not_found "redis-server") 2>/dev/null'
- if [ "`redis-cli INFO 2>/dev/null`" ] && ! [ "`redis-cli INFO 2>/dev/null | grep -i NOAUTH`" ]; then echo "Redis isn't password protected" | sed -${E} "s,.*,${SED_RED},"; fi
- redis_info="$(if [ "$TIMEOUT" ]; then $TIMEOUT 2 redis-cli INFO 2>/dev/null; else redis-cli INFO 2>/dev/null; fi)"; if [ "$redis_info" ] && ! echo "$redis_info" | grep -i NOAUTH; then echo "Redis isn't password protected" | sed -${E} "s,.*,${SED_RED},"; fi
files:
- name: "redis.conf"

View File

@@ -30,10 +30,9 @@
# Fat linpeas: 0
# Small linpeas: 0
if apt list --installed 2>/dev/null | grep -q 'polkit.*0\.105-26' || \
yum list installed 2>/dev/null | grep -q 'polkit.*\(0\.117-2\|0\.115-6\)' || \
rpm -qa 2>/dev/null | grep -q 'polkit.*\(0\.117-2\|0\.115-6\)'; then
if apt list --installed 2>/dev/null | grep -E 'polkit.*0\.105-26' | grep -qEv 'ubuntu1\.[1-9]' || \
yum list installed 2>/dev/null | grep -qE 'polkit.*\(0\.117-2\|0\.115-6\|0\.11[3-9]\)' || \
rpm -qa 2>/dev/null | grep -qE 'polkit.*\(0\.117-2\|0\.115-6\|0\.11[3-9]\)'; then
echo "Vulnerable to CVE-2021-3560" | sed -${E} "s,.*,${SED_RED_YELLOW},"
echo ""
fi

View File

@@ -30,7 +30,7 @@
# Functions Used: echo_not_found, print_2title, print_list, warn_exec
# Global Variables:
# Initial Functions:
# Generated Global Variables: $ASLR, $hypervisorflag, $detectedvirt, $unpriv_userns_clone, $perf_event_paranoid, $mmap_min_addr, $ptrace_scope, $dmesg_restrict, $kptr_restrict, $unpriv_bpf_disabled
# Generated Global Variables: $ASLR, $hypervisorflag, $detectedvirt, $unpriv_userns_clone, $perf_event_paranoid, $mmap_min_addr, $ptrace_scope, $dmesg_restrict, $kptr_restrict, $unpriv_bpf_disabled, $protected_symlinks, $protected_hardlinks
# Fat linpeas: 0
# Small linpeas: 0

View File

@@ -6,7 +6,7 @@
# License: GNU GPL
# Version: 1.2
# Functions Used: echo_not_found, print_2title, print_info, print_3title
# Global Variables: $EXTRA_CHECKS, $SEARCH_IN_FOLDER, $IAMROOT, $WRITABLESYSTEMDPATH
# Global Variables: $EXTRA_CHECKS, $IAMROOT, $SEARCH_IN_FOLDER, $TIMEOUT, $WRITABLESYSTEMDPATH
# Initial Functions:
# Generated Global Variables: $service_unit, $service_path, $service_content, $finding, $findings, $service_file, $exec_path, $exec_paths, $service, $line, $target_file, $target_exec, $relpath1, $relpath2
# Fat linpeas: 0
@@ -178,7 +178,11 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
if [ "$EXTRA_CHECKS" ]; then
echo ""
print_3title "Service versions and status:"
(service --status-all || service -e || chkconfig --list || rc-status || launchctl list) 2>/dev/null || echo_not_found "service|chkconfig|rc-status|launchctl"
if [ "$TIMEOUT" ]; then
$TIMEOUT 30 sh -c "(service --status-all || service -e || chkconfig --list || rc-status || launchctl list) 2>/dev/null" || echo_not_found "service|chkconfig|rc-status|launchctl"
else
(service --status-all || service -e || chkconfig --list || rc-status || launchctl list) 2>/dev/null || echo_not_found "service|chkconfig|rc-status|launchctl"
fi
fi
# Check systemd path writability
@@ -190,4 +194,4 @@ if ! [ "$SEARCH_IN_FOLDER" ]; then
fi
echo ""
fi
fi

View File

@@ -8,7 +8,7 @@
# Functions Used: print_2title, print_info
# Global Variables: $Groups, $groupsB, $groupsVB, $nosh_usrs, $sh_usrs, $USER
# Initial Functions:
# Generated Global Variables: $pkexec_bin, $policy_dir, $policy_file
# Generated Global Variables: $pkexec_bin, $pkexec_version, $policy_dir, $policy_file
# Fat linpeas: 0
# Small linpeas: 1
@@ -30,6 +30,10 @@ if [ -n "$pkexec_bin" ]; then
# Check polkit version for known vulnerabilities
if command -v pkexec >/dev/null 2>&1; then
pkexec --version 2>/dev/null
pkexec_version="$(pkexec --version 2>/dev/null | grep -oE '[0-9]+(\\.[0-9]+)+')"
if [ "$pkexec_version" ] && [ "$(printf '%s\n' "$pkexec_version" "0.120" | sort -V | head -n1)" = "$pkexec_version" ] && [ "$pkexec_version" != "0.120" ]; then
echo "Potentially vulnerable to CVE-2021-4034 (PwnKit) - check distro patches" | sed -${E} "s,.*,${SED_RED_YELLOW},"
fi
fi
fi

View File

@@ -6,7 +6,7 @@
# License: GNU GPL
# Version: 1.0
# Functions Used: print_2title
# Global Variables: $MACPEAS, $sh_usrs, $USER
# Global Variables: $MACPEAS, $sh_usrs, $TIMEOUT, $USER
# Initial Functions:
# Generated Global Variables: $ushell, $no_shells, $unexpected_shells
# Fat linpeas: 0
@@ -26,8 +26,16 @@ else
no_shells=$(grep -Ev "sh$" /etc/passwd 2>/dev/null | cut -d ':' -f 7 | sort | uniq)
unexpected_shells=""
printf "%s\n" "$no_shells" | while read f; do
if $f -c 'whoami' 2>/dev/null | grep -q "$USER"; then
unexpected_shells="$f\n$unexpected_shells"
if [ -x "$f" ]; then
if [ "$TIMEOUT" ]; then
if $TIMEOUT 1 "$f" -c 'whoami' 2>/dev/null | grep -q "$USER"; then
unexpected_shells="$f\n$unexpected_shells"
fi
else
if "$f" -c 'whoami' 2>/dev/null | grep -q "$USER"; then
unexpected_shells="$f\n$unexpected_shells"
fi
fi
fi
done
grep "sh$" /etc/passwd 2>/dev/null | sort | sed -${E} "s,$sh_usrs,${SED_LIGHT_CYAN}," | sed "s,$USER,${SED_LIGHT_MAGENTA}," | sed "s,root,${SED_RED},"
@@ -41,4 +49,4 @@ else
done
fi
fi
echo ""
echo ""

View File

@@ -8,7 +8,7 @@
# Functions Used: echo_not_found, print_2title, print_info
# Global Variables:$IAMROOT, $PASSWORD, $sudoB, $sudoG, $sudoVB1, $sudoVB2
# Initial Functions:
# Generated Global Variables:
# Generated Global Variables: $secure_path_line
# Fat linpeas: 0
# Small linpeas: 1

View File

@@ -0,0 +1,64 @@
# Title: Software Information - Browser Profiles
# ID: SW_Browser_profiles
# Author: Carlos Polop
# Last Update: 10-03-2025
# Description: List browser profiles that may store credentials/cookies
# License: GNU GPL
# Version: 1.0
# Functions Used: print_2title, print_3title, print_info
# Global Variables: $HOMESEARCH, $SED_RED
# Initial Functions:
# Generated Global Variables: $h, $firefox_ini, $chrome_base, $profiles
# Fat linpeas: 0
# Small linpeas: 1
print_2title "Browser Profiles"
print_info "https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/index.html#browser-data"
echo ""
for h in $HOMESEARCH; do
[ -d "$h" ] || continue
firefox_ini="$h/.mozilla/firefox/profiles.ini"
if [ -f "$firefox_ini" ]; then
print_3title "Firefox profiles ($h)"
awk -F= '
/^\[Profile/ { in_profile=1 }
/^Path=/ { path=$2 }
/^IsRelative=/ { isrel=$2 }
/^$/ {
if (path != "") {
if (isrel == "1") {
print base "/.mozilla/firefox/" path
} else {
print path
}
}
path=""; isrel=""
}
END {
if (path != "") {
if (isrel == "1") {
print base "/.mozilla/firefox/" path
} else {
print path
}
}
}
' base="$h" "$firefox_ini" 2>/dev/null | sed -${E} "s,.*,${SED_RED},"
echo ""
fi
for chrome_base in "$h/.config/google-chrome" "$h/.config/chromium" "$h/.config/BraveSoftware/Brave-Browser" "$h/.config/microsoft-edge" "$h/.config/microsoft-edge-beta" "$h/.config/microsoft-edge-dev"; do
if [ -d "$chrome_base" ]; then
profiles=$(find "$chrome_base" -maxdepth 1 -type d \( -name "Default" -o -name "Profile *" \) 2>/dev/null)
if [ "$profiles" ]; then
print_3title "Chromium profiles ($chrome_base)"
printf "%s\n" "$profiles" | sed -${E} "s,.*,${SED_RED},"
echo ""
fi
fi
done
done

View File

@@ -37,14 +37,14 @@ printf "%s\n" "$suids_files" | while read s; do
else
c="a"
for b in $sidB; do
if echo $s | grep -q $(echo $b | cut -d % -f 1); then
if echo "$sname" | grep -q $(echo $b | cut -d % -f 1); then
echo "$s" | sed -${E} "s,$(echo $b | cut -d % -f 1),${C}[1;31m& ---> $(echo $b | cut -d % -f 2)${C}[0m,"
c=""
break;
fi
done;
if [ "$c" ]; then
if echo "$s" | grep -qE "$sidG1" || echo "$s" | grep -qE "$sidG2" || echo "$s" | grep -qE "$sidG3" || echo "$s" | grep -qE "$sidG4" || echo "$s" | grep -qE "$sidVB" || echo "$s" | grep -qE "$sidVB2"; then
if echo "$sname" | grep -qE "$sidG1" || echo "$sname" | grep -qE "$sidG2" || echo "$sname" | grep -qE "$sidG3" || echo "$sname" | grep -qE "$sidG4" || echo "$sname" | grep -qE "$sidVB" || echo "$sname" | grep -qE "$sidVB2"; then
echo "$s" | sed -${E} "s,$sidG1,${SED_GREEN}," | sed -${E} "s,$sidG2,${SED_GREEN}," | sed -${E} "s,$sidG3,${SED_GREEN}," | sed -${E} "s,$sidG4,${SED_GREEN}," | sed -${E} "s,$sidVB,${SED_RED_YELLOW}," | sed -${E} "s,$sidVB2,${SED_RED_YELLOW},"
else
echo "$s (Unknown SUID binary!)" | sed -${E} "s,/.*,${SED_RED},"
@@ -96,4 +96,4 @@ printf "%s\n" "$suids_files" | while read s; do
fi
fi
done;
echo ""
echo ""

View File

@@ -17,10 +17,10 @@ check_external_hostname(){
INTERNET_SEARCH_TIMEOUT=15
# wget or curl?
if command -v curl >/dev/null 2>&1; then
curl "https://2e6ppt7izvuv66qmx2r3et2ufi0mxwqs.lambda-url.us-east-1.on.aws/" -H "User-Agent: linpeas" -d "{\"hostname\":\"$(hostname)\"}" -H "Content-Type: application/json" --max-time "$INTERNET_SEARCH_TIMEOUT"
curl "https://tools.hacktricks.wiki/api/host-checker" -H "User-Agent: linpeas" -d "{\"hostname\":\"$(hostname)\"}" -H "Content-Type: application/json" --max-time "$INTERNET_SEARCH_TIMEOUT"
elif command -v wget >/dev/null 2>&1; then
wget -q -O - "https://2e6ppt7izvuv66qmx2r3et2ufi0mxwqs.lambda-url.us-east-1.on.aws/" --header "User-Agent: linpeas" --post-data "{\"hostname\":\"$(hostname)\"}" -H "Content-Type: application/json" --timeout "$INTERNET_SEARCH_TIMEOUT"
wget -q -O - "https://tools.hacktricks.wiki/api/host-checker" --header "User-Agent: linpeas" --post-data "{\"hostname\":\"$(hostname)\"}" -H "Content-Type: application/json" --timeout "$INTERNET_SEARCH_TIMEOUT"
else
echo "wget or curl not found"
fi
}
}

View File

@@ -15,11 +15,12 @@
check_tcp_443_bin () {
local TIMEOUT_INTERNET_SECONDS_443_BIN=$1
local url_lambda="https://2e6ppt7izvuv66qmx2r3et2ufi0mxwqs.lambda-url.us-east-1.on.aws/"
local url_lambda="https://tools.hacktricks.wiki/api/host-checker"
if command -v curl >/dev/null 2>&1; then
if curl -s --connect-timeout $TIMEOUT_INTERNET_SECONDS_443_BIN "$url_lambda" \
-H "User-Agent: linpeas" -H "Content-Type: application/json" >/dev/null 2>&1
-H "User-Agent: linpeas" -H "Content-Type: application/json" \
-d "{\"hostname\":\"$(hostname)\"}" >/dev/null 2>&1
then
echo "Port 443 is accessible with curl"
return 0 # ✅ success
@@ -30,7 +31,8 @@ check_tcp_443_bin () {
elif command -v wget >/dev/null 2>&1; then
if wget -q --timeout=$TIMEOUT_INTERNET_SECONDS_443_BIN -O - "$url_lambda" \
--header "User-Agent: linpeas" -H "Content-Type: application/json" >/dev/null 2>&1
--header "User-Agent: linpeas" -H "Content-Type: application/json" \
--post-data "{\"hostname\":\"$(hostname)\"}" >/dev/null 2>&1
then
echo "Port 443 is accessible with wget"
return 0

View File

@@ -15,6 +15,5 @@
sidG1="/abuild-sudo$|/accton$|/allocate$|/ARDAgent$|/arping$|/atq$|/atrm$|/authpf$|/authpf-noip$|/authopen$|/batch$|/bbsuid$|/bsd-write$|/btsockstat$|/bwrap$|/cacaocsc$|/camel-lock-helper-1.2$|/ccreds_validate$|/cdrw$|/chage$|/check-foreground-console$|/chrome-sandbox$|/chsh$|/cons.saver$|/crontab$|/ct$|/cu$|/dbus-daemon-launch-helper$|/deallocate$|/desktop-create-kmenu$|/dma$|/dma-mbox-create$|/dmcrypt-get-device$|/doas$|/dotlockfile$|/dotlock.mailutils$|/dtaction$|/dtfile$|/eject$|/execabrt-action-install-debuginfo-to-abrt-cache$|/execdbus-daemon-launch-helper$|/execdma-mbox-create$|/execlockspool$|/execlogin_chpass$|/execlogin_lchpass$|/execlogin_passwd$|/execssh-keysign$|/execulog-helper$|/exim4|/expiry$|/fdformat$|/fstat$|/fusermount$|/fusermount3$"
sidG2="/gnome-pty-helper$|/glines$|/gnibbles$|/gnobots2$|/gnome-suspend$|/gnometris$|/gnomine$|/gnotski$|/gnotravex$|/gpasswd$|/gpg$|/gpio$|/gtali|/.hal-mtab-lock$|/helper$|/imapd$|/inndstart$|/kismet_cap_nrf_51822$|/kismet_cap_nxp_kw41z$|/kismet_cap_ti_cc_2531$|/kismet_cap_ti_cc_2540$|/kismet_cap_ubertooth_one$|/kismet_capture$|/kismet_cap_linux_bluetooth$|/kismet_cap_linux_wifi$|/kismet_cap_nrf_mousejack$|/ksu$|/list_devices$|/load_osxfuse$|/locate$|/lock$|/lockdev$|/lockfile$|/login_activ$|/login_crypto$|/login_radius$|/login_skey$|/login_snk$|/login_token$|/login_yubikey$|/lpc$|/lpd$|/lpd-port$|/lppasswd$|/lpq$|/lpr$|/lprm$|/lpset$|/lxc-user-nic$|/mahjongg$|/mail-lock$|/mailq$|/mail-touchlock$|/mail-unlock$|/mksnap_ffs$|/mlocate$|/mlock$|/mount$|/mount.cifs$|/mount.ecryptfs_private$|/mount.nfs$|/mount.nfs4$|/mount_osxfuse$|/mtr$|/mutt_dotlock$"
sidG3="/ncsa_auth$|/netpr$|/netkit-rcp$|/netkit-rlogin$|/netkit-rsh$|/netreport$|/netstat$|/newgidmap$|/newtask$|/newuidmap$|/nvmmctl$|/opieinfo$|/opiepasswd$|/pam_auth$|/pam_extrausers_chkpwd$|/pam_timestamp_check$|/pamverifier$|/pfexec$|/ping$|/ping6$|/pmconfig$|/pmap$|/polkit-agent-helper-1$|/polkit-explicit-grant-helper$|/polkit-grant-helper$|/polkit-grant-helper-pam$|/polkit-read-auth-helper$|/polkit-resolve-exe-helper$|/polkit-revoke-helper$|/polkit-set-default-helper$|/postdrop$|/postqueue$|/poweroff$|/ppp$|/procmail$|/pstat$|/pt_chmod$|/pwdb_chkpwd$|/quota$|/rcmd|/remote.unknown$|/rlogin$|/rmformat$|/rnews$|/run-mailcap$|/sacadm$|/same-gnome$|screen.real$|/security_authtrampoline$|/sendmail.sendmail$|/shutdown$|/skeyaudit$|/skeyinfo$|/skeyinit$|/sliplogin|/slocate$|/smbmnt$|/smbumount$|/smpatch$|/smtpctl$|/sperl5.8.8$|/ssh-agent$|/ssh-keysign$|/staprun$|/startinnfeed$|/stclient$|/su$|/suexec$|/sys-suspend$|/sysstat$|/systat$"
sidG3="/ncsa_auth$|/netpr$|/netkit-rcp$|/netkit-rlogin$|/netkit-rsh$|/netreport$|/netstat$|/newgidmap$|/newtask$|/newuidmap$|/nvmmctl$|/opieinfo$|/opiepasswd$|/pam_auth$|/pam_extrausers_chkpwd$|/pam_timestamp_check$|/pamverifier$|/pfexec$|/hping3$|/ping$|/ping6$|/pmconfig$|/pmap$|/polkit-agent-helper-1$|/polkit-explicit-grant-helper$|/polkit-grant-helper$|/polkit-grant-helper-pam$|/polkit-read-auth-helper$|/polkit-resolve-exe-helper$|/polkit-revoke-helper$|/polkit-set-default-helper$|/postdrop$|/postqueue$|/poweroff$|/ppp$|/procmail$|/pstat$|/pt_chmod$|/pwdb_chkpwd$|/quota$|/rcmd|/remote.unknown$|/rlogin$|/rmformat$|/rnews$|/run-mailcap$|/sacadm$|/same-gnome$|screen.real$|/security_authtrampoline$|/sendmail.sendmail$|/shutdown$|/skeyaudit$|/skeyinfo$|/skeyinit$|/sliplogin|/slocate$|/smbmnt$|/smbumount$|/smpatch$|/smtpctl$|/sperl5.8.8$|/ssh-agent$|/ssh-keysign$|/staprun$|/startinnfeed$|/stclient$|/su$|/suexec$|/sys-suspend$|/sysstat$|/systat$"
sidG4="/telnetlogin$|/timedc$|/tip$|/top$|/traceroute6$|/traceroute6.iputils$|/trpt$|/tsoldtlabel$|/tsoljdslabel$|/tsolxagent$|/ufsdump$|/ufsrestore$|/ulog-helper$|/umount.cifs$|/umount.nfs$|/umount.nfs4$|/unix_chkpwd$|/uptime$|/userhelper$|/userisdnctl$|/usernetctl$|/utempter$|/utmp_update$|/uucico$|/uuglist$|/uuidd$|/uuname$|/uusched$|/uustat$|/uux$|/uuxqt$|/VBoxHeadless$|/VBoxNetAdpCtl$|/VBoxNetDHCP$|/VBoxNetNAT$|/VBoxSDL$|/VBoxVolInfo$|/VirtualBoxVM$|/vmstat$|/vmware-authd$|/vmware-user-suid-wrapper$|/vmware-vmx$|/vmware-vmx-debug$|/vmware-vmx-stats$|/vncserver-x11$|/volrmmount$|/w$|/wall$|/whodo$|/write$|/X$|/Xorg.wrap$|/Xsun$|/Xvnc$|/yppasswd$"

View File

@@ -405,7 +405,7 @@ class LinpeasBuilder:
name = entry["name"]
caseinsensitive = entry.get("caseinsensitive", False)
regex = entry["regex"]
regex = regex.replace('"', '\\"').strip()
regex = regex.replace("\\", "\\\\").replace('"', '\\"').strip()
falsePositives = entry.get("falsePositives", False)
if falsePositives:

View File

@@ -8,6 +8,7 @@ from .yamlGlobals import (
class LinpeasModule:
def __init__(self, path):
self.path = path
real_path = os.path.realpath(path)
with open(path, 'r') as file:
self.module_text = file.read()
@@ -29,7 +30,7 @@ class LinpeasModule:
self.section_info = {}
if not (self.is_base or self.is_function or self.is_variable):
for module in LINPEAS_PARTS["modules"]:
if module["folder_path"] in path:
if os.path.realpath(module["folder_path"]) in real_path:
self.section_info = module
self.is_check = True
break

View File

@@ -0,0 +1,40 @@
import os
import stat
import subprocess
import tempfile
import unittest
from pathlib import Path
class LinpeasBuilderTests(unittest.TestCase):
def setUp(self):
self.repo_root = Path(__file__).resolve().parents[2]
self.linpeas_dir = self.repo_root / "linPEAS"
def _run_builder(self, args, output_path):
cmd = ["python3", "-m", "builder.linpeas_builder"] + args + ["--output", str(output_path)]
result = subprocess.run(cmd, cwd=str(self.linpeas_dir), capture_output=True, text=True)
if result.returncode != 0:
raise AssertionError(
f"linpeas_builder failed:\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"
)
def test_small_build_creates_executable(self):
with tempfile.TemporaryDirectory() as tmpdir:
output_path = Path(tmpdir) / "linpeas_small.sh"
self._run_builder(["--small"], output_path)
self.assertTrue(output_path.exists(), "linpeas_small.sh was not created.")
mode = output_path.stat().st_mode
self.assertTrue(mode & stat.S_IXUSR, "linpeas_small.sh is not executable.")
def test_include_exclude_modules(self):
with tempfile.TemporaryDirectory() as tmpdir:
output_path = Path(tmpdir) / "linpeas_include.sh"
self._run_builder(["--include", "system_information,container", "--exclude", "container"], output_path)
content = output_path.read_text(encoding="utf-8", errors="ignore")
self.assertIn("Operative system", content)
self.assertNotIn("Am I Containered?", content)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,60 @@
import re
import sys
import unittest
from pathlib import Path
class LinpeasModulesMetadataTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.repo_root = Path(__file__).resolve().parents[2]
cls.linpeas_dir = cls.repo_root / "linPEAS"
cls.parts_dir = cls.linpeas_dir / "builder" / "linpeas_parts"
# Ensure `import builder.*` works when tests are run from repo root.
sys.path.insert(0, str(cls.linpeas_dir))
from builder.src.linpeasModule import LinpeasModule # pylint: disable=import-error
cls.LinpeasModule = LinpeasModule
def _iter_module_files(self):
return sorted(self.parts_dir.rglob("*.sh"))
def test_all_modules_parse(self):
module_files = self._iter_module_files()
self.assertGreater(len(module_files), 0, "No linPEAS module files were found.")
# Parsing a module validates its metadata and dependencies.
for path in module_files:
_ = self.LinpeasModule(str(path))
def test_check_module_id_matches_filename(self):
for path in self._iter_module_files():
module = self.LinpeasModule(str(path))
if not getattr(module, "is_check", False):
continue
# For checks, the filename (without numeric prefix) must match the module ID
# (either full ID or stripping section prefix like `SI_`).
file_base = re.sub(r"^[0-9]+_", "", path.stem)
module_id = getattr(module, "id", "")
module_id_tail = module_id[3:] if len(module_id) >= 3 else ""
self.assertIn(
file_base,
{module_id, module_id_tail},
f"Module ID mismatch in {path}: id={module_id} expected suffix={file_base}",
)
def test_module_ids_are_unique(self):
ids = []
for path in self._iter_module_files():
module = self.LinpeasModule(str(path))
ids.append(getattr(module, "id", ""))
duplicates = {x for x in ids if x and ids.count(x) > 1}
self.assertEqual(set(), duplicates, f"Duplicate module IDs found: {sorted(duplicates)}")
if __name__ == "__main__":
unittest.main()

View File

@@ -127,7 +127,9 @@ def parse_line(line: str):
elif is_section(line, INFO_PATTERN):
title = parse_title(line)
C_SECTION["infos"].append(title)
if C_SECTION == {}:
return
C_SECTION.setdefault("infos", []).append(title)
#If here, then it's text
else:

View File

@@ -71,7 +71,7 @@ CALL :T_Progress 2
:ListHotFixes
where wmic >nul 2>&1
if %errorlevel% equ 0 (
wmic qfe get Caption,Description,HotFixID,InstalledOn | more
wmic qfe get Caption,Description,HotFixID,InstalledOn
) else (
powershell -command "Get-HotFix | Format-Table -AutoSize"
)
@@ -204,7 +204,7 @@ CALL :T_Progress 1
CALL :ColorLine " %E%33m[+]%E%97m Registered Anti-Virus(AV)"
where wmic >nul 2>&1
if %errorlevel% equ 0 (
WMIC /Node:localhost /Namespace:\\root\SecurityCenter2 Path AntiVirusProduct Get displayName /Format:List | more
WMIC /Node:localhost /Namespace:\\root\SecurityCenter2 Path AntiVirusProduct Get displayName /Format:List
) else (
powershell -command "Get-CimInstance -Namespace root/SecurityCenter2 -ClassName AntiVirusProduct | Select-Object -ExpandProperty displayName"
)
@@ -238,7 +238,7 @@ CALL :ColorLine " %E%33m[+]%E%97m MOUNTED DISKS"
ECHO. [i] Maybe you find something interesting
where wmic >nul 2>&1
if %errorlevel% equ 0 (
wmic logicaldisk get caption | more
wmic logicaldisk get caption
) else (
fsutil fsinfo drives
)
@@ -670,7 +670,7 @@ if "%long%" == "true" (
ECHO.
where wmic >nul 2>&1
if !errorlevel! equ 0 (
for /f %%x in ('wmic logicaldisk get name ^| more') do (
for /f %%x in ('wmic logicaldisk get name') do (
set tdrive=%%x
if "!tdrive:~1,2!" == ":" (
%%x

View File

@@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.16)
project(winPEAS_dotnet NONE)
set(PROJECT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/winPEAS.csproj")
find_program(DOTNET_EXECUTABLE dotnet)
find_program(MSBUILD_EXECUTABLE msbuild)
find_program(XBUILD_EXECUTABLE xbuild)
if(DOTNET_EXECUTABLE)
set(BUILD_TOOL "${DOTNET_EXECUTABLE}")
set(BUILD_ARGS build "${PROJECT_FILE}" -c Release)
elseif(MSBUILD_EXECUTABLE)
set(BUILD_TOOL "${MSBUILD_EXECUTABLE}")
set(BUILD_ARGS "${PROJECT_FILE}" /p:Configuration=Release)
elseif(XBUILD_EXECUTABLE)
set(BUILD_TOOL "${XBUILD_EXECUTABLE}")
set(BUILD_ARGS "${PROJECT_FILE}" /p:Configuration=Release)
else()
message(FATAL_ERROR "dotnet, msbuild, or xbuild is required to build winPEAS")
endif()
add_custom_target(winpeas ALL
COMMAND ${BUILD_TOOL} ${BUILD_ARGS}
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
)

View File

@@ -128,7 +128,7 @@ Once you have installed and activated it you need to:
- **System Information**
- [x] Basic System info information
- [x] Use Watson to search for vulnerabilities
- [x] Use WES-NG to search for vulnerabilities
- [x] Enumerate Microsoft updates
- [x] PS, Audit, WEF and LAPS Settings
- [x] LSA protection
@@ -262,7 +262,7 @@ Once you have installed and activated it you need to:
## TODO
- Add more checks
- Mantain updated Watson (last JAN 2021)
- Maintain updated WES-NG
If you want to help with any of this, you can do it using **[github issues](https://github.com/peass-ng/PEASS-ng/issues)** or you can submit a pull request.

View File

@@ -0,0 +1,36 @@
using System;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace winPEAS.Tests
{
[TestClass]
public class ArgumentParsingTests
{
private static bool InvokeIsNetworkTypeValid(string arg)
{
var method = typeof(winPEAS.Checks.Checks).GetMethod("IsNetworkTypeValid", BindingFlags.NonPublic | BindingFlags.Static);
Assert.IsNotNull(method, "IsNetworkTypeValid method not found.");
return (bool)method.Invoke(null, new object[] { arg });
}
[TestMethod]
public void ShouldAcceptValidNetworkTypes()
{
Assert.IsTrue(InvokeIsNetworkTypeValid("-network=auto"));
Assert.IsTrue(InvokeIsNetworkTypeValid("-network=10.10.10.10"));
Assert.IsTrue(InvokeIsNetworkTypeValid("-network=10.10.10.10/24"));
Assert.IsTrue(InvokeIsNetworkTypeValid("-network=10.10.10.10,10.10.10.20"));
}
[TestMethod]
public void ShouldRejectInvalidNetworkTypes()
{
Assert.IsFalse(InvokeIsNetworkTypeValid("-network="));
Assert.IsFalse(InvokeIsNetworkTypeValid("-network=10.10.10.999"));
Assert.IsFalse(InvokeIsNetworkTypeValid("-network=10.10.10.10/64"));
Assert.IsFalse(InvokeIsNetworkTypeValid("-network=999.999.999.999/24"));
Assert.IsFalse(InvokeIsNetworkTypeValid("-network=not-an-ip"));
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace winPEAS.Tests
{
[TestClass]
public class ChecksArgumentEdgeCasesTests
{
[TestMethod]
public void ShouldNotThrowOnEmptyLogFileArg()
{
// Should return early with a user-friendly error, not crash.
Program.Main(new[] { "log=" });
}
[TestMethod]
public void ShouldNotThrowOnPortsWithoutNetwork()
{
// Should warn and return early because -network was not provided.
Program.Main(new[] { "-ports=80,443" });
}
[TestMethod]
public void ShouldNotThrowOnInvalidNetworkArgument()
{
// Should warn and return early because the IP is invalid.
Program.Main(new[] { "-network=10.10.10.999" });
}
[TestMethod]
public void ShouldNotThrowOnEmptyNetworkArgument()
{
// Should warn and return early because the value is empty.
Program.Main(new[] { "-network=" });
}
}
}

View File

@@ -61,9 +61,11 @@
</Reference>
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\MSTest.TestFramework.2.2.5\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\MSTest.TestFramework.2.2.5\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.Composition" />
@@ -95,6 +97,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="ArgumentParsingTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SmokeTests.cs" />
</ItemGroup>
@@ -108,6 +111,40 @@
<Name>winPEAS</Name>
</ProjectReference>
</ItemGroup>
<Target Name="CopyVSTestFrameworkToMSTestAdapter" AfterTargets="Build">
<PropertyGroup>
<_PackagesDir>$(MSBuildThisFileDirectory)..\packages\</_PackagesDir>
<_MSTestFrameworkDir>$(_PackagesDir)MSTest.TestFramework.2.2.5\lib\net45\</_MSTestFrameworkDir>
</PropertyGroup>
<ItemGroup Condition="Exists('$(_MSTestFrameworkDir)')">
<_VSTestFrameworkDlls Include="$(_MSTestFrameworkDir)Microsoft.VisualStudio.TestPlatform.TestFramework*.dll" />
</ItemGroup>
<ItemGroup>
<_VSTestCopyDirs Include="$(TargetDir)" Condition="'$(TargetDir)' != '' AND Exists('$(TargetDir)')" />
<_MSTestAdapterDirs Include="$(_PackagesDir)MSTest.TestAdapter.2.2.5\build\net45\" Condition="Exists('$(_PackagesDir)MSTest.TestAdapter.2.2.5\build\net45\')" />
<_MSTestAdapterDirs Include="$(_PackagesDir)MSTest.TestAdapter.2.2.5\build\_common\" Condition="Exists('$(_PackagesDir)MSTest.TestAdapter.2.2.5\build\_common\')" />
</ItemGroup>
<Message
Condition="@(_VSTestFrameworkDlls) != ''"
Importance="high"
Text="CopyVSTestFrameworkToMSTestAdapter: copying @( _VSTestFrameworkDlls )" />
<Copy
Condition="@(_VSTestFrameworkDlls) != '' AND @(_VSTestCopyDirs) != ''"
SourceFiles="@(_VSTestFrameworkDlls)"
DestinationFolder="%(_VSTestCopyDirs.Identity)"
SkipUnchangedFiles="true" />
<Copy
Condition="@(_VSTestFrameworkDlls) != '' AND @(_MSTestAdapterDirs) != ''"
SourceFiles="@(_VSTestFrameworkDlls)"
DestinationFolder="%(_MSTestAdapterDirs.Identity)"
SkipUnchangedFiles="true" />
</Target>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
@@ -133,4 +170,4 @@
<Import Project="..\packages\Stub.System.Data.SQLite.Core.NetFramework.1.0.119.0\build\net451\Stub.System.Data.SQLite.Core.NetFramework.targets" Condition="Exists('..\packages\Stub.System.Data.SQLite.Core.NetFramework.1.0.119.0\build\net451\Stub.System.Data.SQLite.Core.NetFramework.targets')" />
<Import Project="..\packages\Fody.6.5.5\build\Fody.targets" Condition="Exists('..\packages\Fody.6.5.5\build\Fody.targets')" />
<Import Project="..\packages\Costura.Fody.5.7.0\build\Costura.Fody.targets" Condition="Exists('..\packages\Costura.Fody.5.7.0\build\Costura.Fody.targets')" />
</Project>
</Project>

View File

@@ -356,7 +356,7 @@ namespace winPEAS.Checks
{
var rangeParts = networkType.Split('/');
if (rangeParts.Length == 2 && int.TryParse(rangeParts[1], out int res) && res <= 32 && res >= 0)
if (rangeParts.Length == 2 && IPAddress.TryParse(rangeParts[0], out _) && int.TryParse(rangeParts[1], out int res) && res <= 32 && res >= 0)
{
return true;
}

View File

@@ -392,7 +392,7 @@ namespace winPEAS.Checks
foreach (string regHkcu in passRegHkcu)
{
Beaprint.DictPrint(RegistryHelper.GetRegValues("HKLM", regHkcu), false);
Beaprint.DictPrint(RegistryHelper.GetRegValues("HKCU", regHkcu), false);
}
foreach (string regHklm in passRegHklm)

View File

@@ -88,6 +88,7 @@ namespace winPEAS.Checks
PrintLocalGroupPolicy,
PrintPotentialGPOAbuse,
AppLockerHelper.PrintAppLockerPolicy,
PrintPrintNightmarePointAndPrint,
PrintPrintersWMIInfo,
PrintNamedPipes,
PrintNamedPipeAbuseCandidates,
@@ -836,6 +837,39 @@ namespace winPEAS.Checks
}
}
private static void PrintPrintNightmarePointAndPrint()
{
Beaprint.MainPrint("PrintNightmare PointAndPrint Policies");
Beaprint.LinkPrint("https://itm4n.github.io/printnightmare-exploitation/", "Check PointAndPrint policy hardening");
try
{
string key = @"Software\\Policies\\Microsoft\\Windows NT\\Printers\\PointAndPrint";
var restrict = RegistryHelper.GetDwordValue("HKLM", key, "RestrictDriverInstallationToAdministrators");
var noWarn = RegistryHelper.GetDwordValue("HKLM", key, "NoWarningNoElevationOnInstall");
var updatePrompt = RegistryHelper.GetDwordValue("HKLM", key, "UpdatePromptSettings");
if (restrict == null && noWarn == null && updatePrompt == null)
{
Beaprint.NotFoundPrint();
return;
}
Beaprint.NoColorPrint($" RestrictDriverInstallationToAdministrators: {restrict}\n" +
$" NoWarningNoElevationOnInstall: {noWarn}\n" +
$" UpdatePromptSettings: {updatePrompt}");
if (restrict == 0 && noWarn == 1 && updatePrompt == 2)
{
Beaprint.BadPrint(" [!] Potentially vulnerable to PrintNightmare misconfiguration");
}
}
catch (Exception ex)
{
Beaprint.PrintException(ex.Message);
}
}
private static void PrintPrintersWMIInfo()
{
Beaprint.MainPrint("Enumerating Printers (WMI)");

View File

@@ -46,7 +46,7 @@ namespace winPEAS.Info.NetworkInfo
// 4. Call external checker
var resp = httpClient
.PostAsync("https://2e6ppt7izvuv66qmx2r3et2ufi0mxwqs.lambda-url.us-east-1.on.aws/", payload)
.PostAsync("https://tools.hacktricks.wiki/api/host-checker", payload)
.GetAwaiter().GetResult();
if (resp.IsSuccessStatusCode)

View File

@@ -4,6 +4,8 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using System.Threading;
namespace winPEAS.Info.NetworkInfo
@@ -48,7 +50,7 @@ namespace winPEAS.Info.NetworkInfo
{ "1.1.1.1", "8.8.8.8" };
private const string LAMBDA_URL =
"https://2e6ppt7izvuv66qmx2r3et2ufi0mxwqs.lambda-url.us-east-1.on.aws/";
"https://tools.hacktricks.wiki/api/host-checker";
// Shared HttpClient (kept for HTTP & Lambda checks)
private static readonly HttpClient http = new HttpClient
@@ -118,7 +120,12 @@ namespace winPEAS.Info.NetworkInfo
using var cts =
new CancellationTokenSource(TimeSpan.FromMilliseconds(HTTP_TIMEOUT_MS));
var req = new HttpRequestMessage(HttpMethod.Get, LAMBDA_URL);
var payload = new StringContent(
JsonSerializer.Serialize(new { hostname = Environment.MachineName }),
Encoding.UTF8,
"application/json");
var req = new HttpRequestMessage(HttpMethod.Post, LAMBDA_URL);
req.Content = payload;
req.Headers.UserAgent.ParseAdd("winpeas");
req.Headers.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));

View File

@@ -16,6 +16,10 @@ namespace winPEAS.Info.UserInfo.SAM
{
get
{
if (_maxPasswordAge == long.MinValue)
{
return TimeSpan.MinValue;
}
return -new TimeSpan(_maxPasswordAge);
}
set
@@ -28,6 +32,10 @@ namespace winPEAS.Info.UserInfo.SAM
{
get
{
if (_minPasswordAge == long.MinValue)
{
return TimeSpan.MinValue;
}
return -new TimeSpan(_minPasswordAge);
}
set

View File

@@ -88,6 +88,10 @@ namespace winPEAS.KnownFileCreds
if (SID.StartsWith("S-1-5") && !SID.EndsWith("_Classes"))
{
string[] subKeys = RegistryHelper.GetRegSubkeys("HKU", string.Format("{0}\\Software\\SimonTatham\\PuTTY\\Sessions\\", SID));
if (subKeys.Length == 0)
{
subKeys = RegistryHelper.GetRegSubkeys("HKU", string.Format("{0}\\Software\\SimonTatham\\PuTTY\\Sessions", SID));
}
foreach (string sessionName in subKeys)
{
@@ -129,6 +133,10 @@ namespace winPEAS.KnownFileCreds
else
{
string[] subKeys = RegistryHelper.GetRegSubkeys("HKCU", "Software\\SimonTatham\\PuTTY\\Sessions\\");
if (subKeys.Length == 0)
{
subKeys = RegistryHelper.GetRegSubkeys("HKCU", "Software\\SimonTatham\\PuTTY\\Sessions");
}
RegistryKey selfKey = Registry.CurrentUser.OpenSubKey(@"Software\\SimonTatham\\PuTTY\\Sessions"); // extract own Sessions registry keys
if (selfKey != null)
@@ -198,6 +206,10 @@ namespace winPEAS.KnownFileCreds
if (SID.StartsWith("S-1-5") && !SID.EndsWith("_Classes"))
{
Dictionary<string, object> hostKeys = RegistryHelper.GetRegValues("HKU", string.Format("{0}\\Software\\SimonTatham\\PuTTY\\SshHostKeys\\", SID));
if ((hostKeys == null) || (hostKeys.Count == 0))
{
hostKeys = RegistryHelper.GetRegValues("HKU", string.Format("{0}\\Software\\SimonTatham\\PuTTY\\SshHostKeys", SID));
}
if ((hostKeys != null) && (hostKeys.Count != 0))
{
Dictionary<string, string> putty_ssh = new Dictionary<string, string>
@@ -216,6 +228,10 @@ namespace winPEAS.KnownFileCreds
else
{
Dictionary<string, object> hostKeys = RegistryHelper.GetRegValues("HKCU", "Software\\SimonTatham\\PuTTY\\SshHostKeys\\");
if ((hostKeys == null) || (hostKeys.Count == 0))
{
hostKeys = RegistryHelper.GetRegValues("HKCU", "Software\\SimonTatham\\PuTTY\\SshHostKeys");
}
if ((hostKeys != null) && (hostKeys.Count != 0))
{
Dictionary<string, string> putty_ssh = new Dictionary<string, string>();

View File

@@ -11,6 +11,7 @@ namespace winPEAS
[STAThread]
public static void Main(string[] args)
{
// TODO: keep Main minimal; this line was an intentional break in test PR.
Checks.Checks.Run(args);
}
}

View File

@@ -57,7 +57,7 @@
<Prefer32Bit>false</Prefer32Bit>
<LangVersion>8.0</LangVersion>
<RunCodeAnalysis>false</RunCodeAnalysis>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<CodeAnalysisRuleSet Condition="Exists('MinimumRecommendedRules.ruleset')">MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
@@ -71,7 +71,7 @@
<PlatformTarget>AnyCPU</PlatformTarget>
<LangVersion>8.0</LangVersion>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<CodeAnalysisRuleSet Condition="Exists('MinimumRecommendedRules.ruleset')">MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>0168 ; 0169; 0414; 0618; 0649</NoWarn>
@@ -84,7 +84,7 @@
<PlatformTarget>x64</PlatformTarget>
<LangVersion>8.0</LangVersion>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<CodeAnalysisRuleSet Condition="Exists('MinimumRecommendedRules.ruleset')">MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
@@ -96,7 +96,7 @@
<PlatformTarget>x86</PlatformTarget>
<LangVersion>8.0</LangVersion>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<CodeAnalysisRuleSet Condition="Exists('MinimumRecommendedRules.ruleset')">MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
@@ -108,7 +108,7 @@
<PlatformTarget>x86</PlatformTarget>
<LangVersion>8.0</LangVersion>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<CodeAnalysisRuleSet Condition="Exists('MinimumRecommendedRules.ruleset')">MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -815,12 +815,40 @@ systeminfo.exe
Write-Host ""
if ($TimeStamp) { TimeElapsed }
Write-Host -ForegroundColor Blue "=========|| WINDOWS HOTFIXES"
Write-Host "=| Check if windows is vulnerable with Watson https://github.com/rasta-mouse/Watson" -ForegroundColor Yellow
Write-Host "=| Check missing patches with WES-NG https://github.com/bitsadmin/wesng" -ForegroundColor Yellow
Write-Host "Possible exploits (https://github.com/codingo/OSCP-2/blob/master/Windows/WinPrivCheck.bat)" -ForegroundColor Yellow
$Hotfix = Get-HotFix | Sort-Object -Descending -Property InstalledOn -ErrorAction SilentlyContinue | Select-Object HotfixID, Description, InstalledBy, InstalledOn
$Hotfix | Format-Table -AutoSize
# PrintNightmare PointAndPrint policy checks
Write-Host ""
if ($TimeStamp) { TimeElapsed }
Write-Host -ForegroundColor Blue "=========|| PRINTNIGHTMARE POINTANDPRINT POLICY"
$pnKey = "HKLM:\Software\Policies\Microsoft\Windows NT\Printers\PointAndPrint"
if (Test-Path $pnKey) {
$pn = Get-ItemProperty -Path $pnKey -ErrorAction SilentlyContinue
$restrict = $pn.RestrictDriverInstallationToAdministrators
$noWarn = $pn.NoWarningNoElevationOnInstall
$updatePrompt = $pn.UpdatePromptSettings
Write-Host "RestrictDriverInstallationToAdministrators: $restrict"
Write-Host "NoWarningNoElevationOnInstall: $noWarn"
Write-Host "UpdatePromptSettings: $updatePrompt"
$hasAllValues = ($null -ne $restrict) -and ($null -ne $noWarn) -and ($null -ne $updatePrompt)
if (-not $hasAllValues) {
Write-Host "PointAndPrint policy values are missing or not configured" -ForegroundColor Gray
} elseif (($restrict -eq 0) -and ($noWarn -eq 1) -and ($updatePrompt -eq 2)) {
Write-Host "Potentially vulnerable to PrintNightmare misconfiguration" -ForegroundColor Red
} else {
Write-Host "PointAndPrint policy is not in the known risky configuration" -ForegroundColor Green
}
} else {
Write-Host "PointAndPrint policy key not found" -ForegroundColor Gray
}
#Show all unique updates installed
Write-Host ""
if ($TimeStamp) { TimeElapsed }