diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..b44c224
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,149 @@
+name: PSNetScanners Workflow
+on:
+ push:
+ branches:
+ - main
+
+ pull_request:
+ branches:
+ - main
+
+ release:
+ types:
+ - published
+
+env:
+ DOTNET_CLI_TELEMETRY_OPTOUT: 1
+ POWERSHELL_TELEMETRY_OPTOUT: 1
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
+ DOTNET_NOLOGO: true
+ BUILD_CONFIGURATION: ${{ fromJSON('["Debug", "Release"]')[startsWith(github.ref, 'refs/tags/v')] }}
+
+jobs:
+ build:
+ name: build
+ runs-on: windows-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Build module - Debug
+ shell: pwsh
+ run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Build
+ if: ${{ env.BUILD_CONFIGURATION == 'Debug' }}
+
+ - name: Build module - Publish
+ shell: pwsh
+ run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Build
+ if: ${{ env.BUILD_CONFIGURATION == 'Release' }}
+
+ - name: Capture PowerShell Module
+ uses: actions/upload-artifact@v4
+ with:
+ name: PSModule
+ path: output/*.nupkg
+
+ test:
+ name: test
+ needs:
+ - build
+ runs-on: ${{ matrix.info.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ info:
+ - name: PS-5.1
+ psversion: '5.1'
+ os: windows-latest
+ - name: PS-7_Windows
+ psversion: '7'
+ os: windows-latest
+ - name: PS-7_Linux
+ psversion: '7'
+ os: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Restore Built PowerShell Module
+ uses: actions/download-artifact@v4
+ with:
+ name: PSModule
+ path: output
+
+ - name: Install Built PowerShell Module
+ shell: pwsh
+ run: |
+ $manifestItem = Get-Item ([IO.Path]::Combine('module', '*.psd1'))
+ $moduleName = $manifestItem.BaseName
+ $manifest = Test-ModuleManifest -Path $manifestItem.FullName -ErrorAction SilentlyContinue -WarningAction Ignore
+
+ $destPath = [IO.Path]::Combine('output', $moduleName, $manifest.Version)
+ if (-not (Test-Path -LiteralPath $destPath)) {
+ New-Item -Path $destPath -ItemType Directory | Out-Null
+ }
+
+ Get-ChildItem output/*.nupkg | Rename-Item -NewName { $_.Name -replace '.nupkg', '.zip' }
+ Expand-Archive -Path output/*.zip -DestinationPath $destPath -Force -ErrorAction Stop
+
+ - name: Run Tests - PowerShell 5.1
+ if: ${{ matrix.info.psversion == '5.1' }}
+ shell: powershell
+ run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Test
+
+ - name: setcap
+ if: ${{ matrix.info.name == 'PS-7_Linux' }}
+ shell: pwsh
+ run: sudo setcap cap_net_raw=eip /opt/microsoft/powershell/7/pwsh
+
+ - name: Run Tests - PowerShell 7
+ if: ${{ matrix.info.psversion != '5.1' }}
+ shell: pwsh
+ run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Test
+
+ - name: Upload Test Results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: Unit Test Results (${{ matrix.info.name }})
+ path: ./output/TestResults/Pester.xml
+
+ - name: Upload Coverage Results
+ if: always() && !startsWith(github.ref, 'refs/tags/v')
+ uses: actions/upload-artifact@v4
+ with:
+ name: Coverage Results (${{ matrix.info.name }})
+ path: ./output/TestResults/Coverage.xml
+
+ - name: Upload Coverage to codecov
+ if: always() && !startsWith(github.ref, 'refs/tags/v')
+ uses: codecov/codecov-action@v4
+ with:
+ files: ./output/TestResults/Coverage.xml
+ flags: ${{ matrix.info.name }}
+ token: ${{ secrets.CODECOV_TOKEN }}
+
+ publish:
+ name: publish
+ if: startsWith(github.ref, 'refs/tags/v')
+ needs:
+ - build
+ - test
+ runs-on: windows-latest
+ steps:
+ - name: Restore Built PowerShell Module
+ uses: actions/download-artifact@v4
+ with:
+ name: PSModule
+ path: ./
+
+ - name: Publish to Gallery
+ if: github.event_name == 'release'
+ shell: pwsh
+ run: >-
+ dotnet nuget push '*.nupkg'
+ --api-key $env:PSGALLERY_TOKEN
+ --source 'https://www.powershellgallery.com/api/v2/package'
+ --no-symbols
+ env:
+ PSGALLERY_TOKEN: ${{ secrets.PSGALLERY_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..480bc3e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,277 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+benchmarks/
+BenchmarkDotNet.Artifacts/
+tools/dotnet
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+*.zip
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+#*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignoreable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+### Custom entries ###
+output/
+tools/Modules
+test.settings.json
+tests/integration/.vagrant
+tests/integration/cert_setup
+tools/ProjectBuilder/output
+tools/ProjectBuilder/bin
+tools/ProjectBuilder/debug
+tools/ProjectBuilder/obj
diff --git a/.markdownlint.json b/.markdownlint.json
new file mode 100644
index 0000000..4e3f7c3
--- /dev/null
+++ b/.markdownlint.json
@@ -0,0 +1,8 @@
+{
+ "default": true,
+ "no-hard-tabs": true,
+ "no-duplicate-heading": false,
+ "line-length": false,
+ "no-inline-html": false,
+ "ul-indent": false
+}
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..4dafc5c
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,9 @@
+{
+ // See http://go.microsoft.com/fwlink/?LinkId=827846
+ // for the documentation about the extensions.json format
+ "recommendations": [
+ "formulahendry.dotnet-test-explorer",
+ "ms-dotnettools.csharp",
+ "ms-vscode.powershell",
+ ],
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..ff1143b
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,45 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "PowerShell launch",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "pwsh",
+ "args": [
+ "-NoExit",
+ "-NoProfile",
+ "-Command",
+ ". ./tools/prompt.ps1;",
+ "Import-Module ./output/PSNetScanners"
+ ],
+ "cwd": "${workspaceFolder}",
+ "stopAtEntry": false,
+ "console": "externalTerminal",
+ },
+ {
+ "name": "PowerShell Launch Current File",
+ "type": "PowerShell",
+ "request": "launch",
+ "script": "${file}",
+ "cwd": "${workspaceFolder}"
+ },
+ {
+ "name": ".NET FullCLR Attach",
+ "type": "clr",
+ "request": "attach",
+ "processId": "${command:pickProcess}",
+ "justMyCode": true,
+ },
+ {
+ "name": ".NET CoreCLR Attach",
+ "type": "coreclr",
+ "request": "attach",
+ "processId": "${command:pickProcess}",
+ "justMyCode": true,
+ },
+ ],
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..c7f8f57
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,56 @@
+{
+ "cSpell.enableFiletypes": [
+ "!powershell"
+ ],
+ //-------- Files configuration --------
+ // When enabled, will trim trailing whitespace when you save a file.
+ "files.trimTrailingWhitespace": true,
+ // When enabled, insert a final new line at the end of the file when saving it.
+ "files.insertFinalNewline": true,
+ "search.exclude": {
+ "Release": true,
+ "tools/ResGen": true,
+ "tools/dotnet": true,
+ },
+ "json.schemas": [
+ {
+ "fileMatch": [
+ "/test.settings.json"
+ ],
+ "url": "./tests/settings.schema.json"
+ }
+ ],
+ "editor.rulers": [
+ 120,
+ ],
+ //-------- PowerShell configuration --------
+ // Binary modules cannot be unloaded so running in separate processes solves that problem
+ //"powershell.debugging.createTemporaryIntegratedConsole": true,
+ // We use Pester v5 so we don't need the legacy code lens
+ "powershell.pester.useLegacyCodeLens": false,
+ "cSpell.words": [
+ "pwsh"
+ ],
+ "xml.fileAssociations": [
+ {
+ "systemId": "https://raw.githubusercontent.com/PowerShell/PowerShell/master/src/Schemas/Format.xsd",
+ "pattern": "**/*.Format.ps1xml"
+ },
+ {
+ "systemId": "https://raw.githubusercontent.com/PowerShell/PowerShell/master/src/Schemas/Types.xsd",
+ "pattern": "**/*.Types.ps1xml"
+ }
+ ],
+ "[powershell]": {
+ "files.encoding": "utf8bom",
+ "editor.tabSize": 4,
+ "editor.detectIndentation": false,
+ "editor.autoIndent": "full"
+ },
+ "[csharp]": {
+ "editor.maxTokenizationLineLength": 2500,
+ "editor.tabSize": 4,
+ "editor.detectIndentation": false,
+ "editor.autoIndent": "full",
+ }
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..bbde0bc
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,49 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "pwsh",
+ "type": "shell",
+ "args": [
+ "-File",
+ "${workspaceFolder}/build.ps1"
+ ],
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "update docs",
+ "command": "pwsh",
+ "type": "shell",
+ "args": [
+ "-Command",
+ "Import-Module ${workspaceFolder}/output/PSNetScanners; Import-Module ${workspaceFolder}/tools/Modules/platyPS; Update-MarkdownHelpModule ${workspaceFolder}/docs/en-US -AlphabeticParamsOrder -RefreshModulePage -UpdateInputOutput"
+ ],
+ "problemMatcher": [],
+ "dependsOn": [
+ "build"
+ ]
+ },
+ {
+ "label": "test",
+ "command": "pwsh",
+ "type": "shell",
+ "args": [
+ "-File",
+ "${workspaceFolder}/build.ps1",
+ "-Task",
+ "Test"
+ ],
+ "problemMatcher": [],
+ "dependsOn": [
+ "build"
+ ]
+ }
+ ]
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..dd35bd7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Santiago Squarzon
+
+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:
+
+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.
diff --git a/Network-IPScanner.ps1 b/Network-IPScanner.ps1
deleted file mode 100644
index aa6c036..0000000
--- a/Network-IPScanner.ps1
+++ /dev/null
@@ -1,117 +0,0 @@
-$ErrorActionPreference = 'Stop'
-
-# Define List of IPs / Hosts to Ping
-# $list = 'host1', 'host2', 'host3'
-$list = 1..254 | ForEach-Object {
- "192.168.0.$_"
-}
-
-function Test-ICMPConnection {
- [cmdletbinding(DefaultParameterSetName = 'DefaultParams')]
- param(
- [parameter(Mandatory, ValueFromPipeline, Position = 0)]
- [string[]] $Address,
- [parameter(ParameterSetName = 'DefaultParams', Position = 1)]
- [int] $Count = 4,
- [parameter(ParameterSetName = 'DefaultParams', Position = 2)]
- [int] $TimeOut = 1000,
- [parameter(ParameterSetName = 'Quiet', Position = 1)]
- [switch] $Quiet,
- [parameter(ParameterSetName = 'DefaultParams', Position = 3)]
- [string] $Buffer = 'aaaaaaaaaa'
- )
-
- begin {
- $ping = [System.Net.NetworkInformation.Ping]::new()
- $options = [System.Net.NetworkInformation.PingOptions]::new()
- $data = [System.Text.Encoding]::Unicode.GetBytes($Buffer)
- $hostname = $env:COMPUTERNAME
- $options.DontFragment = $true
- }
- process {
- foreach($i in $Address) {
- if($Quiet.IsPresent) {
- return [bool]$ping.Send($i, $TimeOut, $data, $options).RoundtripTime
- }
-
- $resolver = try {
- [System.Net.Dns]::GetHostEntry($i).HostName
- }
- catch { '*' }
-
- 1..$Count | ForEach-Object {
- $response = $ping.Send($i, $TimeOut, $data, $options)
- $latency = (
- '*', [string]::Format('{0} ms', $response.RoundtripTime)
- )[$response.Status -eq 'Success']
-
- [pscustomobject]@{
- Ping = $_
- Source = $hostname
- Address = $response.Address
- Destination = $resolver
- Latency = $latency
- Status = $response.Status
- }
- }
- }
- }
- end {
- $ping.ForEach('Dispose')
- }
-}
-
-# Store the function Definition
-$funcDef = ${function:Test-ICMPConnection}.ToString()
-$scriptBlock = {
- param([string] $ip, [string] $funcDef)
-
- # Load the function in this Scope
- ${function:Test-ICMPConnection} = $funcDef
-
- # Define which arguments will be used for Pinger
- # Default Values are:
- #
- # -Count 1
- # -TimeOut 1000 (Milliseconds)
- # -Buffer 'aaaaaaaaaa' (10 bytes)
- # -Quiet:$false
-
- Test-ICMPConnection -Address $ip -Count 1
-}
-
-& {
- try {
- # Change this value for tweaking
- $Threshold = 100
- $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $Threshold)
- $RunspacePool.Open()
-
- $runspaces = foreach($ip in $list) {
- $params = @{
- IP = $ip
- funcDef = $funcDef
- }
-
- $psinstance = [powershell]::Create().AddScript($scriptBlock).AddParameters($params)
- $psinstance.RunspacePool = $RunspacePool
-
- [pscustomobject]@{
- Instance = $psinstance
- Handle = $psinstance.BeginInvoke()
- }
- }
-
- foreach($r in $runspaces) {
- $r.Instance.EndInvoke($r.Handle)
- $r.Instance.foreach('Dispose')
- }
- }
- catch {
- Write-Warning $_.Exception.Message
- }
- finally {
- $runspaces.foreach('Clear')
- $RunspacePool.foreach('Dispose')
- }
-} | Format-Table -AutoSize
diff --git a/Network-TCPScanner.ps1 b/Network-TCPScanner.ps1
deleted file mode 100644
index d226b3d..0000000
--- a/Network-TCPScanner.ps1
+++ /dev/null
@@ -1,109 +0,0 @@
-$Threshold = 100 # => Number of threads running
-[int[]] $PortsToScan = 80, 443, 125, 8080
-[string[]] $HostsToScan = 'google.com', 'cisco.com', 'amazon.com'
-
-function Test-TCPConnectionAsync {
- [cmdletbinding()]
- param(
- [parameter(Mandatory, Valuefrompipeline)]
- [string[]] $Target,
-
- [parameter(Mandatory, Position = 1)]
- [ValidateRange(1, 65535)]
- [int[]] $Port,
-
- # 1 second minimum, reasonable for TCP connection
- [parameter(Position = 2)]
- [ValidateRange(1000, [int]::MaxValue)]
- [int] $TimeOut = 1200
- )
-
- begin {
- $timer = [System.Diagnostics.Stopwatch]::StartNew()
- $tasks = [System.Collections.Generic.List[System.Collections.Specialized.OrderedDictionary]]::new()
- }
- process {
- foreach($t in $Target) {
- foreach($i in $Port) {
- if($tasks.Count -eq 62) {
- Wait-Tasks
- }
-
- $tcp = [System.Net.Sockets.TcpClient]::new()
- $tasks.Add([ordered]@{
- Instance = $tcp
- Task = $tcp.ConnectAsync($t, $i)
- Output = [ordered]@{
- Source = $env:COMPUTERNAME
- Destination = $t
- Port = $i
- }
- })
- }
- }
- }
- end {
- do {
- $id = [System.Threading.Tasks.Task]::WaitAny($tasks.Task, 200)
- if($id -eq -1) {
- continue
- }
- $instance, $task, $output = $tasks[$id][$tasks[$id].PSBase.Keys]
- $output['Success'] = $task.Status -eq [System.Threading.Tasks.TaskStatus]::RanToCompletion
- $instance.ForEach('Dispose') # Avoid any throws here
- $tasks.RemoveAt($id)
- [pscustomobject] $output
- } while($tasks -and $timer.ElapsedMilliseconds -le $timeout)
-
- foreach($t in $tasks) {
- $instance, $task, $output = $t[$t.PSBase.Keys]
- $output['Success'] = $task.Status -eq [System.Threading.Tasks.TaskStatus]::RanToCompletion
- $instance.ForEach('Dispose') # Avoid any throws here
- [pscustomobject] $output
- }
- }
-}
-
-& {
- try {
- # Store function definition
- $funcDef = ${function:Test-TCPConnectionAsync}.ToString()
- $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $Threshold)
- $RunspacePool.Open()
- $scriptBlock = {
- param([string] $hostname, [int[]] $ports, [string] $func)
-
- # Load the function in this Scope
- ${function:Test-TCPConnectionAsync} = $func
- Test-TCPConnectionAsync -Target $hostname -Port $ports -TimeOut 2000
- }
-
- $runspaces = foreach($i in $HostsToScan) {
- $params = @{
- hostname = $i
- ports = $PortsToScan
- func = $funcDef
- }
-
- $psinstance = [powershell]::Create().AddScript($scriptBlock).AddParameters($params)
- $psinstance.RunspacePool = $RunspacePool
-
- [pscustomobject]@{
- Instance = $psinstance
- Handle = $psinstance.BeginInvoke()
- }
- }
-
- foreach($r in $runspaces) {
- $r.Instance.EndInvoke($r.Handle)
- $r.Instance.foreach('Dispose')
- }
- }
- catch {
- Write-Warning $_.Exception.Message
- }
- finally {
- $runspaces.foreach('Clear')
- $RunspacePool.foreach('Dispose')
- }
-}
diff --git a/README.md b/README.md
index 96dee1c..c9f58b7 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,17 @@
-# PowerShell Network Scanners
+
PSNetScanners
+
+PowerShell ICMP and TCP async scanners
+
+
+[![build](https://github.com/santisq/PSNetScanners/actions/workflows/ci.yml/badge.svg)](https://github.com/santisq/PSNetScanners/actions/workflows/ci.yml)
+[![codecov](https://codecov.io/gh/santisq/PSNetScanners/branch/main/graph/badge.svg?token=b51IOhpLfQ)](https://codecov.io/gh/santisq/PSNetScanners)
+[![PowerShell Gallery](https://img.shields.io/powershellgallery/v/PSNetScanners?label=gallery)](https://www.powershellgallery.com/packages/PSNetScanners)
+[![LICENSE](https://img.shields.io/github/license/santisq/PSNetScanners)](https://github.com/santisq/PSNetScanners/blob/main/LICENSE)
+
+
## DESCRIPTION
+
Two PowerShell scripts designed to scan Network IP Ranges or hostname using [`Runspace`](https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.runspace?view=powershellsdk-7.0.0) for faster execution. And two standalone functions using async techniques for ICMP and TCP scanning.
| Name | Description |
diff --git a/Test-ICMPConnectionAsync.ps1 b/Test-ICMPConnectionAsync.ps1
deleted file mode 100644
index 5c12878..0000000
--- a/Test-ICMPConnectionAsync.ps1
+++ /dev/null
@@ -1,118 +0,0 @@
-using namespace System.Threading.Tasks
-using namespace System.Collections.Generic
-using namespace System.Net.NetworkInformation
-using namespace System.Net
-using namespace System.Diagnostics
-
-function Test-ICMPConnectionAsync {
- [cmdletbinding()]
- param(
- [parameter(Mandatory, ValueFromPipeline, Position = 0)]
- [string[]] $Address,
-
- [parameter(Position = 1)]
- [ValidateRange(1, [int]::MaxValue)]
- [int] $TimeOut = 10, # In seconds!
-
- [parameter(Position = 2)]
- [ValidateRange(1, [int]::MaxValue)]
- [int] $BufferSize = 32
- )
-
- begin {
- $tasks = [List[hashtable]]::new()
- $timer = [Stopwatch]::StartNew()
- $data = [byte[]] [char[]] 'A' * $BufferSize
- $options = [PingOptions]::new()
- $TimeOut = [timespan]::FromSeconds($TimeOut).TotalMilliseconds
- $options.DontFragment = $true
-
- $outObject = {
- [pscustomobject]@{
- Source = $env:COMPUTERNAME
- Target = $target
- Address = $response.Address.IPAddressToString
- DnsName = $dnsresol
- Latency = $latency
- Status = $response.Status
- }
- }
- }
- process {
- foreach($addr in $Address) {
- $ping = [Ping]::new()
- $tasks.Add(@{
- Target = $addr
- Instance = $ping
- PingTask = $ping.SendPingAsync($addr, $TimeOut, $data, $options)
- DnsTask = [Dns]::GetHostEntryAsync($addr)
- })
- }
- }
- end {
- while($tasks -and $timer.ElapsedMilliseconds -le $timeout) {
- $id = [Task]::WaitAny($tasks.PingTask, 200)
- if($id -eq -1) {
- continue
- }
- $target, $instance, $ping, $dns = $tasks[$id]['Target', 'Instance', 'PingTask', 'DnsTask']
-
- try {
- $response = $ping.GetAwaiter().GetResult()
- $latency = [string]::Format('{0} ms', $response.RoundtripTime)
- $ping.Dispose()
- }
- catch {
- $latency = '*'
- $response = @{
- Address = @{ IPAddressToString = '*' }
- Status = $_.Exception.InnerException.InnerException.Message
- }
- }
-
- try {
- $dnsresol = $dns.GetAwaiter().GetResult().HostName
- $dns.Dispose()
- }
- catch {
- $dnsresol = $_.Exception.InnerException.Message
- }
-
- & $outObject
- $instance.Dispose()
- $tasks.RemoveAt($id)
- }
-
- foreach($task in $tasks) {
- $target, $instance, $ping, $dns = $task['Target', 'Instance', 'PingTask', 'DnsTask']
-
- try {
- $response = $ping.GetAwaiter().GetResult()
- $latency = [string]::Format('{0} ms', $response.RoundtripTime)
- $ping.Dispose()
- }
- catch {
- $latency = '*'
- $response = @{
- Address = '*'
- Status = $_.Exception.InnerException.InnerException.Message
- }
- }
-
- try {
- $dnsresol = $dns.GetAwaiter().GetResult().HostName
- $dns.Dispose()
- }
- catch {
- $dnsresol = $_.Exception.InnerException.Message
- }
-
- & $outObject
- $instance.Dispose()
- }
- }
-}
-
-'amazon.com', 'google.com', 'facebook.com' |
- Test-ICMPConnectionAsync -TimeOut 5 |
- Format-Table -AutoSize
\ No newline at end of file
diff --git a/Test-TCPConnectionAsync.ps1 b/Test-TCPConnectionAsync.ps1
deleted file mode 100644
index 40744de..0000000
--- a/Test-TCPConnectionAsync.ps1
+++ /dev/null
@@ -1,113 +0,0 @@
-using namespace System.Diagnostics
-using namespace System.Collections.Generic
-using namespace System.Net.Sockets
-using namespace System.Threading.Tasks
-
-<#
-.DESCRIPTION
-PowerShell Function that leverages the ConnectAsync(...) Method from the TcpClient Class to send the async TCP connection requests.
-
-.EXAMPLE
-'google.com', 'cisco.com', 'amazon.com' | Test-TCPConnectionAsync 80, 443, 8080, 389, 636
-
-.EXAMPLE
-@'
-Target,Port
-google.com,80
-google.com,443
-google.com,8080
-google.com,389
-google.com,636
-cisco.com,80
-cisco.com,443
-cisco.com,8080
-cisco.com,389
-cisco.com,636
-amazon.com,80
-amazon.com,443
-amazon.com,8080
-amazon.com,389
-amazon.com,636
-'@ | ConvertFrom-Csv | Test-TCPConnectionAsync
-#>
-
-function Test-TCPConnectionAsync {
- [cmdletbinding()]
- param(
- [parameter(Mandatory, Valuefrompipeline, ValueFromPipelineByPropertyName)]
- [alias('ComputerName', 'HostName', 'Host', 'Server')]
- [string[]] $Target,
-
- [parameter(Mandatory, ValueFromPipelineByPropertyName)]
- [ValidateRange(1, 65535)]
- [int[]] $Port,
-
- [parameter()]
- [ValidateRange(5, [int]::MaxValue)]
- [int] $TimeOut = 5, # In seconds!
-
- [parameter()]
- [switch] $IPv6
- )
-
- begin {
- $timer = [Stopwatch]::StartNew()
- $queue = [List[hashtable]]::new()
- $TimeOut = [timespan]::FromSeconds($TimeOut).TotalMilliseconds
- if($IPv6.IsPresent) {
- $newTcp = { [TCPClient]::new([AddressFamily]::InterNetworkV6) }
- return
- }
- $newTcp = { [TCPClient]::new() }
- }
- process {
- foreach($item in $Target) {
- foreach($i in $Port) {
- $tcp = & $newTcp
- $queue.Add(@{
- Instance = $tcp
- Task = $tcp.ConnectAsync($item, $i)
- Output = [ordered]@{
- Source = $env:COMPUTERNAME
- Destination = $item
- Port = $i
- }
- })
- }
- }
- }
- end {
- while($queue -and $timer.ElapsedMilliseconds -le $timeout) {
- try {
- $id = [Task]::WaitAny($queue.Task, 200)
- if($id -eq -1) {
- continue
- }
- $instance, $task, $output = $queue[$id]['Instance', 'Task', 'Output']
- if($instance) {
- $instance.Dispose()
- }
- $output['Success'] = $task.Status -eq [TaskStatus]::RanToCompletion
- $queue.RemoveAt($id)
- [pscustomobject] $output
- }
- catch {
- $PSCmdlet.WriteError($_)
- }
- }
-
- foreach($item in $queue) {
- try {
- $instance, $task, $output = $item['Instance', 'Task', 'Output']
- $output['Success'] = $task.Status -eq [TaskStatus]::RanToCompletion
- if($instance) {
- $instance.Dispose()
- }
- [pscustomobject] $output
- }
- catch {
- $PSCmdlet.WriteError($_)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/build.ps1 b/build.ps1
new file mode 100644
index 0000000..650b40b
--- /dev/null
+++ b/build.ps1
@@ -0,0 +1,51 @@
+[CmdletBinding()]
+param(
+ [Parameter()]
+ [ValidateSet('Debug', 'Release')]
+ [string] $Configuration = 'Debug',
+
+ [Parameter()]
+ [ValidateSet('Build', 'Test')]
+ [string[]] $Task = 'Build'
+)
+
+$prev = $ErrorActionPreference
+$ErrorActionPreference = 'Stop'
+
+if (-not ('ProjectBuilder.ProjectInfo' -as [type])) {
+ try {
+ $builderPath = [IO.Path]::Combine($PSScriptRoot, 'tools', 'ProjectBuilder')
+ Push-Location $builderPath
+
+ dotnet @(
+ 'publish'
+ '--configuration', 'Release'
+ '-o', 'output'
+ '--framework', 'netstandard2.0'
+ '--verbosity', 'q'
+ '-nologo'
+ )
+
+ if ($LASTEXITCODE) {
+ throw "Failed to compiled 'ProjectBuilder'"
+ }
+
+ $dll = [IO.Path]::Combine($builderPath, 'output', 'ProjectBuilder.dll')
+ Add-Type -Path $dll
+ }
+ finally {
+ Pop-Location
+ }
+}
+
+$projectInfo = [ProjectBuilder.ProjectInfo]::Create($PSScriptRoot, $Configuration)
+$projectInfo.GetRequirements() | Import-Module -DisableNameChecking -Force
+
+$ErrorActionPreference = $prev
+
+$invokeBuildSplat = @{
+ Task = $Task
+ File = Convert-Path ([IO.Path]::Combine($PSScriptRoot, 'tools', 'InvokeBuild.ps1'))
+ ProjectInfo = $projectInfo
+}
+Invoke-Build @invokeBuildSplat
diff --git a/module/PSNetScanners.Format.ps1xml b/module/PSNetScanners.Format.ps1xml
new file mode 100644
index 0000000..0279642
--- /dev/null
+++ b/module/PSNetScanners.Format.ps1xml
@@ -0,0 +1,148 @@
+
+
+
+
+ DnsViewSuccess
+
+ PSNetScanners.DnsSuccess
+
+
+
+
+
+
+ Status
+
+
+ HostName
+
+
+ AddressList
+
+
+ Aliases
+
+
+
+
+
+
+
+ DnsViewFailure
+
+ PSNetScanners.DnsFailure
+
+
+
+
+
+
+ Status
+
+
+ Exception
+
+
+
+
+
+
+
+ PingAsyncView
+
+ PSNetScanners.PingResult
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Right
+
+
+
+
+
+
+
+
+
+
+
+
+ Source
+
+
+ Destination
+
+
+ DisplayAddress
+
+
+ [PSNetScanners.Internal._Format]::GetFormattedLatency($_)
+
+
+ Status
+
+
+ DnsResult
+
+
+
+
+
+
+
+ TcpAsyncView
+
+ PSNetScanners.TcpResult
+
+
+
+
+
+ 17
+
+
+
+ 17
+
+
+
+ 7
+
+
+
+ 15
+
+
+
+
+
+
+ Source
+
+
+ Destination
+
+
+ Port
+
+
+ Status
+
+
+
+
+
+
+
+
diff --git a/module/PSNetScanners.psd1 b/module/PSNetScanners.psd1
new file mode 100644
index 0000000..a4453af
--- /dev/null
+++ b/module/PSNetScanners.psd1
@@ -0,0 +1,144 @@
+#
+# Module manifest for module 'PSNetScanners'
+#
+# Generated by: Santiago Squarzon
+#
+# Generated on: 10/07/2024
+#
+
+@{
+ # Script module or binary module file associated with this manifest.
+ RootModule = 'bin/netstandard2.0/PSNetScanners.dll'
+
+ # Version number of this module.
+ ModuleVersion = '1.0.0'
+
+ # Supported PSEditions
+ # CompatiblePSEditions = @()
+
+ # ID used to uniquely identify this module
+ GUID = 'eec20b34-11d9-4085-af34-519c5503beb2'
+
+ # Author of this module
+ Author = 'Santiago Squarzon'
+
+ # Company or vendor of this module
+ CompanyName = 'Unknown'
+
+ # Copyright statement for this module
+ Copyright = '(c) Santiago Squarzon. All rights reserved.'
+
+ # Description of the functionality provided by this module
+ Description = 'tcp and icmp async scanners for powershell'
+
+ # Minimum version of the PowerShell engine required by this module
+ PowerShellVersion = '5.1'
+
+ # Name of the PowerShell host required by this module
+ # PowerShellHostName = ''
+
+ # Minimum version of the PowerShell host required by this module
+ # PowerShellHostVersion = ''
+
+ # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
+ # DotNetFrameworkVersion = ''
+
+ # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
+ # ClrVersion = ''
+
+ # Processor architecture (None, X86, Amd64) required by this module
+ # ProcessorArchitecture = ''
+
+ # Modules that must be imported into the global environment prior to importing this module
+ # RequiredModules = @()
+
+ # Assemblies that must be loaded prior to importing this module
+ # RequiredAssemblies = @()
+
+ # Script files (.ps1) that are run in the caller's environment prior to importing this module.
+ # ScriptsToProcess = @()
+
+ # Type files (.ps1xml) to be loaded when importing this module
+ # TypesToProcess = @()
+
+ # Format files (.ps1xml) to be loaded when importing this module
+ FormatsToProcess = @('PSNetScanners.Format.ps1xml')
+
+ # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
+ # NestedModules = @()
+
+ # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
+ FunctionsToExport = @()
+
+ # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
+ CmdletsToExport = @(
+ 'Test-PingAsync'
+ 'Test-TcpAsync'
+ )
+
+ # Variables to export from this module
+ VariablesToExport = @()
+
+ # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
+ AliasesToExport = @(
+ 'pingasync'
+ 'tcpasync'
+ )
+
+ # DSC resources to export from this module
+ # DscResourcesToExport = @()
+
+ # List of all modules packaged with this module
+ # ModuleList = @()
+
+ # List of all files packaged with this module
+ # FileList = @()
+
+ # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
+ PrivateData = @{
+ PSData = @{
+ # Tags applied to this module. These help with module discovery in online galleries.
+ Tags = @(
+ 'parallel'
+ 'concurrency'
+ 'runspace'
+ 'parallel-processing'
+ 'powershell'
+ 'multithreading'
+ 'ping'
+ 'async'
+ 'tcp'
+ 'icmp'
+ )
+
+ # A URL to the license for this module.
+ LicenseUri = 'https://github.com/santisq/PSNetScanners/blob/main/LICENSE'
+
+ # A URL to the main website for this project.
+ ProjectUri = 'https://github.com/santisq/PSNetScanners'
+
+ # A URL to an icon representing this module.
+ # IconUri = ''
+
+ # ReleaseNotes of this module
+ # ReleaseNotes = ''
+
+ # Prerelease string of this module
+ # Prerelease = ''
+
+ # Flag to indicate whether the module requires explicit user acceptance for install/update/save
+ # RequireLicenseAcceptance = $false
+
+ # External dependent modules of this module
+ # ExternalModuleDependencies = @()
+
+ } # End of PSData hashtable
+
+ } # End of PrivateData hashtable
+
+ # HelpInfo URI of this module
+ # HelpInfoURI = ''
+
+ # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
+ # DefaultCommandPrefix = ''
+}
diff --git a/src/PSNetScanners/Abstractions/AbstractWorker.cs b/src/PSNetScanners/Abstractions/AbstractWorker.cs
new file mode 100644
index 0000000..310cce1
--- /dev/null
+++ b/src/PSNetScanners/Abstractions/AbstractWorker.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace PSNetScanners.Abstractions;
+
+internal abstract class WorkerBase(int throttle, Cancellation cancellation)
+ : IDisposable
+{
+ protected CancellationToken Token { get => _cancellation.Token; }
+
+ protected abstract Task Worker { get; }
+
+ internal string Source { get; } = Dns.GetHostName();
+
+ protected readonly Cancellation _cancellation = cancellation;
+
+ protected readonly int _throttle = throttle;
+
+ protected bool _disposed;
+
+ protected abstract Task Start();
+
+ internal void Cancel()
+ {
+ _cancellation.Cancel();
+ Wait();
+ }
+
+ internal void Wait() => Worker.GetAwaiter().GetResult();
+
+ protected abstract void Dispose(bool disposing);
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/PSNetScanners/Abstractions/AbstractWorker_T.cs b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs
new file mode 100644
index 0000000..dd4259d
--- /dev/null
+++ b/src/PSNetScanners/Abstractions/AbstractWorker_T.cs
@@ -0,0 +1,43 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace PSNetScanners.Abstractions;
+
+internal abstract class WorkerBase(int throttle, Cancellation cancellation)
+ : WorkerBase(throttle, cancellation)
+{
+ protected virtual BlockingCollection InputQueue { get; } = [];
+
+ protected virtual BlockingCollection