├── .editorconfig ├── .github └── workflows │ └── php.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── psalm.xml ├── src └── Nmap │ ├── Address.php │ ├── Host.php │ ├── Hostname.php │ ├── Nmap.php │ ├── Port.php │ ├── Script.php │ ├── Service.php │ ├── Util │ └── ProcessExecutor.php │ └── XmlOutputParser.php └── tests ├── Nmap └── Tests │ ├── Fixtures │ ├── Validation │ │ ├── nmap.dtd │ │ ├── test_completed_valid.xml │ │ └── test_interrupted_invalid.xml │ ├── test_ping_scan.xml │ ├── test_ping_without_reverse_dns.xml │ ├── test_scan.xml │ ├── test_scan_specifying_ports.xml │ ├── test_scan_with_os_detection.xml │ ├── test_scan_with_service_info.xml │ └── test_scan_with_verbose.xml │ ├── NmapTest.php │ └── TestCase.php └── bootstrap.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-22.04 8 | strategy: 9 | matrix: 10 | php-versions: ['8.1', '8.2', '8.3', '8.4'] 11 | 12 | steps: 13 | 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: ${{ matrix.php-versions }} 20 | tools: composer 21 | 22 | - name: Install xmlstarlet 23 | run: DEBIAN_FRONTEND=noninteractive sudo apt-get -q update && sudo apt-get -yq install -y xmlstarlet nmap --no-install-recommends 24 | 25 | - name: Validate composer.json and composer.lock 26 | run: composer validate 27 | 28 | - name: Install dependencies 29 | run: composer install --prefer-dist --no-progress --no-suggest 30 | 31 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 32 | # Docs: https://getcomposer.org/doc/articles/scripts.md 33 | 34 | - name: Run lint 35 | run: composer lint 36 | 37 | - name: Run psalm 38 | run: composer psalm-github 39 | 40 | - name: Run phpunit 41 | run: composer phpunit 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | bin/phpunit 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 William Durand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This project is maintained fork of the original project: https://github.com/willdurand/nmap** 2 | 3 | Nmap 4 | ==== 5 | 6 | **Nmap** is a PHP wrapper for [Nmap](http://nmap.org/), a free security scanner 7 | for network exploration. 8 | 9 | ![PHP Build](https://github.com/DavidGoodwin/nmap/workflows/PHP%20Build/badge.svg) 10 | 11 | Starting a scan 12 | ----- 13 | 14 | ```php 15 | $hosts = Nmap::create()->scan([ 'example.com' ]); 16 | 17 | $ports = $hosts[0]->getOpenPorts(); 18 | ``` 19 | 20 | You can specify the ports you want to scan: 21 | 22 | ``` php 23 | $nmap = new Nmap(); 24 | 25 | $nmap->scan([ 'example.com' ], [ 21, 22, 80 ]); 26 | ``` 27 | 28 | **OS detection** and **Service Info** are disabled by default, if you want to 29 | enable them, use the `enableOsDetection()` and/or `enableServiceInfo()` methods: 30 | 31 | ``` php 32 | $nmap 33 | ->enableOsDetection() 34 | ->scan([ 'example.com' ]); 35 | 36 | $nmap 37 | ->enableServiceInfo() 38 | ->scan([ 'example.com' ]); 39 | 40 | // Fluent interface! 41 | $nmap 42 | ->enableOsDetection() 43 | ->enableServiceInfo() 44 | ->scan([ 'example.com' ]); 45 | ``` 46 | 47 | Turn the **verbose mode** by using the `enableVerbose()` method: 48 | 49 | ``` php 50 | $nmap 51 | ->enableVerbose() 52 | ->scan([ 'example.com' ]); 53 | ``` 54 | 55 | For some reasons, you might want to disable port scan, that is why **nmap** 56 | provides a `disablePortScan()` method: 57 | 58 | ``` php 59 | $nmap 60 | ->disablePortScan() 61 | ->scan([ 'example.com' ]); 62 | ``` 63 | 64 | You can also disable the reverse DNS resolution with `disableReverseDNS()`: 65 | 66 | ``` php 67 | $nmap 68 | ->disableReverseDNS() 69 | ->scan([ 'example.com' ]); 70 | ``` 71 | 72 | You can define the process timeout (default to 60 seconds) with `setTimeout()`: 73 | 74 | ``` php 75 | $nmap 76 | ->setTimeout(120) 77 | ->scan([ 'example.com' ]); 78 | ``` 79 | 80 | You can run specific scripts with `setScripts()` and get the result with `getScripts()`: 81 | 82 | ``` php 83 | $hosts = $nmap 84 | ->setTimeout(120) 85 | ->scan([ 'example.com' ], [ 443 ]); 86 | 87 | $hosts[0]->setScripts(['ssl-heartbleed']); 88 | $ports = $hosts[0]->getOpenPorts(); 89 | 90 | $ports[0]->getScripts(); 91 | ``` 92 | 93 | Nmap XML output 94 | ------------------------------- 95 | 96 | Parse existing output: 97 | 98 | ``` php 99 | Nmap::parseOutput($xmlFile); 100 | ``` 101 | 102 | or 103 | 104 | ``` php 105 | $parser = new XmlOutputParser($xmlFile); 106 | $parser->parse(); 107 | ``` 108 | 109 | Validation output file using the Nmap DTD. A custom DTD path can be passed to the validate function. 110 | 111 | ```php 112 | $parser = new XmlOutputParser($xmlFile); 113 | $parser->validate(); 114 | ``` 115 | 116 | Installation 117 | ------------ 118 | 119 | The recommended way to install nmap is through [Composer](http://getcomposer.org/): 120 | 121 | For PHP 8.0 and above - 122 | 123 | ``` json 124 | { 125 | "require": { 126 | "palepurple/nmap": "^3.0" 127 | } 128 | } 129 | ``` 130 | 131 | For older versions of PHP, try ^2.0; see also https://github.com/DavidGoodwin/nmap/releases/tag/2.0.1 132 | 133 | License 134 | ------- 135 | 136 | nmap is released under the MIT License. See the bundled LICENSE file for 137 | details. 138 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "palepurple/nmap", 3 | "description": "nmap is a PHP wrapper for Nmap, a free security scanner for network exploration. This is (mostly) a fork of willdurand/nmap with PHP 7 and vimeo/psalm static analysis improvements.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "William DURAND", 8 | "email": "william.durand1@gmail.com" 9 | }, 10 | { 11 | "name": "David Goodwin", 12 | "email": "david+nmap-php@codepoets.co.uk" 13 | } 14 | ], 15 | "require": { 16 | "php": ">= 8.1", 17 | "symfony/process": "~4.0|~5.0|~6.0|~7.0", 18 | "ext-simplexml": "*", 19 | "symfony/filesystem": "~6.0|~7.0" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "7.*|8.*|9.*", 23 | "psalm/phar": "*", 24 | "php-parallel-lint/php-parallel-lint":"*", 25 | "squizlabs/php_codesniffer" : "*", 26 | "mockery/mockery": "*" 27 | }, 28 | "autoload": { 29 | "psr-0": { "Nmap": "src/" } 30 | }, 31 | "scripts": { 32 | "lint": "vendor/bin/parallel-lint src", 33 | "psalm": "vendor/bin/psalm.phar --show-info=false", 34 | "psalm-github": "vendor/bin/psalm.phar --show-info=false --output-format=github", 35 | "phpcs": "vendor/bin/phpcs --standard=PSR2 src", 36 | "phpcbf": "vendor/bin/phpcbf --standard=PSR2 src", 37 | "phpunit": "vendor/bin/phpunit", 38 | "build" : [ 39 | "@lint", 40 | "@phpcs", 41 | "@psalm", 42 | "@phpunit" 43 | ] 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | ./src/Nmap/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Nmap/Address.php: -------------------------------------------------------------------------------- 1 | address = $address; 33 | $this->type = $type; 34 | $this->vendor = $vendor; 35 | } 36 | 37 | public function getAddress(): string 38 | { 39 | return $this->address; 40 | } 41 | 42 | public function getType(): string 43 | { 44 | return $this->type; 45 | } 46 | 47 | public function getVendor(): string 48 | { 49 | return $this->vendor; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Nmap/Host.php: -------------------------------------------------------------------------------- 1 | addresses = $addresses; 43 | $this->state = $state; 44 | $this->hostnames = $hostnames; 45 | $this->ports = $ports; 46 | } 47 | 48 | public function setScripts(array $scripts): void 49 | { 50 | $this->scripts = $scripts; 51 | } 52 | 53 | public function setOs(string $os): void 54 | { 55 | $this->os = $os; 56 | } 57 | 58 | public function setOsAccuracy(int $accuracy): void 59 | { 60 | $this->os_accuracy = $accuracy; 61 | } 62 | 63 | /** 64 | * @return Address[] 65 | */ 66 | public function getAddresses(): array 67 | { 68 | return $this->addresses; 69 | } 70 | 71 | /** 72 | * @return Address[] 73 | */ 74 | private function getAddressesByType(string $type): array 75 | { 76 | return array_filter($this->addresses, function (Address $address) use ($type) { 77 | return $address->getType() === $type; 78 | }); 79 | } 80 | 81 | /** 82 | * @return Address[] 83 | */ 84 | public function getIpv4Addresses(): array 85 | { 86 | return $this->getAddressesByType(Address::TYPE_IPV4); 87 | } 88 | 89 | /** 90 | * @return Address[] 91 | */ 92 | public function getMacAddresses(): array 93 | { 94 | return $this->getAddressesByType(Address::TYPE_MAC); 95 | } 96 | 97 | /** 98 | * @return string 99 | */ 100 | public function getState(): string 101 | { 102 | return $this->state; 103 | } 104 | 105 | public function getOs(): ?string 106 | { 107 | return $this->os; 108 | } 109 | 110 | public function getOsAccuracy(): ?int 111 | { 112 | return $this->os_accuracy; 113 | } 114 | 115 | /** 116 | * @return Hostname[] 117 | */ 118 | public function getHostnames(): array 119 | { 120 | return $this->hostnames; 121 | } 122 | 123 | /** 124 | * @return Script[] 125 | */ 126 | public function getScripts(): array 127 | { 128 | return $this->scripts; 129 | } 130 | 131 | /** 132 | * @return Port[] 133 | */ 134 | public function getPorts(): array 135 | { 136 | return $this->ports; 137 | } 138 | 139 | /** 140 | * @return Port[] 141 | */ 142 | public function getOpenPorts(): array 143 | { 144 | return array_filter($this->ports, function (Port $port): bool { 145 | return $port->isOpen(); 146 | }); 147 | } 148 | 149 | /** 150 | * @return Port[] 151 | */ 152 | public function getClosedPorts(): array 153 | { 154 | return array_filter($this->ports, fn(Port $port) => $port->isClosed()); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Nmap/Hostname.php: -------------------------------------------------------------------------------- 1 | name = $name; 22 | $this->type = $type; 23 | } 24 | 25 | public function getName(): string 26 | { 27 | return $this->name; 28 | } 29 | 30 | public function getType(): string 31 | { 32 | return $this->type; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Nmap/Nmap.php: -------------------------------------------------------------------------------- 1 | executor = $executor ?: new ProcessExecutor(); 56 | $tmp = $outputFile ?? tempnam(sys_get_temp_dir(), 'nmap-scan-output.xml'); 57 | if (!is_string($tmp)) { 58 | throw new \InvalidArgumentException("No outputFile parameter given, or not able to create one with tempnam, fs problem?"); 59 | } 60 | $this->outputFile = $tmp; 61 | $this->executable = $executable; 62 | 63 | // If executor returns anything else than 0 (success exit code), 64 | // throw an exception since $executable is not executable. 65 | if ($this->executor->execute([$this->executable, ' -h']) !== 0) { 66 | throw new InvalidArgumentException(sprintf('`%s` is not executable.', $this->executable)); 67 | } 68 | } 69 | 70 | public function setExtraOptions(array $options): self 71 | { 72 | $this->extraOptions = $options; 73 | return $this; 74 | } 75 | 76 | /** 77 | * @return array - implode with ' ' to get a command line string. 78 | */ 79 | public function buildCommand(array $targets, array $ports = []): array 80 | { 81 | $options = $this->extraOptions; 82 | 83 | if (true === $this->enableOsDetection) { 84 | $options[] = '-O'; 85 | } 86 | 87 | if (true === $this->enableServiceInfo) { 88 | $options[] = '-sV'; 89 | } 90 | 91 | if (true === $this->enableVerbose) { 92 | $options[] = '-v'; 93 | } 94 | 95 | if (true === $this->disablePortScan) { 96 | $options[] = '-sn'; 97 | } elseif (!empty($ports)) { 98 | $options[] = '-p ' . implode(',', $ports); 99 | } 100 | 101 | if ($this->disableReverseDNS) { 102 | $options[] = '-n'; 103 | } 104 | 105 | if ($this->treatHostsAsOnline) { 106 | $options[] = '-Pn'; 107 | } 108 | 109 | $options[] = '-oX'; 110 | $options[] = $this->outputFile; 111 | 112 | return array_merge([$this->executable], $options, $targets); 113 | } 114 | 115 | /** 116 | * @return Host[] 117 | */ 118 | public function scan(array $targets, array $ports = []): array 119 | { 120 | $command = $this->buildCommand($targets, $ports); 121 | 122 | $this->executor->execute($command, $this->timeout); 123 | 124 | if (!file_exists($this->outputFile)) { 125 | throw new RuntimeException(sprintf('Output file not found ("%s")', $this->outputFile)); 126 | } 127 | 128 | return (new XmlOutputParser($this->outputFile))->parse(); 129 | } 130 | 131 | public function enableOsDetection(bool $enable = true): self 132 | { 133 | $this->enableOsDetection = $enable; 134 | 135 | return $this; 136 | } 137 | 138 | public function enableServiceInfo(bool $enable = true): self 139 | { 140 | $this->enableServiceInfo = $enable; 141 | 142 | return $this; 143 | } 144 | 145 | public function enableVerbose(bool $enable = true): self 146 | { 147 | $this->enableVerbose = $enable; 148 | 149 | return $this; 150 | } 151 | 152 | public function disablePortScan(bool $disable = true): self 153 | { 154 | $this->disablePortScan = $disable; 155 | 156 | return $this; 157 | } 158 | 159 | public function disableReverseDNS(bool $disable = true): self 160 | { 161 | $this->disableReverseDNS = $disable; 162 | 163 | return $this; 164 | } 165 | 166 | public function treatHostsAsOnline(bool $disable = true): self 167 | { 168 | $this->treatHostsAsOnline = $disable; 169 | 170 | return $this; 171 | } 172 | 173 | public function setTimeout(int $timeout): self 174 | { 175 | $this->timeout = $timeout; 176 | 177 | return $this; 178 | } 179 | 180 | /** 181 | * @return \Nmap\Host[] 182 | */ 183 | public static function parseOutput(string $xmlFile) 184 | { 185 | return (new XmlOutputParser($xmlFile))->parse(); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Nmap/Port.php: -------------------------------------------------------------------------------- 1 | number = $number; 37 | $this->protocol = $protocol; 38 | $this->state = $state; 39 | $this->service = $service; 40 | } 41 | 42 | public function setScripts(array $scripts): void 43 | { 44 | $this->scripts = $scripts; 45 | } 46 | 47 | public function getNumber(): int 48 | { 49 | return $this->number; 50 | } 51 | 52 | public function getProtocol(): string 53 | { 54 | return $this->protocol; 55 | } 56 | 57 | /** 58 | * @return string one of self::STATE_OPEN or STATE_CLOSED 59 | */ 60 | public function getState(): string 61 | { 62 | return $this->state; 63 | } 64 | 65 | public function isOpen(): bool 66 | { 67 | return self::STATE_OPEN === $this->state; 68 | } 69 | 70 | public function isClosed(): bool 71 | { 72 | return self::STATE_CLOSED === $this->state; 73 | } 74 | 75 | public function getService(): Service 76 | { 77 | return $this->service; 78 | } 79 | 80 | /** 81 | * @return Script[] 82 | */ 83 | public function getScripts(): array 84 | { 85 | return $this->scripts; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Nmap/Script.php: -------------------------------------------------------------------------------- 1 | id = $id; 17 | $this->output = $output; 18 | $this->elems = $elems; 19 | } 20 | 21 | public function getId(): string 22 | { 23 | return $this->id; 24 | } 25 | 26 | public function getOutput(): string 27 | { 28 | return $this->output; 29 | } 30 | 31 | public function getElems(): array 32 | { 33 | return $this->elems; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Nmap/Service.php: -------------------------------------------------------------------------------- 1 | name = $name; 25 | $this->product = $product; 26 | $this->version = $version; 27 | } 28 | 29 | public function getName(): ?string 30 | { 31 | return $this->name; 32 | } 33 | 34 | public function getProduct(): ?string 35 | { 36 | return $this->product; 37 | } 38 | 39 | public function getVersion(): ?string 40 | { 41 | return $this->version; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Nmap/Util/ProcessExecutor.php: -------------------------------------------------------------------------------- 1 | find($command[0]); 24 | if (!is_string($executable) || empty($executable)) { 25 | throw new InvalidArgumentException(sprintf('Unable to find executable `%s`', $command[0])); 26 | } 27 | $command[0] = $executable; 28 | 29 | $process = new Process($command, null, null, null, $timeout); 30 | $process->run(); 31 | 32 | if (!$process->isSuccessful()) { 33 | throw new RuntimeException(sprintf( 34 | 'Failed to execute "%s"'.PHP_EOL.'%s', 35 | implode(' ', $command), 36 | $process->getErrorOutput() 37 | )); 38 | } 39 | 40 | return (int) $process->getExitCode(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Nmap/XmlOutputParser.php: -------------------------------------------------------------------------------- 1 | '; 20 | 21 | protected Filesystem $filesystem; 22 | 23 | protected string $xmlFile; 24 | 25 | public function __construct(string $xmlFile) 26 | { 27 | $filesystem = new Filesystem(); 28 | if (!$filesystem->exists($xmlFile)) { 29 | throw new FileNotFoundException($xmlFile); 30 | } 31 | $this->filesystem = $filesystem; 32 | $this->xmlFile = $xmlFile; 33 | } 34 | 35 | public function getXmlFile(): string 36 | { 37 | return $this->xmlFile; 38 | } 39 | 40 | /** 41 | * Check if DTD is present under default install path. If not attempt to download 42 | * the most recent. 43 | * 44 | * todo: refresh latast dtd and change path to __DIR__ . '/../tmp/nmap.dtd'; 45 | * 46 | * @link https://nmap.org/book/app-nmap-dtd.html 47 | */ 48 | private function getDtdFiles(?string $dtdPath = ''): array 49 | { 50 | $dtds = []; 51 | 52 | if (!is_string($dtdPath) || empty($dtdPath)) { 53 | $dtdPath = self::$defaultDtd; 54 | } 55 | 56 | if ($this->filesystem->exists($dtdPath)) { 57 | $dtds[] = $dtdPath; 58 | } 59 | 60 | // Download latest official Nmap DTD 61 | $dtdPath = '/tmp/nmap.dtd'; 62 | if (!$this->filesystem->exists($dtdPath)) { 63 | $str = file_get_contents('https://svn.nmap.org/nmap/docs/nmap.dtd'); 64 | 65 | if (!is_string($str)) { 66 | throw new \InvalidArgumentException("Could not download https://svn.nmap.org/nmap/docs/nmap.dtd?"); 67 | } 68 | $this->filesystem->dumpFile($dtdPath, $str); 69 | } 70 | $dtds[] = $dtdPath; 71 | 72 | return $dtds; 73 | } 74 | 75 | /** 76 | * DTD Validation is done using xmlstarlet because this is very cumbersome using standard PHP. 77 | * Besides validation, xmlstarlet contains other useful features for future development. 78 | * 79 | * @link http://xmlstar.sourceforge.net/ 80 | */ 81 | private function getXmlstarlet(): string 82 | { 83 | $xmlstarlet = (new ExecutableFinder)->find('xmlstarlet'); 84 | 85 | if (!is_string($xmlstarlet) || empty($xmlstarlet)) { 86 | throw new RuntimeException('xmlstarlet could not be found'); 87 | } 88 | return $xmlstarlet; 89 | } 90 | 91 | /** 92 | * Validate output file using DTD as recommended by Nmap. 93 | * 94 | * Validation can fail if a much newer or older DTD is used than the Nmap version that created 95 | * the output. Start validation with installed version, if fails or missing fetch latest DTD. 96 | * 97 | * @return bool|string true if valid, an error string if invalid 98 | * @link http://xmlstar.sourceforge.net/doc/UG/ch04s04.html 99 | * @todo: optimize this to find DTD that is associated with nmap version. 100 | */ 101 | public function validate(?string $dtdPath = null): bool|string 102 | { 103 | $dtdFiles = $this->getDtdFiles($dtdPath); 104 | $len = count($dtdFiles); 105 | foreach ($dtdFiles as $index => $dtdFile) { 106 | $process = new Process([ 107 | $this->getXmlstarlet(), 108 | 'val', 109 | '-e', 110 | '--dtd', $dtdFile, 111 | $this->xmlFile 112 | ]); 113 | $process->setTimeout(900); 114 | $process->run(); 115 | 116 | $error = $process->getErrorOutput(); 117 | if (empty($error)) { 118 | return true; 119 | } 120 | if ($index == $len - 1) { 121 | return $error; 122 | } 123 | } 124 | return 'Error'; 125 | } 126 | 127 | /** 128 | * Nmap scans that have been cancelled/failed miss the XML closing element. 129 | * 130 | * This method is currently a simple attempt to 'fix' the XML by appending the XML closing tag. A copy of 131 | * the original file is used, a new file is written to the directory 'recovered'. The xmlFile to import 132 | * will be set to the recovered file. Note: This does not result in a XML that passes DTD validation. 133 | * However, the input can be be parsed. 134 | * 135 | * todo: perhaps 'xmlstarlet do --recover' could be used for recovery (also more for more complex cases). 136 | * However, 'xmlstartlet' appears to adjust the encoding, it is unclear what the impact of this is. 137 | */ 138 | public function attemptFixInvalidFile(): bool 139 | { 140 | $str = file_get_contents($this->xmlFile); 141 | 142 | if (!is_string($str)) { 143 | throw new \InvalidArgumentException("Could not get contents of file: {$this->xmlFile}"); 144 | } 145 | 146 | 147 | if (preg_match('%' . preg_quote(XmlOutputParser::$xmlCloseTag) . '\s+$%m', $str)) { 148 | return false; 149 | } 150 | 151 | $pathinfo = pathinfo($this->xmlFile); 152 | $recoveryDir = $pathinfo['dirname'] . '/recovered'; 153 | if (!$this->filesystem->exists($recoveryDir)) { 154 | $this->filesystem->mkdir($recoveryDir); 155 | } 156 | 157 | $newXmlPath = $recoveryDir . '/' . $pathinfo['basename']; 158 | $this->filesystem->copy($this->xmlFile, $newXmlPath); 159 | $this->filesystem->appendToFile($newXmlPath, XmlOutputParser::$xmlCloseTag); 160 | $this->xmlFile = $newXmlPath; 161 | return true; 162 | } 163 | 164 | /** 165 | * @return Host[] 166 | */ 167 | public function parse(): array 168 | { 169 | $xml = simplexml_load_file($this->xmlFile); 170 | 171 | if (!$xml instanceof SimpleXMLElement || !isset($xml->host)) { 172 | throw new \InvalidArgumentException("{$this->xmlFile} does not appear to be valid."); 173 | } 174 | 175 | $hosts = []; 176 | foreach ($xml->host as $xmlHost) { 177 | $state = $xmlHost->status->attributes()->state ?? null; 178 | if ($state === null) { 179 | // ? log ? throw? 180 | continue; 181 | } 182 | 183 | $hostnameElement = $xmlHost->hostnames->hostname; 184 | 185 | if (!$hostnameElement instanceof SimpleXMLElement) { 186 | continue; // ? log ? throw? 187 | } 188 | 189 | $ports = $xmlHost->ports; 190 | 191 | $host = new Host( 192 | self::parseAddresses($xmlHost), 193 | (string)$state, 194 | isset($xmlHost->hostnames) ? self::parseHostnames($xmlHost->hostnames->hostname) : [], 195 | $ports ? self::parsePorts($ports->port) : [] 196 | ); 197 | 198 | $script = $xmlHost->hostscript->script ?? null; 199 | 200 | if ($script !== null) { 201 | $host->setScripts(self::parseScripts($script)); 202 | } 203 | if (isset($xmlHost->os->osmatch)) { 204 | $osName = $xmlHost->os->osmatch->attributes()->name ?? null; 205 | $osAccuracy = $xmlHost->os->osmatch->attributes()->accuracy ?? null; 206 | 207 | if ($osName !== null) { 208 | $host->setOs((string)$osName); 209 | } 210 | 211 | if ($osAccuracy !== null) { 212 | $host->setOsAccuracy((int)$osAccuracy); 213 | } 214 | } 215 | $hosts[] = $host; 216 | } 217 | 218 | return $hosts; 219 | } 220 | 221 | /** 222 | * @return Hostname[] 223 | */ 224 | public static function parseHostnames(SimpleXMLElement $xmlHostnames): array 225 | { 226 | $hostnames = []; 227 | foreach ($xmlHostnames as $hostname) { 228 | $attrs = $hostname->attributes(); 229 | $name = $type = null; 230 | if (!is_null($attrs)) { 231 | $name = $attrs->name; 232 | $type = $attrs->type; 233 | } 234 | 235 | if (!is_null($name) && !is_null($type)) { 236 | $hostnames[] = new Hostname((string)$name, (string)$type); 237 | } 238 | } 239 | 240 | return $hostnames; 241 | } 242 | 243 | /** 244 | * @return Script[] 245 | */ 246 | public static function parseScripts(SimpleXMLElement $xmlScripts): array 247 | { 248 | $scripts = []; 249 | foreach ($xmlScripts as $xmlScript) { 250 | $attrs = $xmlScript->attributes(); 251 | if (null === $attrs || $attrs->id === null || $attrs->output === null) { 252 | continue; 253 | } 254 | $scripts[] = new Script( 255 | (string)$attrs->id, 256 | (string)$attrs->output, 257 | isset($xmlScript->elem) || isset($xmlScript->table) ? self::parseScriptElems($xmlScript) : [] 258 | ); 259 | } 260 | 261 | return $scripts; 262 | } 263 | 264 | public static function parseScriptElem(SimpleXMLElement $xmlElems): array 265 | { 266 | $elems = []; 267 | foreach ($xmlElems as $xmlElem) { 268 | $attrs = $xmlElem->attributes(); 269 | 270 | if ($attrs === null) { 271 | continue; 272 | } 273 | 274 | if (empty($attrs)) { 275 | $elems[] = (string)$xmlElem[0]; 276 | } else { 277 | $key = $attrs->key ?? null; 278 | if ($key === null) { 279 | continue; 280 | } 281 | $key = (string)$key; 282 | $elems[$key] = (string)$xmlElem[0]; 283 | } 284 | } 285 | return $elems; 286 | } 287 | 288 | public static function parseScriptElems(SimpleXMLElement $xmlScript): array 289 | { 290 | if (isset($xmlScript->table)) { 291 | $elems = []; 292 | foreach ($xmlScript->table as $xmlTable) { 293 | $attributes = $xmlTable->attributes(); 294 | if ($attributes === null) { 295 | continue; 296 | } 297 | 298 | $key = $attributes->key; 299 | if ($key === null) { 300 | continue; 301 | } 302 | $key = (string)$key; 303 | 304 | $elem = $xmlTable->elem ?? null; 305 | 306 | if (!is_null($elem)) { 307 | $elems[$key] = self::parseScriptElem($elem); 308 | } 309 | } 310 | return $elems; 311 | } 312 | 313 | $elem = $xmlScript->elem ?? null; 314 | if (!is_null($elem)) { 315 | return self::parseScriptElem($elem); 316 | } 317 | throw new \InvalidArgumentException("XML must contain either a table for a single elem element"); 318 | } 319 | 320 | /** 321 | * @return Port[] 322 | */ 323 | public static function parsePorts(SimpleXMLElement $xmlPorts): array 324 | { 325 | $ports = []; 326 | foreach ($xmlPorts as $xmlPort) { 327 | $name = $product = $version = null; 328 | 329 | if ($xmlPort->service !== null) { 330 | $attrs = $xmlPort->service->attributes(); 331 | if (!is_null($attrs)) { 332 | $name = (string)$attrs->name; 333 | $product = (string)$attrs->product; 334 | $version = (string)$attrs->version; 335 | } 336 | } 337 | 338 | $service = new Service( 339 | $name, 340 | $product, 341 | $version 342 | ); 343 | 344 | $attrs = $xmlPort->attributes(); 345 | if (!is_null($attrs) && !is_null($xmlPort->state)) { 346 | $state = $xmlPort->state->attributes()->state ?? null; 347 | 348 | if ($state === null) { 349 | // ?? throw ? log ? 350 | continue; 351 | } 352 | 353 | $port = new Port( 354 | (int)$attrs->portid, 355 | (string)$attrs->protocol, 356 | (string)$state, 357 | $service 358 | ); 359 | if (isset($xmlPort->script)) { 360 | $port->setScripts(self::parseScripts($xmlPort->script)); 361 | } 362 | $ports[] = $port; 363 | } 364 | } 365 | 366 | return $ports; 367 | } 368 | 369 | /** 370 | * @return Address[] 371 | */ 372 | public static function parseAddresses(SimpleXMLElement $host): array 373 | { 374 | $addresses = []; 375 | 376 | $iter = $host->xpath('./address'); 377 | 378 | if ($iter === false || $iter === null) { 379 | return $addresses; 380 | } 381 | foreach ($iter as $address) { 382 | $attributes = $address->attributes(); 383 | if (is_null($attributes)) { 384 | continue; 385 | } 386 | $addresses[(string)$attributes->addr] = new Address( 387 | (string)$attributes->addr, 388 | (string)$attributes->addrtype, 389 | isset($attributes->vendor) ? (string)$attributes->vendor : '' 390 | ); 391 | } 392 | 393 | return $addresses; 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /tests/Nmap/Tests/Fixtures/Validation/nmap.dtd: -------------------------------------------------------------------------------- 1 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 70 | 71 | 72 | 75 | 79 | 88 | 89 | 90 | 91 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 112 | 113 | 114 | 115 | 120 | 121 | 122 | 123 | 130 | 131 | 132 | 133 | 138 | 139 | 144 | 148 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 164 | 165 | 166 | 171 | 172 | 173 | 174 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 193 | 194 | 195 | 201 | 202 | 203 | 207 | 208 | 209 | 215 | 216 | 217 | 218 | 219 | 220 | 237 | 238 | 239 | 240 | 241 | 245 | 246 | 247 | 250 | 251 | 252 | 255 | 256 | 257 | 258 | 259 | 264 | 265 | 272 | 273 | 274 | 275 | 280 | 281 | 282 | 285 | 286 | 287 | 290 | 291 | 292 | 296 | 297 | 298 | 303 | 304 | 305 | 309 | 310 | 311 | 315 | 316 | 317 | 321 | 322 | 323 | 329 | 330 | 331 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 352 | 353 | 354 | 359 | 360 | 361 | 362 | 363 | -------------------------------------------------------------------------------- /tests/Nmap/Tests/Fixtures/Validation/test_completed_valid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | cpe:/a:openbsd:openssh:6.6.1p1cpe:/o:linux:linux_kernel 190 | 191 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 465 | 466 | 467 | -------------------------------------------------------------------------------- /tests/Nmap/Tests/Fixtures/Validation/test_interrupted_invalid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/Nmap/Tests/Fixtures/test_ping_scan.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/Nmap/Tests/Fixtures/test_ping_without_reverse_dns.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/Nmap/Tests/Fixtures/test_scan.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/Nmap/Tests/Fixtures/test_scan_specifying_ports.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/Nmap/Tests/Fixtures/test_scan_with_os_detection.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | cpe:/h:cisco:srp_521wcpe:/o:cisco:linux:2.6 27 | 28 | 29 | cpe:/o:linux:linux_kernel:2.6 30 | 31 | 32 | cpe:/o:linux:linux_kernel:2.6.22 33 | 34 | 35 | cpe:/h:asus:rt-n16cpe:/o:asus:linux:2.6 36 | 37 | 38 | cpe:/o:linux:linux_kernel:2.6.22 39 | 40 | 41 | cpe:/o:asus:linux:2.6 42 | 43 | 44 | cpe:/o:linux:linux_kernel:2.6 45 | 46 | 47 | cpe:/o:linux:linux_kernel:2.6.21 48 | 49 | 50 | cpe:/o:linux:linux_kernel:2.6.23 51 | 52 | 53 | cpe:/o:linux:linux_kernel:2.6.26 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/Nmap/Tests/Fixtures/test_scan_with_service_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | cpe:/a:openbsd:openssh:5.1p1cpe:/o:linux:linux_kernel 21 | 22 | 23 | 24 | 25 | 26 | cpe:/a:igor_sysoev:nginx 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/Nmap/Tests/Fixtures/test_scan_with_verbose.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/Nmap/Tests/NmapTest.php: -------------------------------------------------------------------------------- 1 | filesystem->exists($recoverDir = __DIR__.'/Fixtures/Validation/recovered')) { 21 | $this->filesystem->remove($recoverDir); 22 | } 23 | } 24 | 25 | public function testScanBasic() 26 | { 27 | $outputFile = __DIR__.'/Fixtures/test_scan.xml'; 28 | 29 | $expectedCommand = ["nmap", "-oX", $outputFile, 'williamdurand.fr']; 30 | 31 | $executor = $this->getProcessExecutorMock($expectedCommand); 32 | 33 | 34 | $nmap = new Nmap($executor, $outputFile); 35 | $hosts = $nmap->scan(['williamdurand.fr']); 36 | $this->assertCount(1, $hosts); 37 | 38 | $host = current($hosts); 39 | 40 | $this->assertCount(2, $host->getAddresses()); 41 | $this->assertEquals('204.232.175.78', current($host->getIpv4Addresses())->getAddress()); 42 | $this->assertArrayHasKey('204.232.175.78', $host->getIpv4Addresses()); 43 | $this->assertArrayNotHasKey('00:C0:49:00:11:22', $host->getIpv4Addresses()); 44 | $this->assertEquals(Address::TYPE_IPV4, current($host->getIpv4Addresses())->getType()); 45 | $this->assertEmpty(current($host->getIpv4Addresses())->getVendor()); 46 | $this->assertEquals('00:C0:49:00:11:22', current($host->getMacAddresses())->getAddress()); 47 | $this->assertArrayHasKey('00:C0:49:00:11:22', $host->getMacAddresses()); 48 | $this->assertArrayNotHasKey('204.232.175.78', $host->getMacAddresses()); 49 | $this->assertEquals(Address::TYPE_MAC, current($host->getMacAddresses())->getType()); 50 | $this->assertEquals('U.S. Robotics', current($host->getMacAddresses())->getVendor()); 51 | $this->assertEquals(Host::STATE_UP, $host->getState()); 52 | 53 | $hostnames = $host->getHostnames(); 54 | $this->assertCount(2, $hostnames); 55 | $this->assertEquals('williamdurand.fr', $hostnames[0]->getName()); 56 | $this->assertEquals('user', $hostnames[0]->getType()); 57 | $this->assertEquals('pages.github.com', $hostnames[1]->getName()); 58 | $this->assertEquals('PTR', $hostnames[1]->getType()); 59 | 60 | $ports = $host->getPorts(); 61 | $this->assertCount(5, $ports); 62 | $this->assertCount(3, $host->getOpenPorts()); 63 | $this->assertCount(2, $host->getClosedPorts()); 64 | 65 | $this->assertEquals(22, $ports[0]->getNumber()); 66 | $this->assertEquals('tcp', $ports[0]->getProtocol()); 67 | $this->assertEquals(Port::STATE_OPEN, $ports[0]->getState()); 68 | $this->assertNotNull($ports[0]->getService()); 69 | $this->assertEquals('ssh', $ports[0]->getService()->getName()); 70 | } 71 | 72 | public function testScanSpecifyingPorts() 73 | { 74 | $outputFile = __DIR__.'/Fixtures/test_scan_specifying_ports.xml'; 75 | 76 | $expectedCommand = ["nmap", "-p 21,22,80", "-oX", $outputFile, 'williamdurand.fr']; 77 | 78 | $executor = $this->getProcessExecutorMock($expectedCommand); 79 | 80 | $nmap = new Nmap($executor, $outputFile); 81 | $hosts = $nmap->scan(['williamdurand.fr'], [21, 22, 80]); 82 | $this->assertCount(1, $hosts); 83 | 84 | $host = current($hosts); 85 | 86 | $this->assertCount(1, $host->getAddresses()); 87 | $this->assertEquals('204.232.175.78', current($host->getIpv4Addresses())->getAddress()); 88 | $this->assertArrayHasKey('204.232.175.78', $host->getIpv4Addresses()); 89 | $this->assertEquals(Address::TYPE_IPV4, current($host->getIpv4Addresses())->getType()); 90 | $this->assertEmpty(current($host->getIpv4Addresses())->getVendor()); 91 | $this->assertEquals(Host::STATE_UP, $host->getState()); 92 | 93 | $hostnames = $host->getHostnames(); 94 | $this->assertCount(2, $hostnames); 95 | $this->assertEquals('williamdurand.fr', $hostnames[0]->getName()); 96 | $this->assertEquals('user', $hostnames[0]->getType()); 97 | $this->assertEquals('pages.github.com', $hostnames[1]->getName()); 98 | $this->assertEquals('PTR', $hostnames[1]->getType()); 99 | 100 | $ports = $host->getPorts(); 101 | $this->assertCount(3, $ports); 102 | 103 | $this->assertEquals(21, $ports[0]->getNumber()); 104 | $this->assertEquals('ftp', $ports[0]->getService()->getName()); 105 | $this->assertEquals(22, $ports[1]->getNumber()); 106 | $this->assertEquals('ssh', $ports[1]->getService()->getName()); 107 | $this->assertEquals(80, $ports[2]->getNumber()); 108 | $this->assertEquals('http', $ports[2]->getService()->getName()); 109 | } 110 | 111 | public function testScanWithOsDetection() 112 | { 113 | $outputFile = __DIR__.'/Fixtures/test_scan_with_os_detection.xml'; 114 | $expectedCommand = ["nmap", "-O", "-oX", $outputFile, 'williamdurand.fr']; 115 | 116 | $executor = $this->getProcessExecutorMock($expectedCommand); 117 | 118 | $nmap = new Nmap($executor, $outputFile); 119 | $hosts = $nmap 120 | ->enableOsDetection() 121 | ->scan(['williamdurand.fr']); 122 | 123 | $this->assertEquals($hosts[0]->getOs(), 'Cisco SRP 521W WAP (Linux 2.6)'); 124 | $this->assertEquals($hosts[0]->getOsAccuracy(), 93); 125 | } 126 | 127 | public function testScanWithServiceInfo() 128 | { 129 | $outputFile = __DIR__.'/Fixtures/test_scan_with_service_info.xml'; 130 | $expectedCommand = ["nmap", "-sV", "-oX", $outputFile, 'williamdurand.fr']; 131 | 132 | $executor = $this->getProcessExecutorMock($expectedCommand); 133 | 134 | $nmap = new Nmap($executor, $outputFile); 135 | $hosts = $nmap 136 | ->enableServiceInfo() 137 | ->scan(['williamdurand.fr']); 138 | 139 | $host = current($hosts); 140 | $ports = $host->getPorts(); 141 | 142 | $service = $ports[0]->getService(); 143 | $this->assertEquals('ssh', $service->getName()); 144 | $this->assertEquals('OpenSSH', $service->getProduct()); 145 | $this->assertEquals('5.1p1 Debian 5github8', $service->getVersion()); 146 | 147 | $service = $ports[1]->getService(); 148 | $this->assertEquals('http', $service->getName()); 149 | $this->assertEquals('nginx', $service->getProduct()); 150 | } 151 | 152 | public function testScanWithVerbose() 153 | { 154 | $outputFile = __DIR__.'/Fixtures/test_scan_with_verbose.xml'; 155 | $expectedCommand = ["nmap", "-v", "-oX", $outputFile, 'williamdurand.fr']; 156 | 157 | $executor = $this->getProcessExecutorMock($expectedCommand); 158 | 159 | $nmap = new Nmap($executor, $outputFile); 160 | 161 | $hosts = $nmap 162 | ->enableVerbose() 163 | ->scan(['williamdurand.fr']); 164 | 165 | $this->assertNotEmpty($hosts); 166 | } 167 | 168 | public function testPingScan() 169 | { 170 | $outputFile = __DIR__.'/Fixtures/test_ping_scan.xml'; 171 | $expectedCommand = ["nmap", "-sn", "-oX", $outputFile, 'williamdurand.fr']; 172 | 173 | $executor = $this->getProcessExecutorMock($expectedCommand); 174 | 175 | $nmap = new Nmap($executor, $outputFile); 176 | $hosts = $nmap 177 | ->disablePortScan() 178 | ->scan(['williamdurand.fr']); 179 | 180 | $this->assertNotEmpty($hosts); 181 | } 182 | 183 | public function testScanWithoutReverseDNS() 184 | { 185 | $outputFile = __DIR__.'/Fixtures/test_ping_without_reverse_dns.xml'; 186 | $expectedCommand = ["nmap", "-n", "-oX", $outputFile, 'williamdurand.fr']; 187 | 188 | $executor = $this->getProcessExecutorMock($expectedCommand); 189 | 190 | $nmap = new Nmap($executor, $outputFile); 191 | $hosts = $nmap 192 | ->disableReverseDNS() 193 | ->scan(['williamdurand.fr']); 194 | 195 | $this->assertNotEmpty($hosts); 196 | } 197 | 198 | public function testScanWithTreatHostsAsOnline() 199 | { 200 | $outputFile = __DIR__.'/Fixtures/test_scan_with_verbose.xml'; 201 | $expectedCommand = ["nmap", "-Pn", "-oX", $outputFile, 'williamdurand.fr']; 202 | 203 | $executor = $this->getProcessExecutorMock($expectedCommand); 204 | 205 | $nmap = new Nmap($executor, $outputFile); 206 | $hosts = $nmap->treatHostsAsOnline()->scan(['williamdurand.fr']); 207 | 208 | $this->assertNotEmpty($nmap); 209 | } 210 | 211 | public function testScanWithUserTimeout() 212 | { 213 | $outputFile = __DIR__.'/Fixtures/test_scan.xml'; 214 | $timeout = 123; 215 | 216 | $mock = m::mock(\Nmap\Util\ProcessExecutor::class); 217 | 218 | $mock->shouldReceive('execute')->withArgs( 219 | function (array $args) { 220 | return $args[1] == ' -h'; 221 | })->once()->andReturn(0); 222 | 223 | $mock->shouldReceive('execute')->withArgs( 224 | function ($args, $timeout) { 225 | return $timeout == 123; 226 | })->once()->andReturn(0); 227 | 228 | $nmap = new Nmap($mock, $outputFile); 229 | $nmap->setTimeout($timeout)->scan(['williamdurand.fr']); 230 | } 231 | 232 | public function testExecutableNotExecutable() 233 | { 234 | $mock = m::mock(\Nmap\Util\ProcessExecutor::class); 235 | 236 | $mock->shouldReceive('execute')->withArgs( 237 | function (array $args) { 238 | return $args[1] == ' -h'; 239 | })->once()->andReturn(1); 240 | 241 | 242 | $this->expectException(\InvalidArgumentException::class); 243 | new Nmap($mock); 244 | } 245 | 246 | /** 247 | * @return m\Mock 248 | */ 249 | private function getProcessExecutorMock(array $expectedCommand) 250 | { 251 | $mock = m::mock(\Nmap\Util\ProcessExecutor::class); 252 | 253 | $mock->shouldReceive('execute')->withArgs( 254 | function (array $args) { 255 | return $args[1] == ' -h'; 256 | })->once()->andReturn(0); 257 | 258 | $mock 259 | ->shouldReceive('execute') 260 | ->withArgs([ 261 | $expectedCommand, 262 | 60, 263 | ]) 264 | ->once() 265 | ->andReturn(0); 266 | 267 | return $mock; 268 | } 269 | 270 | public function testExistingXmlOutputFileCanBeParsed() 271 | { 272 | $hosts = Nmap::parseOutput(__DIR__.'/Fixtures/test_scan.xml'); 273 | $host = current($hosts); 274 | $this->assertCount(1, $hosts); 275 | $this->assertCount(5, $host->getPorts()); 276 | } 277 | 278 | public function testOutputValidationInvalid() 279 | { 280 | $parser = new XmlOutputParser(__DIR__.'/Fixtures/Validation/test_interrupted_invalid.xml'); 281 | $dtd = __DIR__.'/Fixtures/Validation/nmap.dtd'; 282 | $this->assertStringContainsString('Premature end of data in tag nmaprun', $parser->validate($dtd)); 283 | } 284 | 285 | public function testOutputValidationValid() 286 | { 287 | $parser = new XmlOutputParser(__DIR__.'/Fixtures/Validation/test_completed_valid.xml'); 288 | $dtd = __DIR__.'/Fixtures/Validation/nmap.dtd'; 289 | $this->assertTrue($parser->validate($dtd)); 290 | } 291 | 292 | public function testOutputDefaultDtdPath() 293 | { 294 | $this->assertFileExists(XmlOutputParser::$defaultDtd); 295 | } 296 | 297 | public function testOutputValidationValidByUsingDtdFallback() 298 | { 299 | $parser = new XmlOutputParser(__DIR__.'/Fixtures/Validation/test_completed_valid.xml'); 300 | $this->assertTrue($parser->validate('notavalidpath')); 301 | } 302 | 303 | public function testOutputFileRecovery() 304 | { 305 | // Invalid input 306 | $input = __DIR__.'/Fixtures/Validation/test_interrupted_invalid.xml'; 307 | $parser = new XmlOutputParser($input); 308 | $this->assertTrue($parser->attemptFixInvalidFile()); 309 | 310 | // Assert recovery 311 | $output = __DIR__.'/Fixtures/Validation/recovered/test_interrupted_invalid.xml'; 312 | $this->assertFileExists($output); 313 | $this->assertTrue(filesize($output) > filesize($input)); 314 | $this->assertStringEndsWith(XmlOutputParser::$xmlCloseTag, file_get_contents($output)); 315 | } 316 | 317 | public function testOutputFileRecoveryOnValidFile() 318 | { 319 | $parser = new XmlOutputParser(__DIR__.'/Fixtures/Validation/test_completed_valid.xml'); 320 | $this->assertFalse($parser->attemptFixInvalidFile()); 321 | } 322 | 323 | } 324 | -------------------------------------------------------------------------------- /tests/Nmap/Tests/TestCase.php: -------------------------------------------------------------------------------- 1 | filesystem = new Filesystem(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | add('Nmap\Tests', __DIR__); 16 | --------------------------------------------------------------------------------