├── .github ├── actions │ └── setup-ruby-1.71.0 │ │ ├── .gitattributes │ │ ├── .github │ │ ├── ISSUE_TEMPLATE │ │ │ └── bug_report.md │ │ └── workflows │ │ │ ├── release.yml │ │ │ └── test.yml │ │ ├── .gitignore │ │ ├── .tool-versions │ │ ├── CONTRIBUTING.md │ │ ├── Gemfile │ │ ├── LICENSE │ │ ├── README.md │ │ ├── action.yml │ │ ├── bundler.js │ │ ├── common.js │ │ ├── dist │ │ └── index.js │ │ ├── gemfiles │ │ ├── bundler1.gemfile │ │ ├── nokogiri.gemfile │ │ ├── rails5.gemfile │ │ └── rails6.gemfile │ │ ├── generate-windows-versions.rb │ │ ├── index.js │ │ ├── package.json │ │ ├── pre-commit │ │ ├── ruby-builder-versions.js │ │ ├── ruby-builder.js │ │ ├── test_subprocess.rb │ │ ├── versions-strings-for-builder.rb │ │ ├── windows-versions.js │ │ ├── windows.js │ │ └── yarn.lock └── workflows │ ├── docker-deploy.yml │ ├── docker-test.yml │ ├── gem-test.yml │ └── source-test.yml ├── .gitignore ├── .gitmodules ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── bin └── ssh_scan ├── config └── policies │ ├── just_etm_macs.yaml │ ├── mozilla_intermediate.yml │ └── mozilla_modern.yml ├── data └── README ├── examples ├── 192.168.1.1.json ├── github.com.json └── localhost_v6.json ├── lib ├── ssh_scan.rb ├── ssh_scan │ ├── attribute.rb │ ├── banner.rb │ ├── client.rb │ ├── constants.rb │ ├── error.rb │ ├── error │ │ ├── closed_connection.rb │ │ ├── connect_timeout.rb │ │ ├── connection_refused.rb │ │ ├── disconnected.rb │ │ ├── no_banner.rb │ │ └── no_kex_response.rb │ ├── fingerprint_database.rb │ ├── grader.rb │ ├── os.rb │ ├── os │ │ ├── centos.rb │ │ ├── cisco.rb │ │ ├── debian.rb │ │ ├── dopra.rb │ │ ├── freebsd.rb │ │ ├── raspbian.rb │ │ ├── redhat.rb │ │ ├── ros.rb │ │ ├── ubuntu.rb │ │ ├── unknown.rb │ │ └── windows.rb │ ├── policy.rb │ ├── policy_manager.rb │ ├── protocol.rb │ ├── public_key.rb │ ├── result.rb │ ├── scan_engine.rb │ ├── ssh_fp.rb │ ├── ssh_lib.rb │ ├── ssh_lib │ │ ├── ciscossh.rb │ │ ├── cryptlib.rb │ │ ├── doprassh.rb │ │ ├── dropbear.rb │ │ ├── flowssh.rb │ │ ├── ipssh.rb │ │ ├── libssh.rb │ │ ├── mpssh.rb │ │ ├── nosssh.rb │ │ ├── openssh.rb │ │ ├── pgp.rb │ │ ├── romsshell.rb │ │ ├── rosssh.rb │ │ ├── sentryssh.rb │ │ └── unknown.rb │ ├── subprocess.rb │ ├── target_parser.rb │ ├── update.rb │ └── version.rb └── string_ext.rb ├── scripts └── Ubuntu.rb ├── spec ├── public_key_spec.rb ├── spec_helper.rb └── ssh_scan │ ├── attribute_spec.rb │ ├── banner │ ├── banner_spec.rb │ ├── generic_spec.rb │ ├── helper.rb │ ├── mixed_spec.rb │ ├── os │ │ ├── debian_spec.rb │ │ ├── freebsd_spec.rb │ │ ├── raspbian_spec.rb │ │ ├── ubuntu_spec.rb │ │ └── windows_spec.rb │ └── ssh_lib │ │ ├── cryptlib_spec.rb │ │ ├── dropbear_spec.rb │ │ ├── flowssh_spec.rb │ │ ├── ipssh_spec.rb │ │ ├── mpssh_spec.rb │ │ ├── nosssh_spec.rb │ │ ├── openssh_spec.rb │ │ ├── pgp_spec.rb │ │ ├── romsshell_spec.rb │ │ └── sentryssh_spec.rb │ ├── client_spec.rb │ ├── constants_spec.rb │ ├── fingerprint_database_spec.rb │ ├── grader_spec.rb │ ├── integration.sh │ ├── policy_manager_spec.rb │ ├── policy_spec.rb │ ├── protocol_spec.rb │ ├── result_spec.rb │ ├── ssh_fp_spec.rb │ ├── string_ext_spec.rb │ ├── target_parser_spec.rb │ └── version_spec.rb └── ssh_scan.gemspec /.github/actions/setup-ruby-1.71.0/.gitattributes: -------------------------------------------------------------------------------- 1 | /dist/index.js linguist-generated 2 | /package-lock.json linguist-generated 3 | /yarn.lock linguist-generated 4 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 22 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Update the v1 branch when a release is published 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | fetch-depth: 0 12 | - run: git push origin HEAD:v1 13 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test this action 2 | on: 3 | pull_request: 4 | push: 5 | branches-ignore: 6 | - v1 7 | tags-ignore: 8 | - '*' 9 | paths-ignore: 10 | - README.md 11 | schedule: 12 | - cron: '0 7 * * SUN' 13 | jobs: 14 | test: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ ubuntu-18.04, ubuntu-20.04, macos-10.15, macos-11.0, windows-2016, windows-2019 ] 19 | ruby: [ 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, '3.0', ruby-head, jruby, jruby-head, truffleruby, truffleruby-head ] 20 | include: 21 | - { os: windows-2016, ruby: mingw } 22 | - { os: windows-2019, ruby: mingw } 23 | - { os: windows-2019, ruby: mswin } 24 | exclude: 25 | - { os: windows-2016, ruby: debug } 26 | - { os: windows-2016, ruby: truffleruby } 27 | - { os: windows-2016, ruby: truffleruby-head } 28 | - { os: windows-2019, ruby: debug } 29 | - { os: windows-2019, ruby: truffleruby } 30 | - { os: windows-2019, ruby: truffleruby-head } 31 | 32 | name: ${{ matrix.os }} ${{ matrix.ruby }} 33 | runs-on: ${{ matrix.os }} 34 | steps: 35 | - uses: actions/checkout@v2 36 | 37 | - uses: ./ 38 | with: 39 | ruby-version: ${{ matrix.ruby }} 40 | bundler-cache: true 41 | - run: ruby -v 42 | - name: PATH 43 | shell: bash 44 | run: echo $PATH 45 | 46 | - name: build compiler 47 | run: | 48 | ruby -e "puts 'build compiler: ' + RbConfig::CONFIG.fetch('CC_VERSION_MESSAGE', 'unknown').lines.first" 49 | - name: gcc and ridk version (mingw) 50 | if: startsWith(matrix.os, 'windows') 51 | run: | 52 | $abi, $plat = $(ruby -e "STDOUT.write RbConfig::CONFIG['ruby_version'] + ' ' + RUBY_PLATFORM").split(' ') 53 | if ($plat.Contains('mingw')) { 54 | gcc --version 55 | if ($abi -ge '2.4') { 56 | ridk version 57 | } else { 58 | echo 'ridk is unavailable' 59 | } 60 | } 61 | - name: RbConfig::CONFIG 62 | run: ruby -rrbconfig -rpp -e 'pp RbConfig::CONFIG' 63 | - name: RbConfig::MAKEFILE_CONFIG 64 | run: ruby -rrbconfig -rpp -e 'pp RbConfig::MAKEFILE_CONFIG' 65 | 66 | - name: Subprocess test 67 | run: ruby test_subprocess.rb 68 | - name: OpenSSL version 69 | run: ruby -ropenssl -e 'puts OpenSSL::OPENSSL_LIBRARY_VERSION' 70 | - name: OpenSSL test 71 | run: ruby -ropen-uri -e 'puts URI.send(:open, %{https://rubygems.org/}) { |f| f.read(1024) }' 72 | 73 | - name: C extension test 74 | run: gem install json:2.2.0 --no-document 75 | - run: bundle --version 76 | # This step is redundant with `bundler-cache: true` but is there to check a redundant `bundle install` still works 77 | - run: bundle install 78 | - run: bundle exec rake --version 79 | 80 | - name: which ruby, rake 81 | if: "!startsWith(matrix.os, 'windows')" 82 | run: which -a ruby rake 83 | - name: where ruby, rake 84 | if: startsWith(matrix.os, 'windows') 85 | run: | 86 | $ErrorActionPreference = 'Continue' 87 | $where = 'ruby', 'rake' 88 | foreach ($e in $where) { 89 | $rslt = where.exe $e 2>&1 | Out-String 90 | if ($rslt.contains($e)) { echo $rslt.Trim() } 91 | else { echo "Can't find $e" } 92 | echo '' 93 | } 94 | - name: bash test 95 | shell: bash 96 | run: echo ~ 97 | # Disabled until https://github.com/jruby/jruby/issues/6648 is fixed 98 | # - name: Windows JRuby 99 | # if: startsWith(matrix.os, 'windows') && startsWith(matrix.ruby, 'jruby') 100 | # run: gem install sassc -N 101 | 102 | testExactBundlerVersion: 103 | name: "Test with an exact Bundler version" 104 | runs-on: ubuntu-latest 105 | steps: 106 | - uses: actions/checkout@v2 107 | - uses: ./ 108 | with: 109 | ruby-version: 2.6 110 | bundler: 2.2.3 111 | - run: bundle --version | grep -F "Bundler version 2.2.3" 112 | 113 | testDependencyOnBundler1: 114 | name: "Test gemfile depending on Bundler 1" 115 | runs-on: ubuntu-latest 116 | env: 117 | BUNDLE_GEMFILE: gemfiles/bundler1.gemfile 118 | steps: 119 | - uses: actions/checkout@v2 120 | - uses: ./ 121 | with: 122 | ruby-version: 2.7 123 | bundler: 1 124 | bundler-cache: true 125 | - run: bundle --version | grep -F "Bundler version 1." 126 | 127 | testGemfileMatrix: 128 | strategy: 129 | fail-fast: false 130 | matrix: 131 | gemfile: [ rails5, rails6 ] 132 | name: "Test with ${{ matrix.gemfile }} gemfile" 133 | runs-on: ubuntu-latest 134 | env: 135 | BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile 136 | steps: 137 | - uses: actions/checkout@v2 138 | - uses: ./ 139 | with: 140 | ruby-version: 2.6 141 | bundler-cache: true 142 | - run: bundle exec rails --version 143 | 144 | testTruffleRubyNokogiri: 145 | name: "Test installing a Gemfile with nokogiri on TruffleRuby" 146 | runs-on: ubuntu-latest 147 | env: 148 | BUNDLE_GEMFILE: gemfiles/nokogiri.gemfile 149 | steps: 150 | - uses: actions/checkout@v2 151 | - uses: ./ 152 | with: 153 | ruby-version: truffleruby-head 154 | bundler-cache: true 155 | - run: bundle list | grep nokogiri 156 | 157 | lint: 158 | runs-on: ubuntu-20.04 159 | steps: 160 | - uses: actions/checkout@v2 161 | - run: yarn install 162 | - run: yarn run package 163 | - name: Check generated files are up to date 164 | run: git diff --exit-code 165 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | # Editors 4 | .vscode 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Other Dependency directories 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 12.0.0 2 | ruby 2.7.0 3 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Installing dependencies 4 | 5 | ```bash 6 | $ yarn install 7 | ``` 8 | 9 | `npm` doesn't install the correct dependencies for `eslint` so we use `yarn`. 10 | 11 | ## Regenerating dist/index.js 12 | 13 | ```bash 14 | $ yarn run package 15 | ``` 16 | 17 | It is recommended to add this as a `git` `pre-commit` hook: 18 | 19 | ```bash 20 | $ cp pre-commit .git/hooks/pre-commit 21 | ``` 22 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/Gemfile: -------------------------------------------------------------------------------- 1 | # Used for testing 2 | source 'https://rubygems.org' 3 | 4 | gem "rake" 5 | gem "path" 6 | gem "json", ">= 2.3.0" -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Benoit Daloze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup Ruby, JRuby and TruffleRuby' 2 | description: 'Download a prebuilt Ruby and add it to the PATH in 5 seconds' 3 | author: 'Benoit Daloze' 4 | branding: 5 | color: red 6 | icon: download 7 | inputs: 8 | ruby-version: 9 | description: 'Engine and version to use, see the syntax in the README. Reads from .ruby-version or .tool-versions if unset.' 10 | required: false 11 | default: 'default' 12 | bundler: 13 | description: | 14 | The version of Bundler to install. Either 'none', 'latest', 'Gemfile.lock', or a version number (e.g., 1, 2, 2.1.4). 15 | For 'Gemfile.lock', the version is determined based on the BUNDLED WITH section from the file Gemfile.lock, $BUNDLE_GEMFILE.lock or gems.locked. 16 | Defaults to 'Gemfile.lock' if the file exists and 'latest' otherwise. 17 | required: false 18 | default: 'default' 19 | bundler-cache: 20 | description: 'Run "bundle install", and cache the result automatically. Either true or false.' 21 | required: false 22 | default: 'false' 23 | working-directory: 24 | description: 'The working directory to use for resolving paths for .ruby-version, .tool-versions and Gemfile.lock.' 25 | required: false 26 | default: '.' 27 | cache-version: 28 | description: | 29 | Arbitrary string that will be added to the cache key of the bundler cache. Set or change it if you need 30 | to invalidate the cache. 31 | required: false 32 | default: '0' 33 | outputs: 34 | ruby-prefix: 35 | description: 'The prefix of the installed ruby' 36 | runs: 37 | using: 'node12' 38 | main: 'dist/index.js' 39 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/bundler.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const core = require('@actions/core') 4 | const exec = require('@actions/exec') 5 | const cache = require('@actions/cache') 6 | const common = require('./common') 7 | 8 | export const DEFAULT_CACHE_VERSION = '0' 9 | 10 | // The returned gemfile is guaranteed to exist, the lockfile might not exist 11 | export function detectGemfiles() { 12 | const gemfilePath = process.env['BUNDLE_GEMFILE'] || 'Gemfile' 13 | if (fs.existsSync(gemfilePath)) { 14 | return [gemfilePath, `${gemfilePath}.lock`] 15 | } else if (process.env['BUNDLE_GEMFILE']) { 16 | throw new Error(`$BUNDLE_GEMFILE is set to ${gemfilePath} but does not exist`) 17 | } 18 | 19 | if (fs.existsSync("gems.rb")) { 20 | return ["gems.rb", "gems.locked"] 21 | } 22 | 23 | return [null, null] 24 | } 25 | 26 | function readBundledWithFromGemfileLock(lockFile) { 27 | if (lockFile !== null && fs.existsSync(lockFile)) { 28 | const contents = fs.readFileSync(lockFile, 'utf8') 29 | const lines = contents.split(/\r?\n/) 30 | const bundledWithLine = lines.findIndex(line => /^BUNDLED WITH$/.test(line.trim())) 31 | if (bundledWithLine !== -1) { 32 | const nextLine = lines[bundledWithLine+1] 33 | if (nextLine && /^\d+/.test(nextLine.trim())) { 34 | const bundlerVersion = nextLine.trim() 35 | console.log(`Using Bundler ${bundlerVersion} from ${lockFile} BUNDLED WITH ${bundlerVersion}`) 36 | return bundlerVersion 37 | } 38 | } 39 | } 40 | return null 41 | } 42 | 43 | async function afterLockFile(lockFile, platform, engine) { 44 | if (engine === 'truffleruby' && platform.startsWith('ubuntu-')) { 45 | const contents = fs.readFileSync(lockFile, 'utf8') 46 | if (contents.includes('nokogiri')) { 47 | await common.measure('Installing libxml2-dev libxslt-dev, required to install nokogiri on TruffleRuby', async () => 48 | exec.exec('sudo', ['apt-get', '-yqq', 'install', 'libxml2-dev', 'libxslt-dev'], { silent: true })) 49 | } 50 | } 51 | } 52 | 53 | export async function installBundler(bundlerVersionInput, lockFile, platform, rubyPrefix, engine, rubyVersion) { 54 | let bundlerVersion = bundlerVersionInput 55 | 56 | if (bundlerVersion === 'default' || bundlerVersion === 'Gemfile.lock') { 57 | bundlerVersion = readBundledWithFromGemfileLock(lockFile) 58 | 59 | if (!bundlerVersion) { 60 | bundlerVersion = 'latest' 61 | } 62 | } 63 | 64 | if (bundlerVersion === 'latest') { 65 | bundlerVersion = '2' 66 | } 67 | 68 | if (/^\d+/.test(bundlerVersion)) { 69 | // OK 70 | } else { 71 | throw new Error(`Cannot parse bundler input: ${bundlerVersion}`) 72 | } 73 | 74 | if (engine === 'ruby' && rubyVersion.match(/^2\.[012]/)) { 75 | console.log('Bundler 2 requires Ruby 2.3+, using Bundler 1 on Ruby <= 2.2') 76 | bundlerVersion = '1' 77 | } else if (engine === 'ruby' && rubyVersion.match(/^2\.3\.[01]/)) { 78 | console.log('Ruby 2.3.0 and 2.3.1 have shipped with an old rubygems that only works with Bundler 1') 79 | bundlerVersion = '1' 80 | } else if (engine === 'jruby' && rubyVersion.startsWith('9.1')) { // JRuby 9.1 targets Ruby 2.3, treat it the same 81 | console.log('JRuby 9.1 has a bug with Bundler 2 (https://github.com/ruby/setup-ruby/issues/108), using Bundler 1 instead on JRuby 9.1') 82 | bundlerVersion = '1' 83 | } 84 | 85 | if (common.isHeadVersion(rubyVersion) && common.isBundler2Default(engine, rubyVersion) && bundlerVersion.startsWith('2')) { 86 | // Avoid installing a newer Bundler version for head versions as it might not work. 87 | // For releases, even if they ship with Bundler 2 we install the latest Bundler. 88 | console.log(`Using Bundler 2 shipped with ${engine}-${rubyVersion}`) 89 | } else if (engine === 'truffleruby' && !common.isHeadVersion(rubyVersion) && bundlerVersion.startsWith('1')) { 90 | console.log(`Using Bundler 1 shipped with ${engine}`) 91 | } else { 92 | const gem = path.join(rubyPrefix, 'bin', 'gem') 93 | const bundlerVersionConstraint = bundlerVersion.match(/^\d+\.\d+\.\d+/) ? bundlerVersion : `~> ${bundlerVersion}` 94 | await exec.exec(gem, ['install', 'bundler', '-v', bundlerVersionConstraint, '--no-document']) 95 | } 96 | 97 | return bundlerVersion 98 | } 99 | 100 | export async function bundleInstall(gemfile, lockFile, platform, engine, rubyVersion, bundlerVersion, cacheVersion) { 101 | if (gemfile === null) { 102 | console.log('Could not determine gemfile path, skipping "bundle install" and caching') 103 | return false 104 | } 105 | 106 | let envOptions = {} 107 | if (bundlerVersion.startsWith('1') && common.isBundler2Default(engine, rubyVersion)) { 108 | // If Bundler 1 is specified on Rubies which ship with Bundler 2, 109 | // we need to specify which Bundler version to use explicitly until the lockfile exists. 110 | console.log(`Setting BUNDLER_VERSION=${bundlerVersion} for "bundle config|lock" commands below to ensure Bundler 1 is used`) 111 | envOptions = { env: { ...process.env, BUNDLER_VERSION: bundlerVersion } } 112 | } 113 | 114 | // config 115 | const cachePath = 'vendor/bundle' 116 | // An absolute path, so it is reliably under $PWD/vendor/bundle, and not relative to the gemfile's directory 117 | const bundleCachePath = path.join(process.cwd(), cachePath) 118 | 119 | await exec.exec('bundle', ['config', '--local', 'path', bundleCachePath], envOptions) 120 | 121 | if (fs.existsSync(lockFile)) { 122 | await exec.exec('bundle', ['config', '--local', 'deployment', 'true'], envOptions) 123 | } else { 124 | // Generate the lockfile so we can use it to compute the cache key. 125 | // This will also automatically pick up the latest gem versions compatible with the Gemfile. 126 | await exec.exec('bundle', ['lock'], envOptions) 127 | } 128 | 129 | await afterLockFile(lockFile, platform, engine) 130 | 131 | // cache key 132 | const paths = [cachePath] 133 | const baseKey = await computeBaseKey(platform, engine, rubyVersion, lockFile, cacheVersion) 134 | const key = `${baseKey}-${await common.hashFile(lockFile)}` 135 | // If only Gemfile.lock changes we can reuse part of the cache, and clean old gem versions below 136 | const restoreKeys = [`${baseKey}-`] 137 | console.log(`Cache key: ${key}`) 138 | 139 | // restore cache & install 140 | let cachedKey = null 141 | try { 142 | cachedKey = await cache.restoreCache(paths, key, restoreKeys) 143 | } catch (error) { 144 | if (error.name === cache.ValidationError.name) { 145 | throw error; 146 | } else { 147 | core.info(`[warning] There was an error restoring the cache ${error.message}`) 148 | } 149 | } 150 | 151 | if (cachedKey) { 152 | console.log(`Found cache for key: ${cachedKey}`) 153 | } 154 | 155 | // Always run 'bundle install' to list the gems 156 | await exec.exec('bundle', ['install', '--jobs', '4']) 157 | 158 | // @actions/cache only allows to save for non-existing keys 159 | if (cachedKey !== key) { 160 | if (cachedKey) { // existing cache but Gemfile.lock differs, clean old gems 161 | await exec.exec('bundle', ['clean']) 162 | } 163 | 164 | // Error handling from https://github.com/actions/cache/blob/master/src/save.ts 165 | console.log('Saving cache') 166 | try { 167 | await cache.saveCache(paths, key) 168 | } catch (error) { 169 | if (error.name === cache.ValidationError.name) { 170 | throw error; 171 | } else if (error.name === cache.ReserveCacheError.name) { 172 | core.info(error.message); 173 | } else { 174 | core.info(`[warning]${error.message}`) 175 | } 176 | } 177 | } 178 | 179 | return true 180 | } 181 | 182 | async function computeBaseKey(platform, engine, version, lockFile, cacheVersion) { 183 | const cacheVersionSuffix = DEFAULT_CACHE_VERSION === cacheVersion ? '' : `-cachever:${cacheVersion}` 184 | let key = `setup-ruby-bundler-cache-v3-${platform}-${engine}-${version}${cacheVersionSuffix}` 185 | 186 | if (engine !== 'jruby' && common.isHeadVersion(version)) { 187 | let revision = ''; 188 | await exec.exec('ruby', ['-e', 'print RUBY_REVISION'], { 189 | silent: true, 190 | listeners: { 191 | stdout: (data) => { 192 | revision += data.toString(); 193 | } 194 | } 195 | }); 196 | key += `-revision-${revision}` 197 | } 198 | 199 | key += `-${lockFile}` 200 | return key 201 | } 202 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/common.js: -------------------------------------------------------------------------------- 1 | const os = require('os') 2 | const path = require('path') 3 | const fs = require('fs') 4 | const util = require('util') 5 | const stream = require('stream') 6 | const crypto = require('crypto') 7 | const core = require('@actions/core') 8 | const { performance } = require('perf_hooks') 9 | 10 | export const windows = (os.platform() === 'win32') 11 | // Extract to SSD on Windows, see https://github.com/ruby/setup-ruby/pull/14 12 | export const drive = (windows ? (process.env['GITHUB_WORKSPACE'] || 'C')[0] : undefined) 13 | 14 | export function partition(string, separator) { 15 | const i = string.indexOf(separator) 16 | if (i === -1) { 17 | throw new Error(`No separator ${separator} in string ${string}`) 18 | } 19 | return [string.slice(0, i), string.slice(i + separator.length, string.length)] 20 | } 21 | 22 | let inGroup = false 23 | 24 | export async function measure(name, block) { 25 | const body = async () => { 26 | const start = performance.now() 27 | try { 28 | return await block() 29 | } finally { 30 | const end = performance.now() 31 | const duration = (end - start) / 1000.0 32 | console.log(`Took ${duration.toFixed(2).padStart(6)} seconds`) 33 | } 34 | } 35 | 36 | if (inGroup) { 37 | // Nested groups are not yet supported on GitHub Actions 38 | console.log(`> ${name}`) 39 | return await body() 40 | } else { 41 | inGroup = true 42 | try { 43 | return await core.group(name, body) 44 | } finally { 45 | inGroup = false 46 | } 47 | } 48 | } 49 | 50 | export function isHeadVersion(rubyVersion) { 51 | return rubyVersion === 'head' || rubyVersion === 'debug' || rubyVersion === 'mingw' || rubyVersion === 'mswin' 52 | } 53 | 54 | export function isStableVersion(rubyVersion) { 55 | return /^\d+(\.\d+)*$/.test(rubyVersion) 56 | } 57 | 58 | export function isBundler2Default(engine, rubyVersion) { 59 | if (engine === 'ruby') { 60 | return isHeadVersion(rubyVersion) || floatVersion(rubyVersion) >= 2.7 61 | } else if (engine === 'truffleruby') { 62 | return isHeadVersion(rubyVersion) || floatVersion(rubyVersion) >= 21.0 63 | } else if (engine === 'jruby') { 64 | return isHeadVersion(rubyVersion) || floatVersion(rubyVersion) >= 9.3 65 | } else { 66 | return false 67 | } 68 | } 69 | 70 | export function floatVersion(rubyVersion) { 71 | const match = rubyVersion.match(/^\d+\.\d+/) 72 | if (match) { 73 | return parseFloat(match[0]) 74 | } else { 75 | return 0.0 76 | } 77 | } 78 | 79 | export async function hashFile(file) { 80 | // See https://github.com/actions/runner/blob/master/src/Misc/expressionFunc/hashFiles/src/hashFiles.ts 81 | const hash = crypto.createHash('sha256') 82 | const pipeline = util.promisify(stream.pipeline) 83 | await pipeline(fs.createReadStream(file), hash) 84 | return hash.digest('hex') 85 | } 86 | 87 | function getImageOS() { 88 | const imageOS = process.env['ImageOS'] 89 | if (!imageOS) { 90 | throw new Error('The environment variable ImageOS must be set') 91 | } 92 | return imageOS 93 | } 94 | 95 | export function getVirtualEnvironmentName() { 96 | const imageOS = getImageOS() 97 | 98 | let match = imageOS.match(/^ubuntu(\d+)/) // e.g. ubuntu18 99 | if (match) { 100 | return `ubuntu-${match[1]}.04` 101 | } 102 | 103 | match = imageOS.match(/^macos(\d{2})(\d+)?/) // e.g. macos1015, macos11 104 | if (match) { 105 | return `macos-${match[1]}.${match[2] || '0'}` 106 | } 107 | 108 | match = imageOS.match(/^win(\d+)/) // e.g. win19 109 | if (match) { 110 | return `windows-20${match[1]}` 111 | } 112 | 113 | throw new Error(`Unknown ImageOS ${imageOS}`) 114 | } 115 | 116 | export function shouldUseToolCache(engine, version) { 117 | return engine === 'ruby' && !isHeadVersion(version) 118 | } 119 | 120 | function getPlatformToolCache(platform) { 121 | // Hardcode paths rather than using $RUNNER_TOOL_CACHE because the prebuilt Rubies cannot be moved anyway 122 | if (platform.startsWith('ubuntu-')) { 123 | return '/opt/hostedtoolcache' 124 | } else if (platform.startsWith('macos-')) { 125 | return '/Users/runner/hostedtoolcache' 126 | } else if (platform.startsWith('windows-')) { 127 | return 'C:/hostedtoolcache/windows' 128 | } else { 129 | throw new Error('Unknown platform') 130 | } 131 | } 132 | 133 | export function getToolCacheRubyPrefix(platform, version) { 134 | const toolCache = getPlatformToolCache(platform) 135 | return path.join(toolCache, 'Ruby', version, 'x64') 136 | } 137 | 138 | export function createToolCacheCompleteFile(toolCacheRubyPrefix) { 139 | const completeFile = `${toolCacheRubyPrefix}.complete` 140 | fs.writeFileSync(completeFile, '') 141 | } 142 | 143 | // convert windows path like C:\Users\runneradmin to /c/Users/runneradmin 144 | export function win2nix(path) { 145 | if (/^[A-Z]:/i.test(path)) { 146 | // path starts with drive 147 | path = `/${path[0].toLowerCase()}${partition(path, ':')[1]}` 148 | } 149 | return path.replace(/\\/g, '/').replace(/ /g, '\\ ') 150 | } 151 | 152 | export function setupPath(newPathEntries) { 153 | const envPath = windows ? 'Path' : 'PATH' 154 | const originalPath = process.env[envPath].split(path.delimiter) 155 | let cleanPath = originalPath.filter(entry => !/\bruby\b/i.test(entry)) 156 | 157 | // First remove the conflicting path entries 158 | if (cleanPath.length !== originalPath.length) { 159 | core.startGroup(`Cleaning ${envPath}`) 160 | console.log(`Entries removed from ${envPath} to avoid conflicts with Ruby:`) 161 | for (const entry of originalPath) { 162 | if (!cleanPath.includes(entry)) { 163 | console.log(` ${entry}`) 164 | } 165 | } 166 | core.exportVariable(envPath, cleanPath.join(path.delimiter)) 167 | core.endGroup() 168 | } 169 | 170 | // Then add new path entries using core.addPath() 171 | let newPath 172 | if (windows) { 173 | // add MSYS2 in path for all Rubies on Windows, as it provides a better bash shell and a native toolchain 174 | const msys2 = ['C:\\msys64\\mingw64\\bin', 'C:\\msys64\\usr\\bin'] 175 | newPath = [...newPathEntries, ...msys2] 176 | } else { 177 | newPath = newPathEntries 178 | } 179 | core.addPath(newPath.join(path.delimiter)) 180 | } 181 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/gemfiles/bundler1.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "bundler", "~> 1.0" 4 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/gemfiles/nokogiri.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "nokogiri" 4 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/gemfiles/rails5.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", "~> 5.2.0" 4 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/gemfiles/rails6.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", "~> 6.0.0" 4 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/generate-windows-versions.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'yaml' 3 | require 'json' 4 | 5 | min_requirements = ['~> 2.1.9', '>= 2.2.6'].map { |req| Gem::Requirement.new(req) } 6 | 7 | url = 'https://raw.githubusercontent.com/oneclick/rubyinstaller.org-website/master/_data/downloads.yaml' 8 | entries = YAML.load(Net::HTTP.get(URI(url)), symbolize_names: true) 9 | 10 | versions = entries.select { |entry| 11 | entry[:filetype] == 'rubyinstaller7z' and 12 | entry[:name].include?('(x64)') 13 | }.group_by { |entry| 14 | entry[:name][/Ruby (\d+\.\d+\.\d+)/, 1] 15 | }.map { |version, builds| 16 | unless builds.sort_by { |build| build[:name] } == builds.reverse 17 | raise "not sorted as expected for #{version}" 18 | end 19 | [version, builds.first] 20 | }.sort_by { |version, entry| 21 | Gem::Version.new(version) 22 | }.select { |version, entry| 23 | min_requirements.any? { |req| req.satisfied_by?(Gem::Version.new(version)) } 24 | }.map { |version, entry| 25 | [version, entry[:href]] 26 | }.to_h 27 | 28 | versions['head'] = 'https://github.com/oneclick/rubyinstaller2/releases/download/rubyinstaller-head/rubyinstaller-head-x64.7z' 29 | versions['mingw'] = 'https://github.com/MSP-Greg/ruby-loco/releases/download/ruby-master/ruby-mingw.7z' 30 | versions['mswin'] = 'https://github.com/MSP-Greg/ruby-loco/releases/download/ruby-master/ruby-mswin.7z' 31 | 32 | js = "export const versions = #{JSON.pretty_generate(versions)}\n" 33 | File.binwrite 'windows-versions.js', js 34 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/index.js: -------------------------------------------------------------------------------- 1 | const os = require('os') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const core = require('@actions/core') 5 | const common = require('./common') 6 | const bundler = require('./bundler') 7 | 8 | const windows = common.windows 9 | 10 | const inputDefaults = { 11 | 'ruby-version': 'default', 12 | 'bundler': 'default', 13 | 'bundler-cache': 'true', 14 | 'working-directory': '.', 15 | 'cache-version': bundler.DEFAULT_CACHE_VERSION, 16 | } 17 | 18 | // entry point when this action is run on its own 19 | export async function run() { 20 | try { 21 | await setupRuby() 22 | } catch (error) { 23 | core.setFailed(error.message) 24 | } 25 | } 26 | 27 | // entry point when this action is run from other actions 28 | export async function setupRuby(options = {}) { 29 | const inputs = { ...options } 30 | for (const key in inputDefaults) { 31 | if (!Object.prototype.hasOwnProperty.call(inputs, key)) { 32 | inputs[key] = core.getInput(key) || inputDefaults[key] 33 | } 34 | } 35 | 36 | process.chdir(inputs['working-directory']) 37 | 38 | const platform = common.getVirtualEnvironmentName() 39 | const [engine, parsedVersion] = parseRubyEngineAndVersion(inputs['ruby-version']) 40 | 41 | let installer 42 | if (platform.startsWith('windows-') && engine !== 'jruby') { 43 | installer = require('./windows') 44 | } else { 45 | installer = require('./ruby-builder') 46 | } 47 | 48 | const engineVersions = installer.getAvailableVersions(platform, engine) 49 | const version = validateRubyEngineAndVersion(platform, engineVersions, engine, parsedVersion) 50 | 51 | createGemRC() 52 | envPreInstall() 53 | 54 | const rubyPrefix = await installer.install(platform, engine, version) 55 | 56 | // When setup-ruby is used by other actions, this allows code in them to run 57 | // before 'bundle install'. Installed dependencies may require additional 58 | // libraries & headers, build tools, etc. 59 | if (inputs['afterSetupPathHook'] instanceof Function) { 60 | await inputs['afterSetupPathHook']({ platform, rubyPrefix, engine, version }) 61 | } 62 | 63 | if (inputs['bundler'] !== 'none') { 64 | const [gemfile, lockFile] = bundler.detectGemfiles() 65 | 66 | const bundlerVersion = await common.measure('Installing Bundler', async () => 67 | bundler.installBundler(inputs['bundler'], lockFile, platform, rubyPrefix, engine, version)) 68 | 69 | if (inputs['bundler-cache'] === 'true') { 70 | await common.measure('bundle install', async () => 71 | bundler.bundleInstall(gemfile, lockFile, platform, engine, version, bundlerVersion, inputs['cache-version'])) 72 | } 73 | } 74 | 75 | core.setOutput('ruby-prefix', rubyPrefix) 76 | } 77 | 78 | function parseRubyEngineAndVersion(rubyVersion) { 79 | if (rubyVersion === 'default') { 80 | if (fs.existsSync('.ruby-version')) { 81 | rubyVersion = '.ruby-version' 82 | } else if (fs.existsSync('.tool-versions')) { 83 | rubyVersion = '.tool-versions' 84 | } else { 85 | throw new Error('input ruby-version needs to be specified if no .ruby-version or .tool-versions file exists') 86 | } 87 | } 88 | 89 | if (rubyVersion === '.ruby-version') { // Read from .ruby-version 90 | rubyVersion = fs.readFileSync('.ruby-version', 'utf8').trim() 91 | console.log(`Using ${rubyVersion} as input from file .ruby-version`) 92 | } else if (rubyVersion === '.tool-versions') { // Read from .tool-versions 93 | const toolVersions = fs.readFileSync('.tool-versions', 'utf8').trim() 94 | const rubyLine = toolVersions.split(/\r?\n/).filter(e => e.match(/^ruby\s/))[0] 95 | rubyVersion = rubyLine.match(/^ruby\s+(.+)$/)[1] 96 | console.log(`Using ${rubyVersion} as input from file .tool-versions`) 97 | } 98 | 99 | let engine, version 100 | if (rubyVersion.match(/^(\d+)/) || common.isHeadVersion(rubyVersion)) { // X.Y.Z => ruby-X.Y.Z 101 | engine = 'ruby' 102 | version = rubyVersion 103 | } else if (!rubyVersion.includes('-')) { // myruby -> myruby-stableVersion 104 | engine = rubyVersion 105 | version = '' // Let the logic in validateRubyEngineAndVersion() find the version 106 | } else { // engine-X.Y.Z 107 | [engine, version] = common.partition(rubyVersion, '-') 108 | } 109 | 110 | return [engine, version] 111 | } 112 | 113 | function validateRubyEngineAndVersion(platform, engineVersions, engine, parsedVersion) { 114 | if (!engineVersions) { 115 | throw new Error(`Unknown engine ${engine} on ${platform}`) 116 | } 117 | 118 | let version = parsedVersion 119 | if (!engineVersions.includes(parsedVersion)) { 120 | const latestToFirstVersion = engineVersions.slice().reverse() 121 | // Try to match stable versions first, so an empty version (engine-only) matches the latest stable version 122 | let found = latestToFirstVersion.find(v => common.isStableVersion(v) && v.startsWith(parsedVersion)) 123 | if (!found) { 124 | // Exclude head versions, they must be exact matches 125 | found = latestToFirstVersion.find(v => !common.isHeadVersion(v) && v.startsWith(parsedVersion)) 126 | } 127 | 128 | if (found) { 129 | version = found 130 | } else { 131 | throw new Error(`Unknown version ${parsedVersion} for ${engine} on ${platform} 132 | available versions for ${engine} on ${platform}: ${engineVersions.join(', ')} 133 | Make sure you use the latest version of the action with - uses: ruby/setup-ruby@v1 134 | File an issue at https://github.com/ruby/setup-ruby/issues if would like support for a new version`) 135 | } 136 | } 137 | 138 | return version 139 | } 140 | 141 | function createGemRC() { 142 | const gemrc = path.join(os.homedir(), '.gemrc') 143 | if (!fs.existsSync(gemrc)) { 144 | fs.writeFileSync(gemrc, `gem: --no-document${os.EOL}`) 145 | } 146 | } 147 | 148 | // sets up ENV variables 149 | // currently only used on Windows runners 150 | function envPreInstall() { 151 | const ENV = process.env 152 | if (windows) { 153 | // puts normal Ruby temp folder on SSD 154 | core.exportVariable('TMPDIR', ENV['RUNNER_TEMP']) 155 | // bash - sets home to match native windows, normally C:\Users\ 156 | core.exportVariable('HOME', ENV['HOMEDRIVE'] + ENV['HOMEPATH']) 157 | // bash - needed to maintain Path from Windows 158 | core.exportVariable('MSYS2_PATH_TYPE', 'inherit') 159 | } 160 | } 161 | 162 | if (__filename.endsWith('index.js')) { run() } 163 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setup-ruby", 3 | "version": "0.1.0", 4 | "description": "Download a prebuilt Ruby and add it to the PATH in 5 seconds", 5 | "main": "index.js", 6 | "scripts": { 7 | "package": "ncc build index.js -o dist" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ruby/setup-ruby.git" 12 | }, 13 | "keywords": [ 14 | "GitHub", 15 | "Actions", 16 | "Ruby" 17 | ], 18 | "author": "Benoit Daloze", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/ruby/setup-ruby/issues" 22 | }, 23 | "homepage": "https://github.com/ruby/setup-ruby", 24 | "dependencies": { 25 | "@actions/cache": "^1.0.5", 26 | "@actions/core": "^1.2.6", 27 | "@actions/exec": "^1.0.3", 28 | "@actions/io": "^1.0.2", 29 | "@actions/tool-cache": "^1.3.4" 30 | }, 31 | "devDependencies": { 32 | "@vercel/ncc": "^0.23.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sh 2 | set -e 3 | yarn install 4 | yarn run package 5 | exec git add dist/index.js 6 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/ruby-builder-versions.js: -------------------------------------------------------------------------------- 1 | export function getVersions(platform) { 2 | const versions = { 3 | "ruby": [ 4 | "2.1.9", 5 | "2.2.10", 6 | "2.3.0", "2.3.1", "2.3.2", "2.3.3", "2.3.4", "2.3.5", "2.3.6", "2.3.7", "2.3.8", 7 | "2.4.0", "2.4.1", "2.4.2", "2.4.3", "2.4.4", "2.4.5", "2.4.6", "2.4.7", "2.4.9", "2.4.10", 8 | "2.5.0", "2.5.1", "2.5.2", "2.5.3", "2.5.4", "2.5.5", "2.5.6", "2.5.7", "2.5.8", "2.5.9", 9 | "2.6.0", "2.6.1", "2.6.2", "2.6.3", "2.6.4", "2.6.5", "2.6.6", "2.6.7", 10 | "2.7.0", "2.7.1", "2.7.2", "2.7.3", 11 | "3.0.0-preview1", "3.0.0-preview2", "3.0.0-rc1", "3.0.0", "3.0.1", 12 | "head", "debug", 13 | ], 14 | "jruby": [ 15 | "9.1.17.0", 16 | "9.2.9.0", "9.2.10.0", "9.2.11.0", "9.2.11.1", "9.2.12.0", "9.2.13.0", "9.2.14.0", "9.2.15.0", "9.2.16.0", "9.2.17.0", 17 | "head" 18 | ], 19 | "truffleruby": [ 20 | "19.3.0", "19.3.1", 21 | "20.0.0", "20.1.0", "20.2.0", "20.3.0", 22 | "21.0.0", "21.1.0", 23 | "head" 24 | ] 25 | } 26 | 27 | return versions 28 | } 29 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/ruby-builder.js: -------------------------------------------------------------------------------- 1 | const os = require('os') 2 | const path = require('path') 3 | const exec = require('@actions/exec') 4 | const io = require('@actions/io') 5 | const tc = require('@actions/tool-cache') 6 | const common = require('./common') 7 | const rubyBuilderVersions = require('./ruby-builder-versions') 8 | 9 | const builderReleaseTag = 'toolcache' 10 | const releasesURL = 'https://github.com/ruby/ruby-builder/releases' 11 | 12 | const windows = common.windows 13 | 14 | export function getAvailableVersions(platform, engine) { 15 | return rubyBuilderVersions.getVersions(platform)[engine] 16 | } 17 | 18 | export async function install(platform, engine, version) { 19 | let rubyPrefix, inToolCache 20 | if (common.shouldUseToolCache(engine, version)) { 21 | inToolCache = tc.find('Ruby', version) 22 | if (inToolCache) { 23 | rubyPrefix = inToolCache 24 | } else { 25 | rubyPrefix = common.getToolCacheRubyPrefix(platform, version) 26 | } 27 | } else if (windows) { 28 | rubyPrefix = path.join(`${common.drive}:`, `${engine}-${version}`) 29 | } else { 30 | rubyPrefix = path.join(os.homedir(), '.rubies', `${engine}-${version}`) 31 | } 32 | 33 | // Set the PATH now, so the MSYS2 'tar' is in Path on Windows 34 | common.setupPath([path.join(rubyPrefix, 'bin')]) 35 | 36 | if (!inToolCache) { 37 | await downloadAndExtract(platform, engine, version, rubyPrefix); 38 | } 39 | 40 | return rubyPrefix 41 | } 42 | 43 | async function downloadAndExtract(platform, engine, version, rubyPrefix) { 44 | const parentDir = path.dirname(rubyPrefix) 45 | 46 | await io.rmRF(rubyPrefix) 47 | await io.mkdirP(parentDir) 48 | 49 | const downloadPath = await common.measure('Downloading Ruby', async () => { 50 | const url = getDownloadURL(platform, engine, version) 51 | console.log(url) 52 | return await tc.downloadTool(url) 53 | }) 54 | 55 | await common.measure('Extracting Ruby', async () => { 56 | if (windows) { 57 | // Windows 2016 doesn't have system tar, use MSYS2's, it needs unix style paths 58 | await exec.exec('tar', ['-xz', '-C', common.win2nix(parentDir), '-f', common.win2nix(downloadPath)]) 59 | } else { 60 | await exec.exec('tar', ['-xz', '-C', parentDir, '-f', downloadPath]) 61 | } 62 | }) 63 | 64 | if (common.shouldUseToolCache(engine, version)) { 65 | common.createToolCacheCompleteFile(rubyPrefix) 66 | } 67 | } 68 | 69 | function getDownloadURL(platform, engine, version) { 70 | let builderPlatform = platform 71 | if (platform.startsWith('windows-')) { 72 | builderPlatform = 'windows-latest' 73 | } else if (platform.startsWith('macos-')) { 74 | builderPlatform = 'macos-latest' 75 | } 76 | 77 | if (common.isHeadVersion(version)) { 78 | return getLatestHeadBuildURL(builderPlatform, engine, version) 79 | } else { 80 | return `${releasesURL}/download/${builderReleaseTag}/${engine}-${version}-${builderPlatform}.tar.gz` 81 | } 82 | } 83 | 84 | function getLatestHeadBuildURL(platform, engine, version) { 85 | return `https://github.com/ruby/${engine}-dev-builder/releases/latest/download/${engine}-${version}-${platform}.tar.gz` 86 | } 87 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/test_subprocess.rb: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | require 'stringio' 3 | 4 | puts "CPPFLAGS: #{RbConfig::CONFIG["CPPFLAGS"]}" 5 | 6 | $stderr = StringIO.new 7 | begin 8 | system RbConfig.ruby, "-e", "p :OK" 9 | out = $stderr.string 10 | ensure 11 | $stderr = STDERR 12 | end 13 | abort out unless out.empty? 14 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/versions-strings-for-builder.rb: -------------------------------------------------------------------------------- 1 | hash = File.read('ruby-builder-versions.js')[/\bversions = {[^}]+}/] 2 | versions = eval hash 3 | 4 | by_minor = versions[:ruby].group_by { |v| v[/^\d\.\d/] } 5 | 6 | (1..7).each do |minor| 7 | p by_minor["2.#{minor}"].map { |v| "ruby-#{v}" } 8 | end 9 | 10 | puts 11 | p (versions[:truffleruby] - %w[head]).map { |v| "truffleruby-#{v}" } 12 | 13 | puts 14 | p (versions[:jruby] - %w[head]).map { |v| "jruby-#{v}" } 15 | 16 | (versions[:jruby] - %w[head]).each do |v| 17 | puts "- { os: windows-latest, jruby-version: #{v}, ruby: jruby-#{v} }" 18 | end 19 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/windows-versions.js: -------------------------------------------------------------------------------- 1 | export const versions = { 2 | "2.1.9": "https://github.com/oneclick/rubyinstaller/releases/download/ruby-2.1.9/ruby-2.1.9-x64-mingw32.7z", 3 | "2.2.6": "https://github.com/oneclick/rubyinstaller/releases/download/ruby-2.2.6/ruby-2.2.6-x64-mingw32.7z", 4 | "2.3.0": "https://github.com/oneclick/rubyinstaller/releases/download/ruby-2.3.0/ruby-2.3.0-x64-mingw32.7z", 5 | "2.3.1": "https://github.com/oneclick/rubyinstaller/releases/download/ruby-2.3.1/ruby-2.3.1-x64-mingw32.7z", 6 | "2.3.3": "https://github.com/oneclick/rubyinstaller/releases/download/ruby-2.3.3/ruby-2.3.3-x64-mingw32.7z", 7 | "2.4.1": "https://github.com/oneclick/rubyinstaller2/releases/download/2.4.1-2/rubyinstaller-2.4.1-2-x64.7z", 8 | "2.4.2": "https://github.com/oneclick/rubyinstaller2/releases/download/rubyinstaller-2.4.2-2/rubyinstaller-2.4.2-2-x64.7z", 9 | "2.4.3": "https://github.com/oneclick/rubyinstaller2/releases/download/rubyinstaller-2.4.3-2/rubyinstaller-2.4.3-2-x64.7z", 10 | "2.4.4": "https://github.com/oneclick/rubyinstaller2/releases/download/rubyinstaller-2.4.4-2/rubyinstaller-2.4.4-2-x64.7z", 11 | "2.4.5": "https://github.com/oneclick/rubyinstaller2/releases/download/rubyinstaller-2.4.5-1/rubyinstaller-2.4.5-1-x64.7z", 12 | "2.4.6": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.4.6-1/rubyinstaller-2.4.6-1-x64.7z", 13 | "2.4.7": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.4.7-1/rubyinstaller-2.4.7-1-x64.7z", 14 | "2.4.9": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.4.9-1/rubyinstaller-2.4.9-1-x64.7z", 15 | "2.4.10": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.4.10-1/rubyinstaller-2.4.10-1-x64.7z", 16 | "2.5.0": "https://github.com/oneclick/rubyinstaller2/releases/download/rubyinstaller-2.5.0-2/rubyinstaller-2.5.0-2-x64.7z", 17 | "2.5.1": "https://github.com/oneclick/rubyinstaller2/releases/download/rubyinstaller-2.5.1-2/rubyinstaller-2.5.1-2-x64.7z", 18 | "2.5.3": "https://github.com/oneclick/rubyinstaller2/releases/download/rubyinstaller-2.5.3-1/rubyinstaller-2.5.3-1-x64.7z", 19 | "2.5.5": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.5.5-1/rubyinstaller-2.5.5-1-x64.7z", 20 | "2.5.6": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.5.6-1/rubyinstaller-2.5.6-1-x64.7z", 21 | "2.5.7": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.5.7-1/rubyinstaller-2.5.7-1-x64.7z", 22 | "2.5.8": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.5.8-2/rubyinstaller-2.5.8-2-x64.7z", 23 | "2.5.9": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.5.9-1/rubyinstaller-2.5.9-1-x64.7z", 24 | "2.6.0": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.6.0-1/rubyinstaller-2.6.0-1-x64.7z", 25 | "2.6.1": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.6.1-1/rubyinstaller-2.6.1-1-x64.7z", 26 | "2.6.2": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.6.2-1/rubyinstaller-2.6.2-1-x64.7z", 27 | "2.6.3": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.6.3-1/rubyinstaller-2.6.3-1-x64.7z", 28 | "2.6.4": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.6.4-1/rubyinstaller-2.6.4-1-x64.7z", 29 | "2.6.5": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.6.5-1/rubyinstaller-2.6.5-1-x64.7z", 30 | "2.6.6": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.6.6-2/rubyinstaller-2.6.6-2-x64.7z", 31 | "2.6.7": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.6.7-1/rubyinstaller-2.6.7-1-x64.7z", 32 | "2.7.0": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.7.0-1/rubyinstaller-2.7.0-1-x64.7z", 33 | "2.7.1": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.7.1-1/rubyinstaller-2.7.1-1-x64.7z", 34 | "2.7.2": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.7.2-1/rubyinstaller-2.7.2-1-x64.7z", 35 | "2.7.3": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-2.7.3-1/rubyinstaller-2.7.3-1-x64.7z", 36 | "3.0.0": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-3.0.0-1/rubyinstaller-3.0.0-1-x64.7z", 37 | "3.0.1": "https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-3.0.1-1/rubyinstaller-3.0.1-1-x64.7z", 38 | "head": "https://github.com/oneclick/rubyinstaller2/releases/download/rubyinstaller-head/rubyinstaller-head-x64.7z", 39 | "mingw": "https://github.com/MSP-Greg/ruby-loco/releases/download/ruby-master/ruby-mingw.7z", 40 | "mswin": "https://github.com/MSP-Greg/ruby-loco/releases/download/ruby-master/ruby-mswin.7z" 41 | } 42 | -------------------------------------------------------------------------------- /.github/actions/setup-ruby-1.71.0/windows.js: -------------------------------------------------------------------------------- 1 | // Most of this logic is from 2 | // https://github.com/MSP-Greg/actions-ruby/blob/master/lib/main.js 3 | 4 | const fs = require('fs') 5 | const path = require('path') 6 | const cp = require('child_process') 7 | const core = require('@actions/core') 8 | const exec = require('@actions/exec') 9 | const io = require('@actions/io') 10 | const tc = require('@actions/tool-cache') 11 | const common = require('./common') 12 | const rubyInstallerVersions = require('./windows-versions').versions 13 | 14 | const drive = common.drive 15 | 16 | // needed for 2.1, 2.2, 2.3, and mswin, cert file used by Git for Windows 17 | const certFile = 'C:\\Program Files\\Git\\mingw64\\ssl\\cert.pem' 18 | 19 | // location & path for old RubyInstaller DevKit (MSYS), Ruby 2.1, 2.2 and 2.3 20 | const msys = `${drive}:\\DevKit64` 21 | const msysPathEntries = [`${msys}\\mingw\\x86_64-w64-mingw32\\bin`, `${msys}\\mingw\\bin`, `${msys}\\bin`] 22 | 23 | export function getAvailableVersions(platform, engine) { 24 | if (engine === 'ruby') { 25 | return Object.keys(rubyInstallerVersions) 26 | } else { 27 | return undefined 28 | } 29 | } 30 | 31 | export async function install(platform, engine, version) { 32 | const url = rubyInstallerVersions[version] 33 | 34 | if (!url.endsWith('.7z')) { 35 | throw new Error(`URL should end in .7z: ${url}`) 36 | } 37 | const base = url.slice(url.lastIndexOf('/') + 1, url.length - '.7z'.length) 38 | 39 | let rubyPrefix, inToolCache 40 | if (common.shouldUseToolCache(engine, version)) { 41 | inToolCache = tc.find('Ruby', version) 42 | if (inToolCache) { 43 | rubyPrefix = inToolCache 44 | } else { 45 | rubyPrefix = common.getToolCacheRubyPrefix(platform, version) 46 | } 47 | } else { 48 | rubyPrefix = `${drive}:\\${base}` 49 | } 50 | 51 | let toolchainPaths = (version === 'mswin') ? await setupMSWin() : await setupMingw(version) 52 | 53 | common.setupPath([`${rubyPrefix}\\bin`, ...toolchainPaths]) 54 | 55 | if (!inToolCache) { 56 | await downloadAndExtract(engine, version, url, base, rubyPrefix); 57 | } 58 | 59 | return rubyPrefix 60 | } 61 | 62 | async function downloadAndExtract(engine, version, url, base, rubyPrefix) { 63 | const parentDir = path.dirname(rubyPrefix) 64 | 65 | const downloadPath = await common.measure('Downloading Ruby', async () => { 66 | console.log(url) 67 | return await tc.downloadTool(url) 68 | }) 69 | 70 | await common.measure('Extracting Ruby', async () => 71 | exec.exec('7z', ['x', downloadPath, `-xr!${base}\\share\\doc`, `-o${parentDir}`], { silent: true })) 72 | 73 | if (base !== path.basename(rubyPrefix)) { 74 | await io.mv(path.join(parentDir, base), rubyPrefix) 75 | } 76 | 77 | if (common.shouldUseToolCache(engine, version)) { 78 | common.createToolCacheCompleteFile(rubyPrefix) 79 | } 80 | } 81 | 82 | async function setupMingw(version) { 83 | core.exportVariable('MAKE', 'make.exe') 84 | 85 | if (version.match(/^2\.[123]/)) { 86 | core.exportVariable('SSL_CERT_FILE', certFile) 87 | await common.measure('Installing MSYS', async () => installMSYS(version)) 88 | return msysPathEntries 89 | } else { 90 | return [] 91 | } 92 | } 93 | 94 | // Ruby 2.1, 2.2 and 2.3 95 | async function installMSYS(version) { 96 | const url = 'https://github.com/oneclick/rubyinstaller/releases/download/devkit-4.7.2/DevKit-mingw64-64-4.7.2-20130224-1432-sfx.exe' 97 | const downloadPath = await tc.downloadTool(url) 98 | await exec.exec('7z', ['x', downloadPath, `-o${msys}`], { silent: true }) 99 | 100 | // below are set in the old devkit.rb file ? 101 | core.exportVariable('RI_DEVKIT', msys) 102 | core.exportVariable('CC' , 'gcc') 103 | core.exportVariable('CXX', 'g++') 104 | core.exportVariable('CPP', 'cpp') 105 | core.info(`Installed RubyInstaller DevKit for Ruby ${version}`) 106 | } 107 | 108 | async function setupMSWin() { 109 | core.exportVariable('MAKE', 'nmake.exe') 110 | 111 | // All standard MSVC OpenSSL builds use C:\Program Files\Common Files\SSL 112 | const certsDir = 'C:\\Program Files\\Common Files\\SSL\\certs' 113 | if (!fs.existsSync(certsDir)) { 114 | fs.mkdirSync(certsDir) 115 | } 116 | 117 | // cert.pem location is hard-coded by OpenSSL msvc builds 118 | const cert = 'C:\\Program Files\\Common Files\\SSL\\cert.pem' 119 | if (!fs.existsSync(cert)) { 120 | fs.copyFileSync(certFile, cert) 121 | } 122 | 123 | return await common.measure('Setting up MSVC environment', async () => addVCVARSEnv()) 124 | } 125 | 126 | /* Sets MSVC environment for use in Actions 127 | * allows steps to run without running vcvars*.bat, also for PowerShell 128 | * adds a convenience VCVARS environment variable 129 | * this assumes a single Visual Studio version being available in the windows-latest image */ 130 | export function addVCVARSEnv() { 131 | const vcVars = '"C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise\\VC\\Auxiliary\\Build\\vcvars64.bat"' 132 | core.exportVariable('VCVARS', vcVars) 133 | 134 | let newEnv = new Map() 135 | let cmd = `cmd.exe /c "${vcVars} && set"` 136 | let newSet = cp.execSync(cmd).toString().trim().split(/\r?\n/) 137 | newSet = newSet.filter(line => line.match(/\S=\S/)) 138 | newSet.forEach(s => { 139 | let [k,v] = common.partition(s, '=') 140 | newEnv.set(k,v) 141 | }) 142 | 143 | let newPathEntries = undefined 144 | for (let [k, v] of newEnv) { 145 | if (process.env[k] !== v) { 146 | if (/^Path$/i.test(k)) { 147 | const newPathStr = v.replace(`${path.delimiter}${process.env['Path']}`, '') 148 | newPathEntries = newPathStr.split(path.delimiter) 149 | } else { 150 | core.exportVariable(k, v) 151 | } 152 | } 153 | } 154 | return newPathEntries 155 | } 156 | -------------------------------------------------------------------------------- /.github/workflows/docker-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set up QEMU 13 | uses: docker/setup-qemu-action@v1 14 | - name: Set up Docker Buildx 15 | uses: docker/setup-buildx-action@v1 16 | - name: Login to DockerHub 17 | uses: docker/login-action@v1 18 | with: 19 | username: ${{ secrets.DOCKERHUB_USERNAME }} 20 | password: ${{ secrets.DOCKERHUB_TOKEN }} 21 | - name: Build and push 22 | id: docker_build 23 | uses: docker/build-push-action@v2 24 | with: 25 | push: true 26 | tags: mozilla/ssh_scan:latest 27 | - name: Image digest 28 | run: echo ${{ steps.docker_build.outputs.digest }} -------------------------------------------------------------------------------- /.github/workflows/docker-test.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | unit-tests: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | ruby-version: ['2.6', '2.7', '3.0'] 11 | steps: 12 | - name: Run unit tests 13 | run: docker run -t mozilla/ssh_scan bundle exec rake 14 | integration-tests: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout Source 18 | uses: actions/checkout@v2 19 | - name: Build container image 20 | uses: docker/build-push-action@v2 21 | with: 22 | push: false 23 | tags: | 24 | ${{ github.repository }} 25 | - name: Run integration tests 26 | run: docker run -t mozilla/ssh_scan /app/spec/ssh_scan/integration.sh 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/gem-test.yml: -------------------------------------------------------------------------------- 1 | name: Gem 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | integration-tests: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | ruby-version: ['2.6', '2.7', '3.0'] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Ruby 14 | uses: ./.github/actions/setup-ruby-1.71.0 15 | with: 16 | ruby-version: ${{ matrix.ruby-version }} 17 | bundler-cache: true 18 | - name: Install ssh_scan gem 19 | run: gem install ssh_scan 20 | - name: Set permissions for execution 21 | run: chmod 755 ./spec/ssh_scan/integration.sh 22 | - name: Ruby Integration Tests 23 | run: ./spec/ssh_scan/integration.sh -------------------------------------------------------------------------------- /.github/workflows/source-test.yml: -------------------------------------------------------------------------------- 1 | name: Source 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | unit-tests: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | ruby-version: ['2.6', '2.7', '3.0'] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Ruby 14 | uses: ./.github/actions/setup-ruby-1.71.0 15 | with: 16 | ruby-version: ${{ matrix.ruby-version }} 17 | bundler-cache: true 18 | - name: Setup dependancies 19 | run: bundle install 20 | - name: Run unit tests 21 | run: bundle exec rake 22 | # integration-tests: 23 | # runs-on: ubuntu-latest 24 | # strategy: 25 | # matrix: 26 | # ruby-version: ['2.6', '2.7', '3.0'] 27 | # steps: 28 | # - uses: actions/checkout@v2 29 | # - name: Set up Ruby 30 | # uses: ./.github/actions/setup-ruby-1.71.0 31 | # with: 32 | # ruby-version: ${{ matrix.ruby-version }} 33 | # bundler-cache: true 34 | # - name: Setup dependancies 35 | # run: bundle install 36 | # - name: Set permissions for execution 37 | # run: chmod 755 ./spec/ssh_scan/integration.sh 38 | # - name: Ruby Integration Tests 39 | # run: ./spec/ssh_scan/integration.sh -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | *.db 4 | *.key 5 | *.crt 6 | *.cert 7 | /.config 8 | /coverage/ 9 | /InstalledFiles 10 | /pkg/ 11 | /spec/reports/ 12 | /spec/examples.txt 13 | /test/tmp/ 14 | /test/version_tmp/ 15 | /tmp/ 16 | 17 | ## Specific to RubyMotion: 18 | .dat* 19 | .repl_history 20 | build/ 21 | 22 | ## Documentation cache and generated files: 23 | /.yardoc/ 24 | /_yardoc/ 25 | /doc/ 26 | /rdoc/ 27 | 28 | ## Environment normalization: 29 | /.bundle/ 30 | /vendor/bundle 31 | /lib/bundler/man/ 32 | 33 | # for a library or gem, you might want to ignore these files since the code is 34 | # intended to run in multiple environments; otherwise, check them in: 35 | .ruby-version 36 | .ruby-gemset 37 | 38 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 39 | .rvmrc 40 | gh-pages/ 41 | 42 | # https ssl certificates 43 | cert.pem 44 | key.pem 45 | 46 | # API Database 47 | #ssh_scan 48 | 49 | # Config files 50 | bin/ssh_scan_api_example_config.yml 51 | bin/ssh_scan_worker_example_config.yml 52 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "data/ssh-badkeys"] 2 | path = data/ssh-badkeys 3 | url = https://github.com/rapid7/ssh-badkeys.git 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format documentation 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ssh_scan 2 | 3 | Thanks for your interest in contributing to ssh_scan. 4 | 5 | If you could follow the following guidelines, you will make it much easier for 6 | us to give feedback, help you find whatever problem you have and fix it. 7 | 8 | ## Issues 9 | 10 | If you have questions of any kind, or are unsure of how something works, please 11 | [create an issue](https://github.com/claudijd/ssh_scan/issues/new). 12 | 13 | Please try to answer the following questions in your issue: 14 | 15 | - What did you do? 16 | - What did you expect to happen? 17 | - What happened instead? 18 | 19 | If you have identified a bug, it would be very helpful if you could include a 20 | way to replicate the bug. Ideally a failing test would be perfect, but even a 21 | simple script demonstrating the error would suffice. 22 | 23 | Feature requests are great and if submitted they will be considered for 24 | inclusion, but sending a pull request is much more awesome. 25 | 26 | ## Pull Requests 27 | 28 | If you want your pull requests to be accepted, please follow the following guidelines: 29 | 30 | - [**Add tests!**](https://rspec.info/) Your patch won't be accepted (or will be delayed) if it doesn't have tests. 31 | 32 | - [**Document any change in behaviour**](https://yardoc.org/) Make sure the README and any other 33 | relevant documentation are kept up-to-date. 34 | 35 | - [**Create topic branches**](https://github.com/dchelimsky/rspec/wiki/Topic-Branches) Don't ask us to pull from your master branch. 36 | 37 | - [**One pull request per feature**](https://help.github.com/articles/using-pull-requests) If you want to do more than one thing, send 38 | multiple pull requests. 39 | 40 | - [**Send coherent history**](https://stackoverflow.com/questions/6934752/git-combining-multiple-commits-before-pushing) Make sure each individual commit in your pull 41 | request is meaningful. If you had to make multiple intermediate commits while 42 | developing, please squash them before sending them to us. 43 | 44 | - [**Follow coding conventions**](https://github.com/styleguide/ruby) The standard Ruby stuff, two spaces indent, 45 | don't omit parens unless you have a good reason. 46 | 47 | Thank you so much for contributing! 48 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.0.1-alpine3.13 2 | MAINTAINER Jonathan Claudius 3 | ENV PROJECT=github.com/mozilla/ssh_scan 4 | 5 | WORKDIR /app 6 | ADD . /app 7 | 8 | # required for ssh-keyscan 9 | RUN apk --update add openssh-client 10 | 11 | ENV GEM_HOME /usr/local/bundle/ruby/$RUBY_VERSION 12 | 13 | RUN apk --update add --virtual build-dependencies build-base && \ 14 | bundle install && \ 15 | apk del build-dependencies build-base && \ 16 | rm -rf /var/cache/apk/* -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'coveralls', require: false 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | ssh_scan (0.0.44) 5 | bcrypt_pbkdf (= 1.0.1) 6 | bindata (= 2.4.3) 7 | ed25519 (= 1.2.4) 8 | net-ssh (= 6.0.2) 9 | netaddr (= 2.0.4) 10 | sshkey 11 | 12 | GEM 13 | remote: https://rubygems.org/ 14 | specs: 15 | ast (2.4.2) 16 | bcrypt_pbkdf (1.0.1) 17 | bindata (2.4.3) 18 | coderay (1.1.3) 19 | coveralls (0.8.23) 20 | json (>= 1.8, < 3) 21 | simplecov (~> 0.16.1) 22 | term-ansicolor (~> 1.3) 23 | thor (>= 0.19.4, < 2.0) 24 | tins (~> 1.6) 25 | diff-lcs (1.4.4) 26 | docile (1.3.5) 27 | ed25519 (1.2.4) 28 | json (2.5.1) 29 | method_source (0.9.2) 30 | net-ssh (6.0.2) 31 | netaddr (2.0.4) 32 | parallel (1.20.1) 33 | parser (3.0.1.1) 34 | ast (~> 2.4.1) 35 | pry (0.11.3) 36 | coderay (~> 1.1.0) 37 | method_source (~> 0.9.0) 38 | rainbow (3.0.0) 39 | rake (13.0.3) 40 | regexp_parser (2.1.1) 41 | rexml (3.2.5) 42 | rspec (3.7.0) 43 | rspec-core (~> 3.7.0) 44 | rspec-expectations (~> 3.7.0) 45 | rspec-mocks (~> 3.7.0) 46 | rspec-core (3.7.1) 47 | rspec-support (~> 3.7.0) 48 | rspec-expectations (3.7.0) 49 | diff-lcs (>= 1.2.0, < 2.0) 50 | rspec-support (~> 3.7.0) 51 | rspec-its (1.2.0) 52 | rspec-core (>= 3.0.0) 53 | rspec-expectations (>= 3.0.0) 54 | rspec-mocks (3.7.0) 55 | diff-lcs (>= 1.2.0, < 2.0) 56 | rspec-support (~> 3.7.0) 57 | rspec-support (3.7.1) 58 | rubocop (1.12.1) 59 | parallel (~> 1.10) 60 | parser (>= 3.0.0.0) 61 | rainbow (>= 2.2.2, < 4.0) 62 | regexp_parser (>= 1.8, < 3.0) 63 | rexml 64 | rubocop-ast (>= 1.2.0, < 2.0) 65 | ruby-progressbar (~> 1.7) 66 | unicode-display_width (>= 1.4.0, < 3.0) 67 | rubocop-ast (1.4.1) 68 | parser (>= 2.7.1.5) 69 | ruby-progressbar (1.11.0) 70 | simplecov (0.16.1) 71 | docile (~> 1.1) 72 | json (>= 1.8, < 3) 73 | simplecov-html (~> 0.10.0) 74 | simplecov-html (0.10.2) 75 | sshkey (2.0.0) 76 | sync (0.5.0) 77 | term-ansicolor (1.7.1) 78 | tins (~> 1.0) 79 | thor (1.1.0) 80 | tins (1.29.1) 81 | sync 82 | unicode-display_width (2.0.0) 83 | 84 | PLATFORMS 85 | ruby 86 | 87 | DEPENDENCIES 88 | coveralls 89 | pry (= 0.11.3) 90 | rake (>= 12.3.3) 91 | rspec (= 3.7.0) 92 | rspec-its (= 1.2.0) 93 | rubocop 94 | ssh_scan! 95 | 96 | BUNDLED WITH 97 | 2.2.15 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssh_scan 2 | 3 | [![Gem Version](https://badge.fury.io/rb/ssh_scan.svg)](https://badge.fury.io/rb/ssh_scan) 4 | [![Coverage Status](https://coveralls.io/repos/github/mozilla/ssh_scan/badge.svg?branch=master)](https://coveralls.io/github/mozilla/ssh_scan?branch=master) 5 | 6 | A SSH configuration and policy scanner 7 | 8 | ⚠️ Deprecation Notice ⚠️ 9 | ------------------------- 10 | 11 | Mozilla is no longer maintaining the SSH Scan project. 12 | 13 | Please fork it to continue development. 14 | 15 | ## Key Benefits 16 | 17 | - **Minimal Dependencies** - Uses native Ruby and BinData to do its work, no heavy dependencies. 18 | - **Not Just a Script** - Implementation is portable for use in another project or for automation of tasks. 19 | - **Simple** - Just point `ssh_scan` at an SSH service and get a JSON report of what it supports and its policy status. 20 | - **Configurable** - Make your own custom policies that fit your unique policy requirements. 21 | 22 | ## Setup 23 | 24 | To install and run as a gem, type: 25 | 26 | ```bash 27 | gem install ssh_scan 28 | ssh_scan 29 | ``` 30 | 31 | To run from a docker container, type: 32 | 33 | ```bash 34 | docker pull mozilla/ssh_scan 35 | docker run -it mozilla/ssh_scan -t sshscan.rubidus.com 36 | ``` 37 | 38 | To install and run from source, type: 39 | 40 | ```bash 41 | # clone repo 42 | git clone https://github.com/mozilla/ssh_scan.git 43 | cd ssh_scan 44 | 45 | gem install bundler 46 | bundle install 47 | 48 | ./bin/ssh_scan 49 | ``` 50 | 51 | ## Example Command-Line Usage 52 | 53 | Run `ssh_scan -h` to get this 54 | 55 | ```bash 56 | ssh_scan v0.0.21 (https://github.com/mozilla/ssh_scan) 57 | 58 | Usage: ssh_scan [options] 59 | -t, --target [IP/Range/Hostname] IP/Ranges/Hostname to scan 60 | -f, --file [FilePath] File Path of the file containing IP/Range/Hostnames to scan 61 | -T, --timeout [seconds] Timeout per connect after which ssh_scan gives up on the host 62 | -L, --logger [Log File Path] Enable logger 63 | -O, --from_json [FilePath] File to read JSON output from 64 | -o, --output [FilePath] File to write JSON output to 65 | -p, --port [PORT] Port (Default: 22) 66 | -P, --policy [FILE] Custom policy file (Default: Mozilla Modern) 67 | --threads [NUMBER] Number of worker threads (Default: 5) 68 | --fingerprint-db [FILE] File location of fingerprint database (Default: ./fingerprints.db) 69 | --suppress-update-status Do not check for updates 70 | -u, --unit-test [FILE] Throw appropriate exit codes based on compliance status 71 | -V [STD_LOGGING_LEVEL], 72 | --verbosity 73 | -v, --version Display just version info 74 | -h, --help Show this message 75 | 76 | Examples: 77 | 78 | ssh_scan -t 192.168.1.1 79 | ssh_scan -t server.example.com 80 | ssh_scan -t ::1 81 | ssh_scan -t ::1 -T 5 82 | ssh_scan -f hosts.txt 83 | ssh_scan -o output.json 84 | ssh_scan -O output.json -o rescan_output.json 85 | ssh_scan -t 192.168.1.1 -p 22222 86 | ssh_scan -t 192.168.1.1 -p 22222 -L output.log -V INFO 87 | ssh_scan -t 192.168.1.1 -P custom_policy.yml 88 | ssh_scan -t 192.168.1.1 --unit-test -P custom_policy.yml 89 | ``` 90 | 91 | - See here for [example output](https://github.com/mozilla/ssh_scan/blob/master/examples/192.168.1.1.json) 92 | - See here for [example policies](https://github.com/mozilla/ssh_scan/blob/master/config/policies) 93 | 94 | ## ssh_scan as a service/api? 95 | 96 | This project is solely for ssh_scan engine/command-line usage. 97 | 98 | If you would like to run ssh_scan as a service, please refer to [the ssh_scan_api project](https://github.com/mozilla/ssh_scan_api) 99 | 100 | ## Rubies Supported 101 | 102 | This project is integrated with [travis-ci](http://about.travis-ci.org/) and is regularly tested to work with multiple rubies. 103 | 104 | To checkout the current build status for these rubies, click [here](https://travis-ci.org/#!/mozilla/ssh_scan). 105 | 106 | ## Contributing 107 | 108 | If you are interested in contributing to this project, please see [CONTRIBUTING.md](https://github.com/mozilla/ssh_scan/blob/master/CONTRIBUTING.md). 109 | 110 | ## Credits 111 | 112 | **Sources of Inspiration for ssh_scan** 113 | 114 | - [**Mozilla OpenSSH Security Guide**](https://wiki.mozilla.org/Security/Guidelines/OpenSSH) - For providing a sane baseline policy recommendation for SSH configuration parameters (eg. Ciphers, MACs, and KexAlgos). 115 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'rubygems/package_task' 4 | require 'rspec' 5 | require 'rspec/core' 6 | require 'rspec/core/rake_task' 7 | require 'bundler/setup' 8 | require 'ssh_scan/version' 9 | 10 | $:.unshift File.join(File.dirname(__FILE__), "lib") 11 | 12 | require 'ssh_scan' 13 | 14 | task :default => :spec 15 | 16 | desc "Run all specs in spec directory" 17 | RSpec::Core::RakeTask.new(:spec) 18 | 19 | PACKAGE_NAME = "ssh_scan" 20 | VERSION = SSHScan::VERSION -------------------------------------------------------------------------------- /config/policies/just_etm_macs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Mozilla Modern - with just ETM macs 3 | ssh_version: 2.0 4 | auth_methods: 5 | - publickey 6 | kex: 7 | - curve25519-sha256@libssh.org 8 | - ecdh-sha2-nistp521 9 | - ecdh-sha2-nistp384 10 | - ecdh-sha2-nistp256 11 | - diffie-hellman-group-exchange-sha256 12 | encryption: 13 | - chacha20-poly1305@openssh.com 14 | - aes256-gcm@openssh.com 15 | - aes128-gcm@openssh.com 16 | - aes256-ctr 17 | - aes192-ctr 18 | - aes128-ctr 19 | macs: 20 | - hmac-sha2-512-etm@openssh.com 21 | - hmac-sha2-256-etm@openssh.com 22 | - umac-128-etm@openssh.com 23 | references: 24 | - https://example.com/custom_policy -------------------------------------------------------------------------------- /config/policies/mozilla_intermediate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Mozilla Intermediate 3 | ssh_version: 2.0 4 | auth_methods: 5 | - publickey 6 | kex: 7 | - diffie-hellman-group-exchange-sha256 8 | encryption: 9 | - aes256-ctr 10 | - aes192-ctr 11 | - aes128-ctr 12 | macs: 13 | - hmac-sha2-512 14 | - hmac-sha2-256 15 | references: 16 | - https://wiki.mozilla.org/Security/Guidelines/OpenSSH 17 | -------------------------------------------------------------------------------- /config/policies/mozilla_modern.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Mozilla Modern 3 | ssh_version: 2.0 4 | auth_methods: 5 | - publickey 6 | kex: 7 | - curve25519-sha256@libssh.org 8 | - ecdh-sha2-nistp521 9 | - ecdh-sha2-nistp384 10 | - ecdh-sha2-nistp256 11 | - diffie-hellman-group-exchange-sha256 12 | encryption: 13 | - chacha20-poly1305@openssh.com 14 | - aes256-gcm@openssh.com 15 | - aes128-gcm@openssh.com 16 | - aes256-ctr 17 | - aes192-ctr 18 | - aes128-ctr 19 | macs: 20 | - hmac-sha2-512-etm@openssh.com 21 | - hmac-sha2-256-etm@openssh.com 22 | - umac-128-etm@openssh.com 23 | - hmac-sha2-512 24 | - hmac-sha2-256 25 | - umac-128@openssh.com 26 | references: 27 | - https://wiki.mozilla.org/Security/Guidelines/OpenSSH 28 | -------------------------------------------------------------------------------- /data/README: -------------------------------------------------------------------------------- 1 | This folder is used to store persistent state files, such as fingerprints.db files -------------------------------------------------------------------------------- /examples/192.168.1.1.json: -------------------------------------------------------------------------------- 1 | { 2 | "ssh_scan_version": "0.0.7", 3 | "hostname": "", 4 | "ip": "192.168.1.1", 5 | "port": 22, 6 | "server_banner": "SSH-2.0-OpenSSH_7.1p2 Debian-1", 7 | "ssh_version": 2.0, 8 | "os": "debian", 9 | "os_cpe": "o:debian:debian", 10 | "ssh_lib": "openssh", 11 | "ssh_lib_cpe": "a:openssh:openssh", 12 | "key_algorithms": [ 13 | "curve25519-sha256@libssh.org", 14 | "ecdh-sha2-nistp256", 15 | "ecdh-sha2-nistp384", 16 | "ecdh-sha2-nistp521", 17 | "diffie-hellman-group-exchange-sha256", 18 | "diffie-hellman-group14-sha1" 19 | ], 20 | "server_host_key_algorithms": [ 21 | "ssh-rsa", 22 | "ecdsa-sha2-nistp256", 23 | "ssh-ed25519" 24 | ], 25 | "encryption_algorithms_client_to_server": [ 26 | "chacha20-poly1305@openssh.com", 27 | "aes128-ctr", 28 | "aes192-ctr", 29 | "aes256-ctr", 30 | "aes128-gcm@openssh.com", 31 | "aes256-gcm@openssh.com" 32 | ], 33 | "encryption_algorithms_server_to_client": [ 34 | "chacha20-poly1305@openssh.com", 35 | "aes128-ctr", 36 | "aes192-ctr", 37 | "aes256-ctr", 38 | "aes128-gcm@openssh.com", 39 | "aes256-gcm@openssh.com" 40 | ], 41 | "mac_algorithms_client_to_server": [ 42 | "umac-64-etm@openssh.com", 43 | "umac-128-etm@openssh.com", 44 | "hmac-sha2-256-etm@openssh.com", 45 | "hmac-sha2-512-etm@openssh.com", 46 | "hmac-sha1-etm@openssh.com", 47 | "umac-64@openssh.com", 48 | "umac-128@openssh.com", 49 | "hmac-sha2-256", 50 | "hmac-sha2-512", 51 | "hmac-sha1" 52 | ], 53 | "mac_algorithms_server_to_client": [ 54 | "umac-64-etm@openssh.com", 55 | "umac-128-etm@openssh.com", 56 | "hmac-sha2-256-etm@openssh.com", 57 | "hmac-sha2-512-etm@openssh.com", 58 | "hmac-sha1-etm@openssh.com", 59 | "umac-64@openssh.com", 60 | "umac-128@openssh.com", 61 | "hmac-sha2-256", 62 | "hmac-sha2-512", 63 | "hmac-sha1" 64 | ], 65 | "compression_algorithms_client_to_server": [ 66 | "none", 67 | "zlib@openssh.com" 68 | ], 69 | "compression_algorithms_server_to_client": [ 70 | "none", 71 | "zlib@openssh.com" 72 | ], 73 | "languages_client_to_server": [ 74 | 75 | ], 76 | "languages_server_to_client": [ 77 | 78 | ], 79 | "compliance": { 80 | "policy": "Mozilla Modern", 81 | "compliant": false, 82 | "recommendations": [ 83 | "Remove these Key Exchange Algos: diffie-hellman-group14-sha1", 84 | "Remove these MAC Algos: umac-64-etm@openssh.com, hmac-sha1-etm@openssh.com, umac-64@openssh.com, hmac-sha1" 85 | ] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /examples/github.com.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ssh_scan_version": "0.0.21", 4 | "ip": "192.30.253.112", 5 | "port": 22, 6 | "server_banner": "SSH-2.0-libssh-0.7.0", 7 | "ssh_version": 2.0, 8 | "os": "unknown", 9 | "os_cpe": "o:unknown", 10 | "ssh_lib": "libssh", 11 | "ssh_lib_cpe": "a:libssh:libssh", 12 | "cookie": "3027e8e34a03febab31da1ad74d4533b", 13 | "key_algorithms": [ 14 | "curve25519-sha256@libssh.org", 15 | "ecdh-sha2-nistp256", 16 | "diffie-hellman-group14-sha1", 17 | "diffie-hellman-group1-sha1" 18 | ], 19 | "server_host_key_algorithms": [ 20 | "ssh-dss", 21 | "ssh-rsa" 22 | ], 23 | "encryption_algorithms_client_to_server": [ 24 | "chacha20-poly1305@openssh.com", 25 | "aes256-ctr", 26 | "aes192-ctr", 27 | "aes128-ctr", 28 | "aes256-cbc", 29 | "aes192-cbc", 30 | "aes128-cbc", 31 | "blowfish-cbc" 32 | ], 33 | "encryption_algorithms_server_to_client": [ 34 | "chacha20-poly1305@openssh.com", 35 | "aes256-ctr", 36 | "aes192-ctr", 37 | "aes128-ctr", 38 | "aes256-cbc", 39 | "aes192-cbc", 40 | "aes128-cbc", 41 | "blowfish-cbc" 42 | ], 43 | "mac_algorithms_client_to_server": [ 44 | "hmac-sha1", 45 | "hmac-sha2-256", 46 | "hmac-sha2-512" 47 | ], 48 | "mac_algorithms_server_to_client": [ 49 | "hmac-sha1", 50 | "hmac-sha2-256", 51 | "hmac-sha2-512" 52 | ], 53 | "compression_algorithms_client_to_server": [ 54 | "none", 55 | "zlib", 56 | "zlib@openssh.com" 57 | ], 58 | "compression_algorithms_server_to_client": [ 59 | "none", 60 | "zlib", 61 | "zlib@openssh.com" 62 | ], 63 | "languages_client_to_server": [ 64 | 65 | ], 66 | "languages_server_to_client": [ 67 | 68 | ], 69 | "hostname": "github.com", 70 | "auth_methods": [ 71 | "publickey" 72 | ], 73 | "fingerprints": { 74 | "dsa": { 75 | "known_bad": "false", 76 | "md5": "ad:1c:08:a4:40:e3:6f:9c:f5:66:26:5d:4b:33:5d:8c", 77 | "sha1": "74:91:97:3e:5f:8b:39:d5:32:7c:d4:e0:8b:c8:1b:05:f7:71:0b:49", 78 | "sha256": "6e:bf:48:8c:5b:29:9b:5b:f1:47:78:80:df:91:56:13:ee:15:4f:2c:f5:85:85:4b:20:4d:ad:d7:f0:9e:c9:64" 79 | }, 80 | "rsa": { 81 | "known_bad": "false", 82 | "md5": "16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48", 83 | "sha1": "bf:6b:68:25:d2:97:7c:51:1a:47:5b:be:fb:88:aa:d5:4a:92:ac:73", 84 | "sha256": "9d:38:5b:83:a9:17:52:92:56:1a:5e:c4:d4:81:8e:0a:ca:51:a2:64:f1:74:20:11:2e:f8:8a:c3:a1:39:49:8f" 85 | } 86 | }, 87 | "start_time": "2017-05-26 00:57:04 -0400", 88 | "end_time": "2017-05-26 00:57:05 -0400", 89 | "scan_duration_seconds": 1.286953, 90 | "duplicate_host_key_ips": [ 91 | 92 | ], 93 | "compliance": { 94 | "policy": "Mozilla Modern", 95 | "compliant": false, 96 | "recommendations": [ 97 | "Add these key exchange algorithms: ecdh-sha2-nistp521,ecdh-sha2-nistp384,diffie-hellman-group-exchange-sha256", 98 | "Add these MAC algorithms: hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,umac-128@openssh.com", 99 | "Add these encryption ciphers: aes256-gcm@openssh.com,aes128-gcm@openssh.com", 100 | "Remove these key exchange algorithms: diffie-hellman-group14-sha1, diffie-hellman-group1-sha1", 101 | "Remove these MAC algorithms: hmac-sha1", 102 | "Remove these encryption ciphers: aes256-cbc, aes192-cbc, aes128-cbc, blowfish-cbc" 103 | ], 104 | "references": [ 105 | "https://wiki.mozilla.org/Security/Guidelines/OpenSSH" 106 | ], 107 | "grade": "F" 108 | } 109 | } 110 | ] 111 | -------------------------------------------------------------------------------- /examples/localhost_v6.json: -------------------------------------------------------------------------------- 1 | { 2 | "ssh_scan_version": "0.0.7", 3 | "hostname": "", 4 | "ip": "::1", 5 | "port": 22, 6 | "server_banner": "SSH-2.0-OpenSSH_6.2", 7 | "key_algorithms": [ 8 | "diffie-hellman-group-exchange-sha256", 9 | "diffie-hellman-group-exchange-sha1", 10 | "diffie-hellman-group14-sha1", 11 | "diffie-hellman-group1-sha1" 12 | ], 13 | "server_host_key_algorithms": [ 14 | "ssh-rsa", 15 | "ssh-dss" 16 | ], 17 | "encryption_algorithms_client_to_server": [ 18 | "aes128-ctr", 19 | "aes192-ctr", 20 | "aes256-ctr", 21 | "arcfour256", 22 | "arcfour128", 23 | "aes128-gcm@openssh.com", 24 | "aes256-gcm@openssh.com", 25 | "aes128-cbc", 26 | "3des-cbc", 27 | "blowfish-cbc", 28 | "cast128-cbc", 29 | "aes192-cbc", 30 | "aes256-cbc", 31 | "arcfour", 32 | "rijndael-cbc@lysator.liu.se" 33 | ], 34 | "encryption_algorithms_server_to_client": [ 35 | "aes128-ctr", 36 | "aes192-ctr", 37 | "aes256-ctr", 38 | "arcfour256", 39 | "arcfour128", 40 | "aes128-gcm@openssh.com", 41 | "aes256-gcm@openssh.com", 42 | "aes128-cbc", 43 | "3des-cbc", 44 | "blowfish-cbc", 45 | "cast128-cbc", 46 | "aes192-cbc", 47 | "aes256-cbc", 48 | "arcfour", 49 | "rijndael-cbc@lysator.liu.se" 50 | ], 51 | "mac_algorithms_client_to_server": [ 52 | "hmac-md5-etm@openssh.com", 53 | "hmac-sha1-etm@openssh.com", 54 | "umac-64-etm@openssh.com", 55 | "umac-128-etm@openssh.com", 56 | "hmac-sha2-256-etm@openssh.com", 57 | "hmac-sha2-512-etm@openssh.com", 58 | "hmac-ripemd160-etm@openssh.com", 59 | "hmac-sha1-96-etm@openssh.com", 60 | "hmac-md5-96-etm@openssh.com", 61 | "hmac-md5", 62 | "hmac-sha1", 63 | "umac-64@openssh.com", 64 | "umac-128@openssh.com", 65 | "hmac-sha2-256", 66 | "hmac-sha2-512", 67 | "hmac-ripemd160", 68 | "hmac-ripemd160@openssh.com", 69 | "hmac-sha1-96", 70 | "hmac-md5-96" 71 | ], 72 | "mac_algorithms_server_to_client": [ 73 | "hmac-md5-etm@openssh.com", 74 | "hmac-sha1-etm@openssh.com", 75 | "umac-64-etm@openssh.com", 76 | "umac-128-etm@openssh.com", 77 | "hmac-sha2-256-etm@openssh.com", 78 | "hmac-sha2-512-etm@openssh.com", 79 | "hmac-ripemd160-etm@openssh.com", 80 | "hmac-sha1-96-etm@openssh.com", 81 | "hmac-md5-96-etm@openssh.com", 82 | "hmac-md5", 83 | "hmac-sha1", 84 | "umac-64@openssh.com", 85 | "umac-128@openssh.com", 86 | "hmac-sha2-256", 87 | "hmac-sha2-512", 88 | "hmac-ripemd160", 89 | "hmac-ripemd160@openssh.com", 90 | "hmac-sha1-96", 91 | "hmac-md5-96" 92 | ], 93 | "compression_algorithms_client_to_server": [ 94 | "none", 95 | "zlib@openssh.com" 96 | ], 97 | "compression_algorithms_server_to_client": [ 98 | "none", 99 | "zlib@openssh.com" 100 | ], 101 | "languages_client_to_server": [ 102 | 103 | ], 104 | "languages_server_to_client": [ 105 | 106 | ], 107 | "compliance": { 108 | "policy": "Mozilla Modern", 109 | "compliant": false, 110 | "recommendations": [ 111 | "Add these Key Exchange Algos: curve25519-sha256@libssh.org, ecdh-sha2-nistp521, ecdh-sha2-nistp384, ecdh-sha2-nistp256", 112 | "Add these Encryption Ciphers: chacha20-poly1305@openssh.com", 113 | "Remove these Key Exchange Algos: diffie-hellman-group-exchange-sha1, diffie-hellman-group14-sha1, diffie-hellman-group1-sha1", 114 | "Remove these MAC Algos: hmac-md5-etm@openssh.com, hmac-sha1-etm@openssh.com, umac-64-etm@openssh.com, hmac-ripemd160-etm@openssh.com, hmac-sha1-96-etm@openssh.com, hmac-md5-96-etm@openssh.com, hmac-md5, hmac-sha1, umac-64@openssh.com, hmac-ripemd160, hmac-ripemd160@openssh.com, hmac-sha1-96, hmac-md5-96", 115 | "Remove these Encryption Ciphers: arcfour256, arcfour128, aes128-cbc, 3des-cbc, blowfish-cbc, cast128-cbc, aes192-cbc, aes256-cbc, arcfour, rijndael-cbc@lysator.liu.se" 116 | ] 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/ssh_scan.rb: -------------------------------------------------------------------------------- 1 | #External Deps 2 | require 'bindata' 3 | require 'timeout' 4 | require 'resolv' 5 | 6 | #Internal Deps 7 | require 'ssh_scan/version' 8 | require 'ssh_scan/constants' 9 | require 'ssh_scan/policy' 10 | require 'ssh_scan/policy_manager' 11 | require 'ssh_scan/protocol' 12 | require 'ssh_scan/scan_engine' 13 | require 'ssh_scan/target_parser' 14 | require 'ssh_scan/update' 15 | require 'ssh_scan/fingerprint_database' 16 | require 'ssh_scan/grader' 17 | require 'ssh_scan/result' 18 | 19 | #Monkey Patches 20 | require 'string_ext' 21 | -------------------------------------------------------------------------------- /lib/ssh_scan/attribute.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module SSHScan 4 | # A helper to turn array of strings into arrays of attributes for quick comparison 5 | def self.make_attributes(array) 6 | array.map {|item| SSHScan::Attribute.new(item)} 7 | end 8 | 9 | # A class for making attribute comparison possible beyond simple string comparison 10 | class Attribute 11 | def initialize(attribute_string) 12 | @attribute_string = attribute_string 13 | end 14 | 15 | def to_s 16 | @attribute_string 17 | end 18 | 19 | def base 20 | @attribute_string.split("@").first 21 | end 22 | 23 | def ==(other) 24 | self.base == other.base 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /lib/ssh_scan/banner.rb: -------------------------------------------------------------------------------- 1 | require 'ssh_scan/os' 2 | require 'ssh_scan/ssh_lib' 3 | 4 | module SSHScan 5 | class Banner 6 | def initialize(string) 7 | @string = string 8 | end 9 | 10 | # Create {SSHScan::Banner} object based on target's SSH banner. 11 | # @param string [String] String from which the banner should be 12 | # constructed. 13 | # @return [SSHScan::Banner] {SSHScan::Banner} object 14 | # constructed from string. 15 | def self.read(string) 16 | return SSHScan::Banner.new(string) 17 | end 18 | 19 | # Guess target's SSH version. 20 | # @return [String] If SSH version string looks like "SSH-1.81" 21 | # or "SSH-number" then return the number, else return 22 | # "unknown" 23 | def ssh_version() 24 | if match = @string.match(/SSH-(\d+[\.\d+]+)/) 25 | return match[1].to_f 26 | else 27 | return "unknown" 28 | end 29 | end 30 | 31 | # Guess target's SSH Library (OpenSSH, LibSSH ...). 32 | # See {SSHScan::SSHLib} for a list of SSH libraries supported. 33 | # @return [SSHScan::SSHLib] Guessed {SSHScan::SSHLib} instance, 34 | # otherwise {SSHScan::SSHLib::Unknown} instance. 35 | def ssh_lib_guess() 36 | case @string 37 | when /OpenSSH/i 38 | return SSHScan::SSHLib::OpenSSH.new(@string) 39 | when /LibSSH/i 40 | return SSHScan::SSHLib::LibSSH.new() 41 | when /ipssh/i 42 | return SSHScan::SSHLib::IpSsh.new(@string) 43 | when /Cisco/i 44 | return SSHScan::SSHLib::CiscoSSH.new() 45 | when /ROS/ 46 | return SSHScan::SSHLib::ROSSSH.new() 47 | when /DOPRASSH/i 48 | return SSHScan::SSHLib::DOPRASSH.new() 49 | when /cryptlib/i 50 | return SSHScan::SSHLib::Cryptlib.new() 51 | when /NOS-SSH/i 52 | return SSHScan::SSHLib::NosSSH.new(@string) 53 | when /pgp/i 54 | return SSHScan::SSHLib::PGP.new() 55 | when /ServerTech_SSH|Mocana SSH/i 56 | return SSHScan::SSHLib::SentrySSH.new() 57 | when /mpssh/i 58 | return SSHScan::SSHLib::Mpssh.new(@string) 59 | when /dropbear/i 60 | return SSHScan::SSHLib::Dropbear.new(@string) 61 | when /RomSShell/i 62 | return SSHScan::SSHLib::RomSShell.new(@string) 63 | when /Flowssh/i 64 | return SSHScan::SSHLib::FlowSsh.new(@string) 65 | else 66 | return SSHScan::SSHLib::Unknown.new() 67 | end 68 | end 69 | 70 | # Guess target's OS (Ubuntu, CentOS ...). 71 | # See {SSHScan::OS} for a list of OS(s) supported. 72 | # @return [SSHScan::OS] Guessed {SSHScan::OS} instance, 73 | # otherwise {SSHScan::OS::Unknown} instance. 74 | def os_guess() 75 | case @string 76 | when /Ubuntu/i 77 | return SSHScan::OS::Ubuntu.new(@string) 78 | when /6.6p1-5build1/i # non-standard Ubuntu release 79 | return SSHScan::OS::Ubuntu.new(@string) 80 | when /CentOS/i 81 | return SSHScan::OS::CentOS.new 82 | when /RHEL|RedHat/i 83 | return SSHScan::OS::RedHat.new 84 | when /FreeBSD/i 85 | return SSHScan::OS::FreeBSD.new 86 | when /Debian/i 87 | return SSHScan::OS::Debian.new 88 | when /Windows|Microsoft/i 89 | return SSHScan::OS::Windows.new 90 | when /Cisco/i 91 | return SSHScan::OS::Cisco.new 92 | when /Raspbian/i 93 | return SSHScan::OS::Raspbian.new(@string) 94 | when /ROS/i 95 | return SSHScan::OS::ROS.new 96 | when /DOPRA/i 97 | return SSHScan::OS::DOPRA.new 98 | else 99 | return SSHScan::OS::Unknown.new 100 | end 101 | end 102 | 103 | def ==(other) 104 | self.to_s == other.to_s 105 | end 106 | 107 | def to_s 108 | @string 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/ssh_scan/client.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'timeout' 3 | require 'ssh_scan/constants' 4 | require 'ssh_scan/protocol' 5 | require 'ssh_scan/banner' 6 | require 'ssh_scan/error' 7 | 8 | module SSHScan 9 | class Client 10 | def initialize(ip, port, timeout = 3) 11 | @ip = ip 12 | @timeout = timeout 13 | 14 | @port = port.to_i 15 | @client_banner = SSHScan::Constants::DEFAULT_CLIENT_BANNER 16 | @server_banner = nil 17 | @kex_init_raw = SSHScan::Constants::DEFAULT_KEY_INIT.to_binary_s 18 | end 19 | 20 | def ip 21 | @ip 22 | end 23 | 24 | def port 25 | @port 26 | end 27 | 28 | def banner 29 | @server_banner 30 | end 31 | 32 | def close() 33 | begin 34 | unless @sock.nil? 35 | @sock.close 36 | end 37 | rescue 38 | @sock = nil 39 | end 40 | return true 41 | end 42 | 43 | def connect() 44 | @error = nil 45 | 46 | begin 47 | Timeout::timeout(@timeout) { 48 | @sock = Socket.tcp(@ip, @port, connect_timeout: @timeout) 49 | @raw_server_banner = @sock.gets 50 | } 51 | rescue SocketError => e 52 | @error = SSHScan::Error::ConnectionRefused.new(e.message) 53 | @sock = nil 54 | rescue Errno::ETIMEDOUT, 55 | Timeout::Error => e 56 | @error = SSHScan::Error::ConnectTimeout.new(e.message) 57 | @sock = nil 58 | rescue Errno::ECONNREFUSED => e 59 | @error = SSHScan::Error::ConnectionRefused.new(e.message) 60 | @sock = nil 61 | rescue Errno::ENETUNREACH => e 62 | @error = SSHScan::Error::ConnectionRefused.new(e.message) 63 | @sock = nil 64 | rescue Errno::ECONNRESET => e 65 | @error = SSHScan::Error::ConnectionRefused.new(e.message) 66 | @sock = nil 67 | rescue Errno::EACCES => e 68 | @error = SSHScan::Error::ConnectionRefused.new(e.message) 69 | @sock = nil 70 | rescue Errno::EHOSTUNREACH => e 71 | @error = SSHScan::Error::ConnectionRefused.new(e.message) 72 | @sock = nil 73 | rescue Errno::ENOPROTOOPT => e 74 | @error = SSHScan::Error::ConnectionRefused.new(e.message) 75 | @sock = nil 76 | else 77 | if @raw_server_banner.nil? 78 | @error = SSHScan::Error::NoBanner.new( 79 | "service did not respond with an SSH banner" 80 | ) 81 | @sock = nil 82 | else 83 | @raw_server_banner = @raw_server_banner.chomp 84 | @server_banner = SSHScan::Banner.read(@raw_server_banner) 85 | @sock.puts(@client_banner.to_s) 86 | end 87 | end 88 | end 89 | 90 | def error? 91 | !@error.nil? 92 | end 93 | 94 | def error 95 | @error 96 | end 97 | 98 | def get_kex_result(kex_init_raw = @kex_init_raw) 99 | if !@sock 100 | @error = "Socket is no longer valid" 101 | return nil 102 | end 103 | 104 | kex_exchange_init = nil 105 | 106 | begin 107 | Timeout::timeout(@timeout) { 108 | @sock.write(kex_init_raw) 109 | resp = @sock.read(4) 110 | 111 | if resp.nil? 112 | @error = SSHScan::Error::NoKexResponse.new( 113 | "service did not respond to our kex init request" 114 | ) 115 | @sock = nil 116 | return nil 117 | end 118 | 119 | resp += @sock.read(resp.unpack("N").first) 120 | @sock.close 121 | 122 | kex_exchange_init = SSHScan::KeyExchangeInit.read(resp) 123 | } 124 | rescue Errno::ETIMEDOUT, 125 | Timeout::Error => e 126 | @error = SSHScan::Error::ConnectTimeout.new(e.message) 127 | @sock = nil 128 | return nil 129 | rescue Errno::ECONNREFUSED, 130 | Errno::ENETUNREACH, 131 | Errno::ECONNRESET, 132 | Errno::EACCES, 133 | Errno::EHOSTUNREACH 134 | @error = SSHScan::Error::NoKexResponse.new( 135 | "service did not respond to our kex init request" 136 | ) 137 | @sock = nil 138 | return nil 139 | end 140 | 141 | return kex_exchange_init.to_hash 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /lib/ssh_scan/constants.rb: -------------------------------------------------------------------------------- 1 | require 'string_ext' 2 | require 'ssh_scan/banner' 3 | require 'ssh_scan/protocol' 4 | 5 | module SSHScan 6 | # House all the constants we need. 7 | module Constants 8 | DEFAULT_CLIENT_BANNER = SSHScan::Banner.new("SSH-2.0-ssh_scan") 9 | DEFAULT_SERVER_BANNER = SSHScan::Banner.new("SSH-2.0-server") 10 | 11 | default_key_init_opts = { 12 | :cookie => "e33f813f8cdcc6b00a3d852ec1aea498".unhexify, 13 | :padding => "6e05b3b4".unhexify, 14 | :key_algorithms => ["diffie-hellman-group1-sha1"], 15 | :server_host_key_algorithms => ["ssh-dss","ssh-rsa"], 16 | :encryption_algorithms_client_to_server => [ 17 | "aes128-cbc","3des-cbc","blowfish-cbc","aes192-cbc","aes256-cbc", 18 | "aes128-ctr","aes192-ctr","aes256-ctr" 19 | ], 20 | :encryption_algorithms_server_to_client => [ 21 | "aes128-cbc","3des-cbc","blowfish-cbc","aes192-cbc","aes256-cbc", 22 | "aes128-ctr","aes192-ctr","aes256-ctr" 23 | ], 24 | :mac_algorithms_client_to_server => [ 25 | "hmac-md5","hmac-sha1","hmac-ripemd160" 26 | ], 27 | :mac_algorithms_server_to_client => [ 28 | "hmac-md5","hmac-sha1","hmac-ripemd160" 29 | ], 30 | :compression_algorithms_client_to_server => ["none"], 31 | :compression_algorithms_server_to_client => ["none"], 32 | :languages_client_to_server => [], 33 | :languages_server_to_client => [], 34 | } 35 | 36 | DEFAULT_KEY_INIT = SSHScan::KeyExchangeInit.from_hash(default_key_init_opts) 37 | 38 | DEFAULT_KEY_INIT_RAW = 39 | "000001640414e33f813f8cdcc6b00a3d852ec1aea4980000001a6\ 40 | 469666669652d68656c6c6d616e2d67726f7570312d7368613100\ 41 | 00000f7373682d6473732c7373682d72736100000057616573313\ 42 | 2382d6362632c336465732d6362632c626c6f77666973682d6362\ 43 | 632c6165733139322d6362632c6165733235362d6362632c61657\ 44 | 33132382d6374722c6165733139322d6374722c6165733235362d\ 45 | 637472000000576165733132382d6362632c336465732d6362632\ 46 | c626c6f77666973682d6362632c6165733139322d6362632c6165\ 47 | 733235362d6362632c6165733132382d6374722c6165733139322\ 48 | d6374722c6165733235362d63747200000021686d61632d6d6435\ 49 | 2c686d61632d736861312c686d61632d726970656d64313630000\ 50 | 00021686d61632d6d64352c686d61632d736861312c686d61632d\ 51 | 726970656d64313630000000046e6f6e65000000046e6f6e65000\ 52 | 000000000000000000000006e05b3b4".freeze 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/ssh_scan/error.rb: -------------------------------------------------------------------------------- 1 | require 'ssh_scan/error/connect_timeout' 2 | require 'ssh_scan/error/closed_connection' 3 | require 'ssh_scan/error/connection_refused' 4 | require 'ssh_scan/error/disconnected' 5 | require 'ssh_scan/error/no_banner' 6 | require 'ssh_scan/error/no_kex_response' 7 | -------------------------------------------------------------------------------- /lib/ssh_scan/error/closed_connection.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module Error 3 | # Connection closed from the other side. 4 | class ClosedConnection < RuntimeError 5 | def to_s 6 | "#{self.class.to_s.split('::')[-1]}" 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/ssh_scan/error/connect_timeout.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module Error 3 | # Timed out trying to connect. 4 | class ConnectTimeout < RuntimeError 5 | def initialize(message) 6 | @message = message 7 | end 8 | def to_s 9 | "#{self.class.to_s.split('::')[-1]}: #{@message}" 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/ssh_scan/error/connection_refused.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module Error 3 | # Target refused connection attempt. 4 | class ConnectionRefused < RuntimeError 5 | def initialize(message) 6 | @message = message 7 | end 8 | def to_s 9 | "#{self.class.to_s.split('::')[-1]}: #{@message}" 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/ssh_scan/error/disconnected.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module Error 3 | # Got disconnected somehow. 4 | class Disconnected < RuntimeError 5 | def initialize(message) 6 | @message = message 7 | end 8 | def to_s 9 | "#{self.class.to_s.split('::')[-1]}: #{@message}" 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/ssh_scan/error/no_banner.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module Error 3 | # Target did not respond with an SSH banner. 4 | class NoBanner < RuntimeError 5 | def initialize(message) 6 | @message = message 7 | end 8 | def to_s 9 | "#{self.class.to_s.split('::')[-1]}: #{@message}" 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/ssh_scan/error/no_kex_response.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module Error 3 | # Failed to do key exchange. 4 | class NoKexResponse < RuntimeError 5 | def initialize(message) 6 | @message = message 7 | end 8 | def to_s 9 | "#{self.class.to_s.split('::')[-1]}: #{@message}" 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/ssh_scan/fingerprint_database.rb: -------------------------------------------------------------------------------- 1 | require 'yaml/store' 2 | 3 | module SSHScan 4 | # Create and/or maintain a fingerprint database using YAML Store. 5 | class FingerprintDatabase 6 | def initialize(database_name) 7 | @store = YAML::Store.new(database_name) 8 | end 9 | 10 | # Empty the fingerprints database for given IP. 11 | # @param ip [String] IP for which fingerprints should be 12 | # cleared. 13 | def clear_fingerprints(ip) 14 | @store.transaction do 15 | @store[ip] = [] 16 | end 17 | end 18 | 19 | # Insert a (fingerprint, IP) record. 20 | # @param fingerprint [String] fingerprint to insert 21 | # @param ip [String] IP for which fingerprint has to be added 22 | def add_fingerprint(fingerprint, ip) 23 | @store.transaction do 24 | @store[ip] = [] if @store[ip].nil? 25 | @store[ip] << fingerprint 26 | end 27 | end 28 | 29 | # Find IPs that have the given fingerprint. 30 | # @param fingerprint [String] fingerprint for which search 31 | # should be performed 32 | # @return [Array] return unique IPs for which the given 33 | # fingerprint has an entry 34 | def find_fingerprints(fingerprint) 35 | ip_matches = [] 36 | 37 | @store.transaction(true) do 38 | @store.roots.each do |ip| 39 | @store[ip].each do |other_fingerprint| 40 | if fingerprint == other_fingerprint 41 | ip_matches << ip 42 | end 43 | end 44 | end 45 | end 46 | 47 | return ip_matches.uniq 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/ssh_scan/grader.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | # A very crude means of translating # of compliance recommendations into a a grade 3 | # Basic formula is 100 - (# of recommendations * 10) 4 | class Grader 5 | GRADE_MAP = { 6 | 91..100 => "A", 7 | 81..90 => "B", 8 | 71..80 => "C", 9 | 61..70 => "D", 10 | 0..60 => "F", 11 | } 12 | 13 | def initialize(result) 14 | @result = result 15 | end 16 | 17 | def grade 18 | score = 100 19 | 20 | if @result.compliance_recommendations.each do |recommendation| 21 | score -= 10 22 | end 23 | end 24 | 25 | GRADE_MAP.each do |score_range,grade| 26 | if score_range.include?(score) 27 | return grade 28 | end 29 | end 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /lib/ssh_scan/os.rb: -------------------------------------------------------------------------------- 1 | require 'ssh_scan/os/centos' 2 | require 'ssh_scan/os/debian' 3 | require 'ssh_scan/os/freebsd' 4 | require 'ssh_scan/os/ubuntu' 5 | require 'ssh_scan/os/windows' 6 | require 'ssh_scan/os/redhat' 7 | require 'ssh_scan/os/cisco' 8 | require 'ssh_scan/os/ros' 9 | require 'ssh_scan/os/raspbian' 10 | require 'ssh_scan/os/dopra' 11 | require 'ssh_scan/os/unknown' 12 | -------------------------------------------------------------------------------- /lib/ssh_scan/os/centos.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module OS 3 | class CentOS 4 | def common 5 | "centos" 6 | end 7 | 8 | def cpe 9 | "o:centos:centos" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/os/cisco.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module OS 3 | class Cisco 4 | def common 5 | "cisco" 6 | end 7 | 8 | def cpe 9 | "o:cisco:cisco" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/os/debian.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module OS 3 | class Debian 4 | def common 5 | "debian" 6 | end 7 | 8 | def cpe 9 | "o:debian:debian" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/os/dopra.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module OS 3 | class DOPRA 4 | def common 5 | "dopra" 6 | end 7 | 8 | def cpe 9 | "o:dopra:dopra" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/os/freebsd.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module OS 3 | class FreeBSD 4 | def common 5 | "freebsd" 6 | end 7 | 8 | def cpe 9 | "o:freebsd:freebsd" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/os/raspbian.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module OS 3 | class Raspbian 4 | attr_reader :version 5 | 6 | class Version 7 | def initialize(version_string) 8 | @version_string = version_string 9 | end 10 | 11 | def to_s 12 | @version_string 13 | end 14 | end 15 | 16 | def initialize(banner) 17 | @banner = banner 18 | @version = Raspbian::Version.new(raspbian_version_guess) 19 | end 20 | 21 | # Guess Raspbian OS version. Typically, Raspbian banners 22 | # are like "SSH-2.0-Raspbian-something", where something 23 | # is the Raspbian version. 24 | # @return [String] version string matched from banner, nil 25 | # if not matched 26 | def raspbian_version_guess 27 | return nil if @banner.nil? 28 | match = @banner.match(/SSH-2.0-Raspbian-(\d+)/) 29 | return nil if match.nil? 30 | return match[1] 31 | end 32 | 33 | def common 34 | "raspbian" 35 | end 36 | 37 | def cpe 38 | "o:raspbian:raspbian" 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/ssh_scan/os/redhat.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module OS 3 | class RedHat 4 | def common 5 | "redhat" 6 | end 7 | 8 | def cpe 9 | "o:redhat:redhat" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/os/ros.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module OS 3 | class ROS 4 | def common 5 | "ros" 6 | end 7 | 8 | def cpe 9 | "o:ros:ros" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/ssh_scan/os/ubuntu.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module OS 3 | class Ubuntu 4 | attr_reader :version 5 | 6 | class Version 7 | def initialize(version_string) 8 | @version_string = version_string 9 | end 10 | 11 | def to_s 12 | @version_string 13 | end 14 | end 15 | 16 | # Obtained from scraping ChangeLog on Launchpad 17 | FINGERPRINTS = { 18 | "4.10" => [ 19 | "3.8.1p1-11ubuntu3.3", 20 | "3.8.1p1-11ubuntu3.2", 21 | "3.8.1p1-11ubuntu3" 22 | ], 23 | "5.04" => [ 24 | "3.9p1-1ubuntu2.3", 25 | "3.9p1-1ubuntu2.2", 26 | "3.9p1-1ubuntu2.1", 27 | "3.9p1-1ubuntu2" 28 | ], 29 | "5.10" => [ 30 | "4.1p1-7ubuntu4.2", 31 | "4.1p1-7ubuntu4.1", 32 | "4.1p1-7ubuntu4" 33 | ], 34 | "6.04" => [ 35 | "4.2p1-7ubuntu3.5", 36 | "4.2p1-7ubuntu3.4", 37 | "4.2p1-7ubuntu3.3", 38 | "4.2p1-7ubuntu3.2", 39 | "4.2p1-7ubuntu3.1", 40 | "4.2p1-7ubuntu3", 41 | "4.2p1-7ubuntu2", 42 | "4.2p1-7ubuntu1", 43 | "4.2p1-5ubuntu2", 44 | "4.2p1-5ubuntu1" 45 | ], 46 | "6.10" => [ 47 | "4.3p2-5ubuntu1.2", 48 | "4.3p2-5ubuntu1.1", 49 | "4.3p2-5ubuntu1", 50 | "4.3p2-4ubuntu1", 51 | "4.3p2-2ubuntu5", 52 | "4.3p2-2ubuntu4", 53 | "4.3p2-2ubuntu3", 54 | "4.3p2-2ubuntu2", 55 | "4.3p2-2ubuntu1" 56 | ], 57 | "7.04" => [], 58 | "7.10" => [ 59 | "4.6p1-5ubuntu0.6", 60 | "4.6p1-5ubuntu0.5", 61 | "4.6p1-5ubuntu0.4", 62 | "4.6p1-5ubuntu0.3", 63 | "4.6p1-5ubuntu0.2", 64 | "4.6p1-5ubuntu0.1", 65 | "4.6p1-5build1", 66 | "4.3p2-10ubuntu1" 67 | ], 68 | "8.04" => [ 69 | "4.7p1-8ubuntu3", 70 | "4.7p1-8ubuntu2", 71 | "4.7p1-8ubuntu1.2", 72 | "4.7p1-8ubuntu1.1", 73 | "4.7p1-8ubuntu1", 74 | "4.7p1-7ubuntu1", 75 | "4.7p1-6ubuntu1", 76 | "4.7p1-5ubuntu1", 77 | "4.7p1-4ubuntu1" 78 | ], 79 | "8.10" => [ 80 | "5.1p1-3ubuntu1", 81 | "5.1p1-1ubuntu2", 82 | "5.1p1-1ubuntu1", 83 | "4.7p1-12ubuntu4", 84 | "4.7p1-12ubuntu3", 85 | "4.7p1-12ubuntu2", 86 | "4.7p1-12ubuntu1", 87 | "4.7p1-10ubuntu1", 88 | "4.7p1-9ubuntu1" 89 | ], 90 | "9.04" => [ 91 | "5.1p1-5ubuntu1", 92 | "5.1p1-4ubuntu1" 93 | ], 94 | "9.10" => [ 95 | "5.1p1-6ubuntu2", 96 | "5.1p1-6ubuntu1", 97 | "5.1p1-5ubuntu2" 98 | ], 99 | "10.04" => [ 100 | "5.3p1-3ubuntu7.1", 101 | "5.3p1-3ubuntu7", 102 | "5.3p1-3ubuntu6", 103 | "5.3p1-3ubuntu5", 104 | "5.3p1-3ubuntu4", 105 | "5.3p1-3ubuntu3", 106 | "5.3p1-3ubuntu2", 107 | "5.3p1-3ubuntu1", 108 | "5.3p1-1ubuntu2", 109 | "5.3p1-1ubuntu1", 110 | "5.2p1-2ubuntu1", 111 | "5.2p1-1ubuntu1", 112 | "5.1p1-8ubuntu2", 113 | "5.1p1-8ubuntu1" 114 | ], 115 | "10.10" => [ 116 | "5.5p1-4ubuntu6", 117 | "5.5p1-4ubuntu5", 118 | "5.5p1-4ubuntu4", 119 | "5.5p1-4ubuntu3", 120 | "5.5p1-4ubuntu2", 121 | "5.5p1-4ubuntu1", 122 | "5.5p1-3ubuntu1" 123 | ], 124 | "11.04" => [ 125 | "5.8p1-1ubuntu3", 126 | "5.8p1-1ubuntu2", 127 | "5.8p1-1ubuntu1", 128 | "5.7p1-2ubuntu1", 129 | "5.7p1-1ubuntu1", 130 | "5.6p1-2ubuntu4", 131 | "5.6p1-2ubuntu3", 132 | "5.6p1-2ubuntu2", 133 | "5.6p1-2ubuntu1", 134 | "5.6p1-1ubuntu1" 135 | ], 136 | "11.10" => [ 137 | "5.8p1-7ubuntu1", 138 | "5.8p1-4ubuntu2", 139 | "5.8p1-4ubuntu1" 140 | ], 141 | "12.04" => [ 142 | "5.9p1-5ubuntu1.10", 143 | "5.9p1-5ubuntu1.9", 144 | "5.9p1-5ubuntu1.8", 145 | "5.9p1-5ubuntu1.7", 146 | "5.9p1-5ubuntu1.6", 147 | "5.9p1-5ubuntu1.4", 148 | "5.9p1-5ubuntu1.3", 149 | "5.9p1-5ubuntu1.2", 150 | "5.9p1-5ubuntu1.1", 151 | "5.9p1-5ubuntu1", 152 | "5.9p1-4ubuntu1", 153 | "5.9p1-3ubuntu1", 154 | "5.9p1-2ubuntu2", 155 | "5.9p1-2ubuntu1", 156 | "5.9p1-1ubuntu1" 157 | ], 158 | "12.10" => [ 159 | "6.0p1-3ubuntu1.2", 160 | "6.0p1-3ubuntu1.1", 161 | "6.0p1-3ubuntu1", 162 | "6.0p1-2ubuntu1", 163 | "6.0p1-1ubuntu1" 164 | ], 165 | "13.04" => [ 166 | "6.1p1-1ubuntu1" 167 | ], 168 | "13.10" => [ 169 | "6.2p2-6ubuntu0.5", 170 | "6.2p2-6ubuntu0.4", 171 | "6.2p2-6ubuntu0.3", 172 | "6.2p2-6ubuntu0.2", 173 | "6.2p2-6ubuntu0.1" 174 | ], 175 | "14.04" => [ 176 | "6.6p1-2ubuntu2.8", 177 | "6.6p1-2ubuntu2.7", 178 | "6.6p1-2ubuntu2.6", 179 | "6.6p1-2ubuntu2.5", 180 | "6.6p1-2ubuntu2.4", 181 | "6.6p1-2ubuntu2.3", 182 | "6.6p1-2ubuntu2.2", 183 | "6.6p1-2ubuntu2", 184 | "6.6p1-2ubuntu1", 185 | "6.2p2-6ubuntu1", 186 | "6.6.1p1 Ubuntu-8" 187 | ], 188 | "14.10" => [ 189 | "6.6p1-5build1" 190 | ], 191 | "15.04" => [ 192 | "6.7p1-5ubuntu1.4", 193 | "6.7p1-5ubuntu1.3", 194 | "6.7p1-5ubuntu1.2", 195 | "6.7p1-5ubuntu1" 196 | ], 197 | "15.10" => [ 198 | "6.9p1-2ubuntu0.2", 199 | "6.9p1-2ubuntu0.1", 200 | "6.7p1-6ubuntu2", 201 | "6.7p1-6ubuntu1" 202 | ], 203 | "16.04" => [ 204 | "7.2p2-4ubuntu2.1", 205 | "7.2p2-4ubuntu2", 206 | "7.2p2-4ubuntu1" 207 | ], 208 | "16.10" => [] 209 | } 210 | 211 | def initialize(banner) 212 | @banner = banner 213 | @version = Ubuntu::Version.new(ubuntu_version_guess) 214 | end 215 | 216 | def common 217 | "ubuntu" 218 | end 219 | 220 | # Get the FINGERPRINTS constant hash, generated from the 221 | # scraping script. 222 | # @return [Hash>] FINGERPRINTS constant 223 | # hash 224 | def fingerprints 225 | OS::Ubuntu::FINGERPRINTS 226 | end 227 | 228 | def ubuntu_version_guess 229 | possible_versions = [] 230 | OS::Ubuntu::FINGERPRINTS.keys.each do |ubuntu_version| 231 | OS::Ubuntu::FINGERPRINTS[ubuntu_version].uniq.each do |banner| 232 | openssh_ps, ubuntu_sig = banner.split("-") 233 | openssh_version = openssh_ps 234 | # If the version is like 6.6p1, deduce that 235 | if openssh_ps.include?("p") 236 | openssh_version = openssh_ps.split("p")[0] 237 | end 238 | if @banner.include?("OpenSSH_#{openssh_version}") && 239 | @banner.include?(ubuntu_sig) 240 | possible_versions << ubuntu_version 241 | end 242 | end 243 | end 244 | possible_versions.uniq! 245 | if possible_versions.any? 246 | return possible_versions.join("|") 247 | end 248 | return nil 249 | end 250 | 251 | def cpe 252 | "o:canonical:ubuntu" + (@version.to_s ? ":#{@version}" : "") 253 | end 254 | end 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /lib/ssh_scan/os/unknown.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module OS 3 | class Unknown 4 | def common 5 | "unknown" 6 | end 7 | 8 | def cpe 9 | "o:unknown" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/os/windows.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module OS 3 | class Windows 4 | def common 5 | "windows" 6 | end 7 | 8 | def cpe 9 | "o:microsoft:windows" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/policy.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'ssh_scan/attribute' 3 | 4 | module SSHScan 5 | # Policy methods that deal with key exchange, macs, encryption methods, 6 | # compression methods and more. 7 | class Policy 8 | attr_reader :name, :kex, :macs, :encryption, :compression, 9 | :references, :auth_methods, :ssh_version 10 | 11 | def initialize(opts = {}) 12 | @name = opts['name'] || [] 13 | @kex = opts['kex'] || [] 14 | @macs = opts['macs'] || [] 15 | @encryption = opts['encryption'] || [] 16 | @compression = opts['compression'] || [] 17 | @references = opts['references'] || [] 18 | @auth_methods = opts['auth_methods'] || [] 19 | @ssh_version = opts['ssh_version'] || nil 20 | end 21 | 22 | # Generate a {SSHScan::Policy} object from YAML file. 23 | # @param file [String] filepath 24 | # @return [SSHScan::Policy] new instance with parameters loaded 25 | # from YAML file 26 | def self.from_file(file) 27 | opts = YAML.load_file(file) 28 | self.new(opts) 29 | end 30 | 31 | def kex_attributes 32 | SSHScan.make_attributes(@kex) 33 | end 34 | 35 | def mac_attributes 36 | SSHScan.make_attributes(@macs) 37 | end 38 | 39 | def encryption_attributes 40 | SSHScan.make_attributes(@encryption) 41 | end 42 | 43 | def compression_attributes 44 | SSHScan.make_attributes(@compression) 45 | end 46 | 47 | # Generate a {SSHScan::Policy} object from YAML string. 48 | # @param string [String] YAML string 49 | # @return [SSHScan::Policy] new instance with parameters loaded 50 | # from given string 51 | def self.from_string(string) 52 | opts = YAML.load(string) 53 | self.new(opts) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/ssh_scan/policy_manager.rb: -------------------------------------------------------------------------------- 1 | require 'ssh_scan/attribute' 2 | 3 | module SSHScan 4 | # Policy management methods, compliance checking and recommendations. 5 | class PolicyManager 6 | def initialize(result, policy) 7 | @policy = policy 8 | @result = result 9 | end 10 | 11 | def out_of_policy_encryption 12 | return [] if @policy.encryption.empty? 13 | target_encryption = 14 | @result.encryption_algorithms_client_to_server | 15 | @result.encryption_algorithms_server_to_client 16 | outliers = [] 17 | target_encryption.each do |target_enc| 18 | outliers << target_enc unless @policy.encryption_attributes.include?(SSHScan::Attribute.new(target_enc)) 19 | end 20 | return outliers 21 | end 22 | 23 | def missing_policy_encryption 24 | return [] if @policy.encryption.empty? 25 | target_encryption = 26 | @result.encryption_algorithms_client_to_server | 27 | @result.encryption_algorithms_server_to_client 28 | outliers = [] 29 | @policy.encryption.each do |encryption| 30 | if SSHScan.make_attributes(target_encryption).include?(SSHScan::Attribute.new(encryption)) == false 31 | outliers << encryption 32 | end 33 | end 34 | return outliers 35 | end 36 | 37 | def out_of_policy_macs 38 | return [] if @policy.macs.empty? 39 | target_macs = 40 | @result.mac_algorithms_server_to_client | 41 | @result.mac_algorithms_client_to_server 42 | outliers = [] 43 | target_macs.each do |target_mac| 44 | outliers << target_mac unless @policy.mac_attributes.include?(SSHScan::Attribute.new(target_mac)) 45 | end 46 | return outliers 47 | end 48 | 49 | def missing_policy_macs 50 | return [] if @policy.macs.empty? 51 | target_macs = 52 | @result.mac_algorithms_server_to_client | 53 | @result.mac_algorithms_client_to_server 54 | outliers = [] 55 | 56 | @policy.macs.each do |mac| 57 | if SSHScan.make_attributes(target_macs).include?(SSHScan::Attribute.new(mac)) == false 58 | outliers << mac 59 | end 60 | end 61 | return outliers 62 | end 63 | 64 | def out_of_policy_kex 65 | return [] if @policy.kex.empty? 66 | target_kexs = @result.key_algorithms 67 | outliers = [] 68 | target_kexs.each do |target_kex| 69 | outliers << target_kex unless @policy.kex_attributes.include?(SSHScan::Attribute.new(target_kex)) 70 | end 71 | return outliers 72 | end 73 | 74 | def missing_policy_kex 75 | return [] if @policy.kex.empty? 76 | target_kex = @result.key_algorithms 77 | outliers = [] 78 | 79 | @policy.kex.each do |kex| 80 | if SSHScan.make_attributes(target_kex).include?(SSHScan::Attribute.new(kex)) == false 81 | outliers << kex 82 | end 83 | end 84 | return outliers 85 | end 86 | 87 | def out_of_policy_compression 88 | return [] if @policy.compression.empty? 89 | target_compressions = 90 | @result.compression_algorithms_server_to_client | 91 | @result.compression_algorithms_client_to_server 92 | outliers = [] 93 | target_compressions.each do |target_compression| 94 | outliers << target_compression unless 95 | @policy.compression_attributes.include?(SSHScan::Attribute.new(target_compression)) 96 | end 97 | return outliers 98 | end 99 | 100 | def missing_policy_compression 101 | return [] if @policy.compression.empty? 102 | target_compressions = 103 | @result.compression_algorithms_server_to_client | 104 | @result.compression_algorithms_client_to_server 105 | outliers = [] 106 | 107 | @policy.compression.each do |compression| 108 | if SSHScan.make_attributes(target_compressions).include?(SSHScan::Attribute.new(compression)) == false 109 | outliers << compression 110 | end 111 | end 112 | return outliers 113 | end 114 | 115 | def out_of_policy_auth_methods 116 | return [] if @policy.auth_methods.empty? 117 | return [] if @result.auth_methods.empty? 118 | target_auth_methods = @result.auth_methods 119 | outliers = [] 120 | 121 | if not @policy.auth_methods.empty? 122 | target_auth_methods.each do |auth_method| 123 | if not @policy.auth_methods.include?(auth_method) 124 | outliers << auth_method 125 | end 126 | end 127 | end 128 | return outliers 129 | end 130 | 131 | def out_of_policy_ssh_version 132 | return false if @policy.ssh_version.nil? 133 | target_ssh_version = @result.ssh_version 134 | if @policy.ssh_version 135 | if target_ssh_version < @policy.ssh_version 136 | return true 137 | end 138 | end 139 | return false 140 | end 141 | 142 | def compliant? 143 | out_of_policy_encryption.empty? && 144 | out_of_policy_macs.empty? && 145 | out_of_policy_kex.empty? && 146 | out_of_policy_compression.empty? && 147 | missing_policy_encryption.empty? && 148 | missing_policy_macs.empty? && 149 | missing_policy_kex.empty? && 150 | missing_policy_compression.empty? && 151 | out_of_policy_auth_methods.empty? && 152 | !out_of_policy_ssh_version 153 | end 154 | 155 | def recommendations 156 | recommendations = [] 157 | 158 | # Add these items to be compliant 159 | if missing_policy_kex.any? 160 | recommendations << "Add these key exchange algorithms: \ 161 | #{missing_policy_kex.join(",")}" 162 | end 163 | 164 | if missing_policy_macs.any? 165 | recommendations << "Add these MAC algorithms: \ 166 | #{missing_policy_macs.join(",")}" 167 | end 168 | 169 | if missing_policy_encryption.any? 170 | recommendations << "Add these encryption ciphers: \ 171 | #{missing_policy_encryption.join(",")}" 172 | end 173 | 174 | if missing_policy_compression.any? 175 | recommendations << "Add these compression algorithms: \ 176 | #{missing_policy_compression.join(",")}" 177 | end 178 | 179 | # Remove these items to be compliant 180 | if out_of_policy_kex.any? 181 | recommendations << "Remove these key exchange algorithms: \ 182 | #{out_of_policy_kex.join(", ")}" 183 | end 184 | 185 | if out_of_policy_macs.any? 186 | recommendations << "Remove these MAC algorithms: \ 187 | #{out_of_policy_macs.join(", ")}" 188 | end 189 | 190 | if out_of_policy_encryption.any? 191 | recommendations << "Remove these encryption ciphers: \ 192 | #{out_of_policy_encryption.join(", ")}" 193 | end 194 | 195 | if out_of_policy_compression.any? 196 | recommendations << "Remove these compression algorithms: \ 197 | #{out_of_policy_compression.join(", ")}" 198 | end 199 | 200 | if out_of_policy_auth_methods.any? 201 | recommendations << "Remove these authentication methods: \ 202 | #{out_of_policy_auth_methods.join(", ")}" 203 | end 204 | 205 | # Update these items to be compliant 206 | if out_of_policy_ssh_version 207 | recommendations << "Update your ssh version to: #{@policy.ssh_version}" 208 | end 209 | 210 | return recommendations 211 | end 212 | 213 | def compliance_results 214 | { 215 | "policy" => @policy.name, 216 | "compliant" => compliant?, 217 | "recommendations" => recommendations, 218 | "references" => @policy.references, 219 | } 220 | end 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /lib/ssh_scan/public_key.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'sshkey' 3 | require 'base64' 4 | 5 | module SSHScan 6 | # All cryptography related methods. 7 | module Crypto 8 | # House methods helpful in analysing SSH public keys. 9 | class PublicKey 10 | def initialize(key_string) 11 | @key_string = key_string 12 | end 13 | 14 | def valid? 15 | SSHKey.valid_ssh_public_key?(@key_string) 16 | end 17 | 18 | def type 19 | if @key_string.start_with?("ssh-rsa") 20 | return "rsa" 21 | elsif @key_string.start_with?("ssh-dss") 22 | return "dsa" 23 | elsif @key_string.start_with?("ecdsa-sha2-nistp256") 24 | return "ecdsa-sha2-nistp256" 25 | elsif @key_string.start_with?("ssh-ed25519") 26 | return "ed25519" 27 | else 28 | return "unknown" 29 | end 30 | end 31 | 32 | def length 33 | SSHKey.ssh_public_key_bits(@key_string) 34 | end 35 | 36 | def fingerprint_md5 37 | SSHKey.fingerprint(@key_string) 38 | end 39 | 40 | def fingerprint_sha1 41 | SSHKey.sha1_fingerprint(@key_string) 42 | end 43 | 44 | def fingerprint_sha256 45 | # We're translating this to hex because the SSHKEY default isn't as useful for comparing with SSHFP records 46 | Base64.decode64(SSHKey.sha256_fingerprint(@key_string)).hexify(:delim => ":") 47 | end 48 | 49 | def to_hash 50 | { 51 | self.type => { 52 | "raw" => @key_string, 53 | "length" => self.length, 54 | "fingerprints" => { 55 | "md5" => self.fingerprint_md5, 56 | "sha1" => self.fingerprint_sha1, 57 | "sha256" => self.fingerprint_sha256 58 | } 59 | } 60 | } 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/ssh_scan/result.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'ssh_scan/banner' 3 | require 'ipaddr' 4 | require 'string_ext' 5 | require 'set' 6 | 7 | module SSHScan 8 | class Result 9 | def initialize() 10 | @version = SSHScan::VERSION 11 | @keys = nil 12 | @duplicate_host_key_ips = Set.new() 13 | @compliance = {} 14 | end 15 | 16 | def version 17 | @version 18 | end 19 | 20 | def ip 21 | @ip 22 | end 23 | 24 | def ip=(ip) 25 | unless ip.is_a?(String) && ip.ip_addr? 26 | raise ArgumentError, "Invalid attempt to set IP to a non-IP address value" 27 | end 28 | 29 | @ip = ip 30 | end 31 | 32 | def port 33 | @port 34 | end 35 | 36 | def port=(port) 37 | unless port.is_a?(Integer) && port > 0 && port <= 65535 38 | raise ArgumentError, "Invalid attempt to set port to a non-port value" 39 | end 40 | 41 | @port = port 42 | end 43 | 44 | def banner() 45 | @banner || SSHScan::Banner.new("") 46 | end 47 | 48 | def hostname=(hostname) 49 | @hostname = hostname 50 | end 51 | 52 | def hostname() 53 | @hostname || "" 54 | end 55 | 56 | def banner=(banner) 57 | unless banner.is_a?(SSHScan::Banner) 58 | raise ArgumentError, "Invalid attempt to set banner with a non-banner object" 59 | end 60 | 61 | @banner = banner 62 | end 63 | 64 | def ssh_version 65 | self.banner.ssh_version 66 | end 67 | 68 | def os_guess_common 69 | self.banner.os_guess.common 70 | end 71 | 72 | def os_guess_cpe 73 | self.banner.os_guess.cpe 74 | end 75 | 76 | def ssh_lib_guess_common 77 | self.banner.ssh_lib_guess.common 78 | end 79 | 80 | def ssh_lib_guess_cpe 81 | self.banner.ssh_lib_guess.cpe 82 | end 83 | 84 | def cookie 85 | @cookie || "" 86 | end 87 | 88 | def key_algorithms 89 | @hex_result_hash ? @hex_result_hash[:key_algorithms] : [] 90 | end 91 | 92 | def server_host_key_algorithms 93 | @hex_result_hash ? @hex_result_hash[:server_host_key_algorithms] : [] 94 | end 95 | 96 | def encryption_algorithms_client_to_server 97 | @hex_result_hash ? @hex_result_hash[:encryption_algorithms_client_to_server] : [] 98 | end 99 | 100 | def encryption_algorithms_server_to_client 101 | @hex_result_hash ? @hex_result_hash[:encryption_algorithms_server_to_client] : [] 102 | end 103 | 104 | def mac_algorithms_client_to_server 105 | @hex_result_hash ? @hex_result_hash[:mac_algorithms_client_to_server] : [] 106 | end 107 | 108 | def mac_algorithms_server_to_client 109 | @hex_result_hash ? @hex_result_hash[:mac_algorithms_server_to_client] : [] 110 | end 111 | 112 | def compression_algorithms_client_to_server 113 | @hex_result_hash ? @hex_result_hash[:compression_algorithms_client_to_server] : [] 114 | end 115 | 116 | def compression_algorithms_server_to_client 117 | @hex_result_hash ? @hex_result_hash[:compression_algorithms_server_to_client] : [] 118 | end 119 | 120 | def languages_client_to_server 121 | @hex_result_hash ? @hex_result_hash[:languages_client_to_server] : [] 122 | end 123 | 124 | def languages_server_to_client 125 | @hex_result_hash ? @hex_result_hash[:languages_server_to_client] : [] 126 | end 127 | 128 | def set_kex_result(kex_result) 129 | @hex_result_hash = kex_result.to_hash 130 | end 131 | 132 | def set_start_time 133 | @start_time = Time.now 134 | end 135 | 136 | def start_time 137 | @start_time 138 | end 139 | 140 | def set_end_time 141 | @end_time = Time.now 142 | end 143 | 144 | def scan_duration 145 | if start_time.nil? || end_time.nil? 146 | return nil 147 | end 148 | 149 | end_time - start_time 150 | end 151 | 152 | def end_time 153 | @end_time 154 | end 155 | 156 | def auth_methods=(auth_methods) 157 | @auth_methods = auth_methods 158 | end 159 | 160 | def keys 161 | @keys || {} 162 | end 163 | 164 | def keys=(keys) 165 | @keys = keys 166 | end 167 | 168 | def dns_keys 169 | @dns_keys 170 | end 171 | 172 | def dns_keys=(dns_keys) 173 | @dns_keys = dns_keys 174 | end 175 | 176 | def duplicate_host_key_ips=(duplicate_host_key_ips) 177 | @duplicate_host_key_ips = duplicate_host_key_ips 178 | end 179 | 180 | def duplicate_host_key_ips 181 | @duplicate_host_key_ips.to_a 182 | end 183 | 184 | def auth_methods() 185 | @auth_methods || [] 186 | end 187 | 188 | def set_compliance=(compliance) 189 | @compliance = compliance 190 | end 191 | 192 | def compliance_policy 193 | @compliance["policy"] 194 | end 195 | 196 | def compliant? 197 | @compliance["compliant"] 198 | end 199 | 200 | def compliance_references 201 | @compliance["references"] 202 | end 203 | 204 | def compliance_recommendations 205 | @compliance["recommendations"] 206 | end 207 | 208 | def set_client_attributes(client) 209 | self.ip = client.ip 210 | self.port = client.port || 22 211 | self.banner = client.banner || SSHScan::Banner.new("") 212 | end 213 | 214 | def error=(error) 215 | @error = error.to_s 216 | end 217 | 218 | def unset_error 219 | @error = nil 220 | end 221 | 222 | def error? 223 | !@error.nil? 224 | end 225 | 226 | def error 227 | @error 228 | end 229 | 230 | def grade=(grade) 231 | @compliance["grade"] = grade 232 | end 233 | 234 | def grade 235 | @compliance["grade"] 236 | end 237 | 238 | def to_hash 239 | hashed_object = { 240 | "ssh_scan_version" => self.version, 241 | "ip" => self.ip, 242 | "hostname" => self.hostname, 243 | "port" => self.port, 244 | "server_banner" => self.banner.to_s, 245 | "ssh_version" => self.ssh_version, 246 | "os" => self.os_guess_common, 247 | "os_cpe" => self.os_guess_cpe, 248 | "ssh_lib" => self.ssh_lib_guess_common, 249 | "ssh_lib_cpe" => self.ssh_lib_guess_cpe, 250 | "key_algorithms" => self.key_algorithms, 251 | "encryption_algorithms_client_to_server" => self.encryption_algorithms_client_to_server, 252 | "encryption_algorithms_server_to_client" => self.encryption_algorithms_server_to_client, 253 | "mac_algorithms_client_to_server" => self.mac_algorithms_client_to_server, 254 | "mac_algorithms_server_to_client" => self.mac_algorithms_server_to_client, 255 | "compression_algorithms_client_to_server" => self.compression_algorithms_client_to_server, 256 | "compression_algorithms_server_to_client" => self.compression_algorithms_server_to_client, 257 | "languages_client_to_server" => self.languages_client_to_server, 258 | "languages_server_to_client" => self.languages_server_to_client, 259 | "auth_methods" => self.auth_methods, 260 | "keys" => self.keys, 261 | "dns_keys" => self.dns_keys, 262 | "duplicate_host_key_ips" => self.duplicate_host_key_ips.uniq, 263 | "compliance" => @compliance, 264 | "start_time" => self.start_time, 265 | "end_time" => self.end_time, 266 | "scan_duration_seconds" => self.scan_duration, 267 | } 268 | 269 | if self.error? 270 | hashed_object["error"] = self.error 271 | end 272 | 273 | hashed_object 274 | end 275 | 276 | def to_json 277 | self.to_hash.to_json 278 | end 279 | end 280 | end -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_fp.rb: -------------------------------------------------------------------------------- 1 | require 'resolv' 2 | 3 | module SSHScan 4 | class SshFp 5 | 6 | ALGO_MAP = { 7 | 0 => "reserved", # Reference: https://tools.ietf.org/html/rfc4255#section-2.4 8 | 1 => "rsa", # Reference: https://tools.ietf.org/html/rfc4255#section-2.4 9 | 2 => "dss", # Reference: https://tools.ietf.org/html/rfc4255#section-2.4 10 | 3 => "ecdsa", # Reference: https://tools.ietf.org/html/rfc6594#section-5.3.1 11 | 4 => "ed25519" # Reference: https://tools.ietf.org/html/rfc7479 12 | } 13 | 14 | 15 | FPTYPE_MAP = { 16 | 0 => "reserved", # Reference: https://tools.ietf.org/html/rfc4255#section-2.4 17 | 1 => "sha1", # Reference: https://tools.ietf.org/html/rfc4255#section-2.4 18 | 2 => "sha256" # Reference: https://tools.ietf.org/html/rfc6594#section-5.1.2 19 | } 20 | 21 | 22 | def query(fqdn) 23 | sshfp_records = [] 24 | 25 | # try up to 5 times to resolve ssh_fp's 26 | 5.times do 27 | 28 | # Reference: https://stackoverflow.com/questions/28867626/how-to-use-resolvdnsresourcegeneric 29 | # Note: this includes some fixes too, I'll post a direct link back to the SO article. 30 | Resolv::DNS.open do |dns| 31 | all_records = dns.getresources(fqdn, Resolv::DNS::Resource::IN::ANY ) rescue nil 32 | all_records.each do |rr| 33 | if rr.is_a? Resolv::DNS::Resource::Generic then 34 | classname = rr.class.name.split('::').last 35 | if classname == "Type44_Class1" 36 | data = rr.data.bytes 37 | algo = data[0].to_s 38 | fptype = data[1].to_s 39 | fp = data[2..-1] 40 | hex = fp.map{|b| b.to_s(16).rjust(2,'0') }.join(':') 41 | sshfp_records << {"fptype" => FPTYPE_MAP[fptype.to_i], "algo" => ALGO_MAP[algo.to_i], "hex" => hex} 42 | end 43 | end 44 | end 45 | end 46 | 47 | if sshfp_records.any? 48 | return sshfp_records.sort_by { |k| k["hex"] } 49 | end 50 | 51 | sleep 0.5 52 | end 53 | 54 | return sshfp_records 55 | end 56 | end 57 | end -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib.rb: -------------------------------------------------------------------------------- 1 | require 'ssh_scan/ssh_lib/openssh' 2 | require 'ssh_scan/ssh_lib/libssh' 3 | require 'ssh_scan/ssh_lib/ciscossh' 4 | require 'ssh_scan/ssh_lib/rosssh' 5 | require 'ssh_scan/ssh_lib/doprassh' 6 | require 'ssh_scan/ssh_lib/dropbear' 7 | require 'ssh_scan/ssh_lib/romsshell' 8 | require 'ssh_scan/ssh_lib/flowssh' 9 | require 'ssh_scan/ssh_lib/cryptlib' 10 | require 'ssh_scan/ssh_lib/mpssh' 11 | require 'ssh_scan/ssh_lib/sentryssh' 12 | require 'ssh_scan/ssh_lib/ipssh' 13 | require 'ssh_scan/ssh_lib/pgp' 14 | require 'ssh_scan/ssh_lib/nosssh' 15 | require 'ssh_scan/ssh_lib/unknown' 16 | -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/ciscossh.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class CiscoSSH 4 | def common 5 | "ciscossh" 6 | end 7 | 8 | def cpe 9 | "a:cisco:ciscossh" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/cryptlib.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class Cryptlib 4 | def common 5 | "cryptlib" 6 | end 7 | 8 | def cpe 9 | "a:cryptlib:cryptlib" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/doprassh.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class DOPRASSH 4 | def common 5 | "doprassh" 6 | end 7 | 8 | def cpe 9 | "a:doprassh:doprassh" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/dropbear.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class Dropbear 4 | attr_reader :version 5 | 6 | class Version 7 | def initialize(version_string) 8 | if version_string == nil 9 | @version_string = "unknown" 10 | else 11 | @version_string = version_string 12 | end 13 | end 14 | def to_s 15 | @version_string 16 | end 17 | end 18 | 19 | def initialize(banner) 20 | @banner = banner 21 | @version = Dropbear::Version.new(dropbear_version_guess) 22 | end 23 | 24 | def dropbear_version_guess 25 | return nil if @banner.nil? 26 | match = @banner.match(/SSH-2.0-dropbear_(\d+.\d+(?:.\d)?(?:test(:?\d)?)?)/) 27 | return nil if match.nil? 28 | return match[1] 29 | end 30 | 31 | def common 32 | "dropbear" 33 | end 34 | 35 | def cpe 36 | "a:dropbear:dropbear" << (":" + version.to_s) unless version.nil? 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/flowssh.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class FlowSsh 4 | class Version 5 | def initialize(version_string) 6 | @version_string = version_string 7 | end 8 | 9 | def to_s 10 | @version_string 11 | end 12 | end 13 | 14 | def initialize(banner = nil) 15 | @banner = banner 16 | end 17 | 18 | def version() 19 | return nil if @banner.nil? 20 | match = @banner.match(/(\d+[\.\d+]+(p)?(\d+)?) FlowSsh/) 21 | return nil if match.nil? 22 | return FlowSsh::Version.new(match[1]) 23 | end 24 | 25 | def common 26 | "flowssh" 27 | end 28 | 29 | def cpe 30 | "a:bitvise:flowssh" << (":" + version.to_s) unless version.nil? 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/ipssh.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class IpSsh 4 | class Version 5 | def initialize(version_string) 6 | @version_string = version_string 7 | end 8 | 9 | def to_s 10 | @version_string 11 | end 12 | end 13 | 14 | def initialize(banner = nil) 15 | @banner = banner 16 | end 17 | 18 | def version() 19 | return nil if @banner.nil? 20 | match = @banner.match(/IPSSH-(\d+[\.\d+]+(p)?(\d+)?)/) 21 | return nil if match.nil? 22 | return IpSsh::Version.new(match[1]) 23 | end 24 | 25 | def common 26 | "ipssh" 27 | end 28 | 29 | def cpe 30 | "a:ipssh:ipssh" << (":" + version.to_s) unless version.nil? 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/libssh.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class LibSSH 4 | def common 5 | "libssh" 6 | end 7 | 8 | def cpe 9 | "a:libssh:libssh" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/mpssh.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class Mpssh 4 | class Version 5 | def initialize(version_string) 6 | @version_string = version_string 7 | end 8 | 9 | def to_s 10 | @version_string 11 | end 12 | end 13 | 14 | def initialize(banner = nil) 15 | @banner = banner 16 | end 17 | 18 | def version() 19 | return nil if @banner.nil? 20 | match = @banner.match(/mpSSH_(\d+[\.\d+]+(p)?(\d+)?)/i) 21 | return nil if match.nil? 22 | return Mpssh::Version.new(match[1]) 23 | end 24 | 25 | def common 26 | "mpssh" 27 | end 28 | 29 | def cpe 30 | "a:mpssh:mpssh" << (":" + version.to_s) unless version.nil? 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/nosssh.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class NosSSH 4 | class Version 5 | def initialize(version_string) 6 | @version_string = version_string 7 | end 8 | 9 | def to_s 10 | @version_string 11 | end 12 | end 13 | 14 | def initialize(banner = nil) 15 | @banner = banner 16 | end 17 | 18 | def version() 19 | return nil if @banner.nil? 20 | match = @banner.match(/NOS-SSH_(\d+[\.\d+]+)/) 21 | return nil if match.nil? 22 | return NosSSH::Version.new(match[1]) 23 | end 24 | 25 | def common 26 | "nosssh" 27 | end 28 | 29 | def cpe 30 | "a:nosssh:nosssh" << (":" + version.to_s) unless version.nil? 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/openssh.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class OpenSSH 4 | class Version 5 | def initialize(version_string) 6 | @version_string = version_string 7 | end 8 | 9 | def to_s 10 | @version_string 11 | end 12 | end 13 | 14 | def initialize(banner = nil) 15 | @banner = banner 16 | end 17 | 18 | def version() 19 | return nil if @banner.nil? 20 | match = @banner.match(/OpenSSH_(\d+[\.\d+]+(p)?(\d+)?)/) 21 | return nil if match.nil? 22 | return OpenSSH::Version.new(match[1]) 23 | end 24 | 25 | def common 26 | "openssh" 27 | end 28 | 29 | def cpe 30 | "a:openssh:openssh" << (":" + version.to_s) unless version.nil? 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/pgp.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class PGP 4 | def common 5 | "pgp" 6 | end 7 | 8 | def cpe 9 | "a:pgp:pgp" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/romsshell.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class RomSShell 4 | class Version 5 | def initialize(version_string) 6 | @version_string = version_string 7 | end 8 | 9 | def to_s 10 | @version_string 11 | end 12 | end 13 | 14 | def initialize(banner = nil) 15 | @banner = banner 16 | end 17 | 18 | def version() 19 | return nil if @banner.nil? 20 | match = @banner.match(/RomSShell_(\d+[\.\d+]+(p)?(\d+)?)/) 21 | return nil if match.nil? 22 | return RomSShell::Version.new(match[1]) 23 | end 24 | 25 | def common 26 | "romsshell" 27 | end 28 | 29 | def cpe 30 | "a:allegrosoft:romsshell" << (":" + version.to_s) unless version.nil? 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/rosssh.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class ROSSSH 4 | def common 5 | "rosssh" 6 | end 7 | 8 | def cpe 9 | "a:rosssh:rosssh" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/sentryssh.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class SentrySSH 4 | def common 5 | "sentryssh" 6 | end 7 | 8 | def cpe 9 | "a:servertech:sentryssh" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/ssh_lib/unknown.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | module SSHLib 3 | class Unknown 4 | def common 5 | "unknown" 6 | end 7 | 8 | def cpe 9 | "a:unknown" 10 | end 11 | 12 | def version 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ssh_scan/subprocess.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | 3 | module Utils 4 | class Subprocess 5 | def initialize(cmd, &block) 6 | # see: http://stackoverflow.com/a/1162850/83386 7 | Open3.popen3(cmd) do |stdin, stdout, stderr, thread| 8 | # read each stream from a new thread 9 | { :out => stdout, :err => stderr }.each do |key, stream| 10 | Thread.new do 11 | until (line = stream.gets).nil? do 12 | # yield the block depending on the stream 13 | if key == :out 14 | yield line, nil, thread if block_given? 15 | else 16 | yield nil, line, thread if block_given? 17 | end 18 | end 19 | end 20 | end 21 | 22 | thread.join # don't exit until the external process is done 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/ssh_scan/target_parser.rb: -------------------------------------------------------------------------------- 1 | require 'netaddr' 2 | require 'string_ext' 3 | 4 | module SSHScan 5 | # Enumeration methods for IP notations. 6 | class TargetParser 7 | # Enumerate CIDR addresses, single IPs and IP ranges. 8 | # @param ip [String] IP address 9 | # @param port [Fixnum] port 10 | # @return [Array] array of enumerated addresses 11 | def enumerateIPRange(ip,port=nil) 12 | if ip.fqdn? 13 | if port.nil? 14 | socket = ip 15 | else 16 | socket = ip.concat(":").concat(port.to_s) 17 | end 18 | return [socket] 19 | else 20 | if ip.include? "/" 21 | begin 22 | ip_net = NetAddr::IPv4Net.parse(ip) 23 | rescue 24 | raise ArgumentError, "Invalid target: #{ip}" 25 | end 26 | 27 | sock_array = [] 28 | 1.upto(ip_net.len - 2) do |i| 29 | sock_array << ip_net.nth(i).to_s 30 | end 31 | 32 | if !port.nil? 33 | sock_array.map! { |i| i.concat(":").concat(port.to_s) } 34 | end 35 | return sock_array 36 | else 37 | if port.nil? 38 | socket = ip 39 | else 40 | socket = ip.concat(":").concat(port.to_s) 41 | end 42 | return [socket] 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/ssh_scan/update.rb: -------------------------------------------------------------------------------- 1 | require 'ssh_scan/os' 2 | require 'ssh_scan/ssh_lib' 3 | require 'ssh_scan/version' 4 | require 'net/http' 5 | 6 | module SSHScan 7 | # Handle {SSHScan} updates. 8 | class Update 9 | def initialize 10 | @errors = [] 11 | end 12 | 13 | def next_patch_version(version = SSHScan::VERSION) 14 | major, minor, patch = version.split(".") 15 | patch_num = patch.to_i 16 | patch_num += 1 17 | 18 | return [major, minor, patch_num.to_s].join(".") 19 | end 20 | 21 | def next_minor_version(version = SSHScan::VERSION) 22 | major, minor = version.split(".")[0, 2] 23 | minor_num = minor.to_i 24 | minor_num += 1 25 | 26 | return [major, minor_num.to_s, "0"].join(".") 27 | end 28 | 29 | def next_major_version(version = SSHScan::VERSION) 30 | major = version.split(".")[0] 31 | major_num = major.to_i 32 | major_num += 1 33 | 34 | return [major_num.to_s, "0", "0"].join(".") 35 | end 36 | 37 | # Returns true if the given gem version exists. 38 | # @param version [String] version string 39 | # @return [Boolean] true if given gem exists, else false 40 | def gem_exists?(version = SSHScan::VERSION) 41 | uri = URI("https://rubygems.org/gems/ssh_scan/versions/#{version}") 42 | 43 | begin 44 | res = Net::HTTP.get_response(uri) 45 | rescue SocketError => e 46 | @errors << e.message 47 | return false 48 | end 49 | 50 | if res.code != "200" 51 | return false 52 | else 53 | return true 54 | end 55 | end 56 | 57 | def errors 58 | @errors.uniq 59 | end 60 | 61 | # Tries to check if the next patch, minor or major version 62 | # is available or not. If so, returns true. 63 | # @param version [String] version string 64 | # @return [Boolean] true if next major/minor version available, 65 | # else false 66 | def newer_gem_available?(version = SSHScan::VERSION) 67 | if gem_exists?(next_patch_version(version)) 68 | return true 69 | end 70 | 71 | if gem_exists?(next_minor_version(version)) 72 | return true 73 | end 74 | 75 | if gem_exists?(next_major_version(version)) 76 | return true 77 | end 78 | 79 | return false 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/ssh_scan/version.rb: -------------------------------------------------------------------------------- 1 | module SSHScan 2 | VERSION = '0.0.44' 3 | end 4 | -------------------------------------------------------------------------------- /lib/string_ext.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddr' 2 | require 'resolv' 3 | require 'timeout' 4 | 5 | # Extend string to include some helpful stuff 6 | class String 7 | def unhexify 8 | [self].pack("H*") 9 | end 10 | 11 | def hexify 12 | self.each_byte.map { |b| b.to_s(16).rjust(2,'0') }.join 13 | end 14 | 15 | def ip_addr? 16 | begin 17 | IPAddr.new(self) 18 | 19 | # Using ArgumentError instead of IPAddr::InvalidAddressError 20 | # for 1.9.3 backward compatibility 21 | rescue ArgumentError 22 | return false 23 | end 24 | 25 | return true 26 | end 27 | 28 | def resolve_fqdn_as_ipv6(timeout = 3) 29 | begin 30 | Timeout::timeout(timeout) { 31 | Resolv::DNS.open do |dns| 32 | ress = dns.getresources self, Resolv::DNS::Resource::IN::AAAA 33 | temp = ress.map { |r| r.address } 34 | return temp[0] 35 | end 36 | } 37 | rescue Timeout::Error 38 | return "" 39 | end 40 | end 41 | 42 | def resolve_fqdn_as_ipv4(timeout = 3) 43 | begin 44 | Timeout::timeout(timeout) { 45 | Resolv::DNS.open do |dns| 46 | ress = dns.getresources self, Resolv::DNS::Resource::IN::A 47 | temp = ress.map { |r| r.address } 48 | return temp[0] 49 | end 50 | } 51 | rescue Timeout::Error 52 | return "" 53 | end 54 | 55 | end 56 | 57 | def resolve_fqdn 58 | begin 59 | IPSocket.getaddress(self) 60 | rescue SocketError 61 | nil # Can return anything you want here 62 | end 63 | end 64 | 65 | def resolve_ptr(timeout = 3) 66 | begin 67 | Timeout::timeout(timeout) { 68 | reversed_dns = Resolv.new.getname(self) 69 | return reversed_dns 70 | } 71 | rescue Timeout::Error,Resolv::ResolvError 72 | return "" 73 | end 74 | end 75 | 76 | # Stolen from: https://github.com/emonti/rbkb/blob/master/lib/rbkb/extends/string.rb 77 | def hexify(opts={}) 78 | delim = opts[:delim] 79 | pre = (opts[:prefix] || "") 80 | suf = (opts[:suffix] || "") 81 | 82 | if (rx=opts[:rx]) and not rx.kind_of? Regexp 83 | raise "rx must be a regular expression for a character class" 84 | end 85 | 86 | hx = [("0".."9").to_a, ("a".."f").to_a].flatten 87 | 88 | out=Array.new 89 | 90 | self.each_byte do |c| 91 | hc = if (rx and not rx.match c.chr) 92 | c.chr 93 | else 94 | pre + (hx[(c >> 4)] + hx[(c & 0xf )]) + suf 95 | end 96 | out << (hc) 97 | end 98 | out.join(delim) 99 | end 100 | 101 | def fqdn? 102 | begin 103 | resolve_fqdn 104 | rescue SocketError, Timeout::Error 105 | return false 106 | end 107 | 108 | if ip_addr? 109 | return false 110 | else 111 | return true 112 | end 113 | end 114 | 115 | end 116 | -------------------------------------------------------------------------------- /scripts/Ubuntu.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'pp' 4 | require 'net/http' 5 | 6 | urls = [ 7 | "https://launchpad.net/ubuntu/+source/openssh/+changelog", 8 | "https://launchpad.net/ubuntu/+source/openssh/+changelog\ 9 | ?batch=75&memo=75&start=75", 10 | "https://launchpad.net/ubuntu/+source/openssh/+changelog\ 11 | ?batch=75&memo=150&start=150" 12 | ] 13 | 14 | lines = [] 15 | urls.each do |url| 16 | lines += Net::HTTP.get(URI(url)).lines 17 | end 18 | 19 | codenames = { 20 | "4.10" => "warty", 21 | "5.04" => "hoary", 22 | "5.10" => "breezy", 23 | "6.04" => "dapper", 24 | "6.10" => "edgy", 25 | "7.04" => "feisty", 26 | "7.10" => "gutsy", 27 | "8.04" => "hardy", 28 | "8.10" => "intrepid", 29 | "9.04" => "jaunty", 30 | "9.10" => "karmic", 31 | "10.04" => "lucid", 32 | "10.10" => "maverick", 33 | "11.04" => "natty", 34 | "11.10" => "oneiric", 35 | "12.04" => "precise", 36 | "12.10" => "quantal", 37 | "13.04" => "raring", 38 | "13.10" => "saucy", 39 | "14.04" => "trusty", 40 | "14.10" => "utopic", 41 | "15.04" => "vivid", 42 | "15.10" => "wily", 43 | "16.04" => "xenial", 44 | "16.10" => "yakkety" 45 | } 46 | 47 | versions = {} 48 | codenames.keys.each do |key| 49 | versions[key] = [] 50 | end 51 | 52 | lines.each do |line| 53 | next if !line.include?("openssh (") 54 | fingerprint = line.strip.scan(/\(([^\)]+)\)/) 55 | versions.keys.each do |key| 56 | matches = 0 57 | if line.include?(codenames[key]) 58 | fingerprint.each do |val| 59 | versions[key] << val.first.split(":")[1..-1].join(":") 60 | end 61 | matches += 1 62 | end 63 | end 64 | end 65 | 66 | pp versions 67 | -------------------------------------------------------------------------------- /spec/public_key_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require 'ssh_scan/public_key' 4 | 5 | describe SSHScan::Crypto::PublicKey do 6 | context "when parsing an RSA key string" do 7 | it "should parse it and have the right values for each attribute" do 8 | key_string = "ssh-rsa " + 9 | "AAAAB3NzaC1yc2EAAAADAQABAAABAQCl/BNLUxR49+3AKqhf6sWKr" + 10 | "h8XXzqXV00bEPFtcJFWxyRqC5pPWo9zRRiS2jitIcqljIQVohEEZH" + 11 | "t48vZaA1hniVfe/FmrFzuCOuQOIP2fuRgLSNHu+lWVScsHoX/MuYX" + 12 | "EIxj6aW7UpFn4lD01mvPtazXFO/tJ+LRs49YBP7UvL1smIS2xoyuH" + 13 | "7kZDN17QG08YwbIB2fApMl8rXH+2Rpj5hlv+7rcZ1dqCGtmXmvsv8" + 14 | "fKGYd7BxRy0s/d7EY4e/DeDxA1qTNV9BrBTNn6jAKIedTE5s4GNRb" + 15 | "N/Q20mP2qmw70PiTGROw6xp9SBFA7N9hjjOT7iutK/pa7y1joXKjeJ" 16 | key = SSHScan::Crypto::PublicKey.new(key_string) 17 | expect(key).to be_kind_of SSHScan::Crypto::PublicKey 18 | expect(key.valid?).to be true 19 | expect(key.type).to eq("rsa") 20 | expect(key.length).to be 2048 21 | expect(key.fingerprint_md5).to eq("fc:c5:5b:0d:f0:c6:fd:fe:80:18:62:2c:05:38:20:8a") 22 | expect(key.fingerprint_sha1).to eq("e1:3c:71:49:80:37:87:32:b5:0c:e3:86:41:ef:2e:2a:2f:14:e3:58") 23 | expect(key.fingerprint_sha256).to eq("68:7d:30:37:67:2c:eb:1e:4a:b5:ff:4f:bc:8c:e8:41:e1:55:a8:30:42:e0:8e:3c:e3:0a:ba:bc:db:fd:5c:50") 24 | expect(key.to_hash).to eq( 25 | { 26 | "rsa" => { 27 | "fingerprints" => { 28 | "md5"=>"fc:c5:5b:0d:f0:c6:fd:fe:80:18:62:2c:05:38:20:8a", 29 | "sha1"=>"e1:3c:71:49:80:37:87:32:b5:0c:e3:86:41:ef:2e:2a:2f:14:e3:58", 30 | "sha256"=>"68:7d:30:37:67:2c:eb:1e:4a:b5:ff:4f:bc:8c:e8:41:e1:55:a8:30:42:e0:8e:3c:e3:0a:ba:bc:db:fd:5c:50" 31 | }, 32 | "length" => 2048, 33 | "raw" => "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCl/BNLUxR49+3AKqhf6sWKrh8XXzqXV00bEPFtcJFWxyRqC5pPWo9zRRiS2jitIcqljIQVohEEZHt48vZaA1hniVfe/FmrFzuCOuQOIP2fuRgLSNHu+lWVScsHoX/MuYXEIxj6aW7UpFn4lD01mvPtazXFO/tJ+LRs49YBP7UvL1smIS2xoyuH7kZDN17QG08YwbIB2fApMl8rXH+2Rpj5hlv+7rcZ1dqCGtmXmvsv8fKGYd7BxRy0s/d7EY4e/DeDxA1qTNV9BrBTNn6jAKIedTE5s4GNRbN/Q20mP2qmw70PiTGROw6xp9SBFA7N9hjjOT7iutK/pa7y1joXKjeJ", 34 | } 35 | } 36 | ) 37 | end 38 | end 39 | 40 | context "when parsing an DSA key string" do 41 | it "should parse it and have the right values for each attribute" do 42 | key_string = "ssh-dss " + 43 | "AAAAB3NzaC1kc3MAAACBAOXOC6kuB7xDMgHS79KFQITNeAT9tMKd2oK1" + 44 | "c6bQEHRgTSMP3sWZ1cntWVFKl5u6MEuEBBT9PZKWsy7vRE525Wwt+NbR" + 45 | "IBso3vYFF1MtxZKpAsF+gbGI7y+aZcIceXrHkkY2bz3oGb9I9MZ2DSu2" + 46 | "9crW11YHCmuOJ2FJiDcx7dV9AAAAFQC+Ws9e0KJaAsN8cj75DbTQumrd" + 47 | "JQAAAIBjn5EA5JvQg7xu8TRcNmZWhuyBLoOZczU6nk2h4i+x4pbpVMVr" + 48 | "Ch5Lr8wsH60w7IW4yKg6JvPlzmQW0ZRZAwnU9sC3YO64H1RFQg8tnmRr" + 49 | "w0I9oi6wKPEe5rLgbdr9jYHePs9tiV+ZFfUKmXh0s7srr/dwmX/gHCPI" + 50 | "whLEVa+dLQAAAIEAn/+dSyf6KXdfKNyx9MYc1l2/2YUhVuxClF26PNQX" + 51 | "0CZhcSoDyUXU/eAqaS7S6EYqtM/8FK1OZY1tzM5Nm4GWY2LLF22Q2YkK" + 52 | "ItkhfS3GaD5JeuTQ+HK0F+wQjmpqt2pUulVQXQAjvE1qoRFQ4/yeVrvh" + 53 | "VqCzFICnariQP7tMYEo=" 54 | key = SSHScan::Crypto::PublicKey.new(key_string) 55 | expect(key).to be_kind_of SSHScan::Crypto::PublicKey 56 | expect(key.valid?).to be true 57 | expect(key.type).to eq("dsa") 58 | expect(key.length).to be 1024 59 | expect(key.fingerprint_md5).to eq("6b:5f:8d:57:be:2e:55:7f:e3:d7:15:d1:66:17:d8:8c") 60 | expect(key.fingerprint_sha1).to eq("49:84:7f:d7:9d:84:2a:20:61:72:10:3f:2c:b1:16:9b:12:5b:e7:07") 61 | expect(key.fingerprint_sha256).to eq("b1:66:73:82:b1:b1:ce:cf:da:32:67:14:db:0e:85:c8:44:ff:22:28:5d:e8:72:f5:a8:dc:83:66:73:b4:34:3c") 62 | expect(key.to_hash).to eq( 63 | { 64 | "dsa" => { 65 | "fingerprints" => { 66 | "md5"=>"6b:5f:8d:57:be:2e:55:7f:e3:d7:15:d1:66:17:d8:8c", 67 | "sha1"=>"49:84:7f:d7:9d:84:2a:20:61:72:10:3f:2c:b1:16:9b:12:5b:e7:07", 68 | "sha256"=>"b1:66:73:82:b1:b1:ce:cf:da:32:67:14:db:0e:85:c8:44:ff:22:28:5d:e8:72:f5:a8:dc:83:66:73:b4:34:3c" 69 | }, 70 | "length" => 1024, 71 | "raw" => "ssh-dss AAAAB3NzaC1kc3MAAACBAOXOC6kuB7xDMgHS79KFQITNeAT9tMKd2oK1c6bQEHRgTSMP3sWZ1cntWVFKl5u6MEuEBBT9PZKWsy7vRE525Wwt+NbRIBso3vYFF1MtxZKpAsF+gbGI7y+aZcIceXrHkkY2bz3oGb9I9MZ2DSu29crW11YHCmuOJ2FJiDcx7dV9AAAAFQC+Ws9e0KJaAsN8cj75DbTQumrdJQAAAIBjn5EA5JvQg7xu8TRcNmZWhuyBLoOZczU6nk2h4i+x4pbpVMVrCh5Lr8wsH60w7IW4yKg6JvPlzmQW0ZRZAwnU9sC3YO64H1RFQg8tnmRrw0I9oi6wKPEe5rLgbdr9jYHePs9tiV+ZFfUKmXh0s7srr/dwmX/gHCPIwhLEVa+dLQAAAIEAn/+dSyf6KXdfKNyx9MYc1l2/2YUhVuxClF26PNQX0CZhcSoDyUXU/eAqaS7S6EYqtM/8FK1OZY1tzM5Nm4GWY2LLF22Q2YkKItkhfS3GaD5JeuTQ+HK0F+wQjmpqt2pUulVQXQAjvE1qoRFQ4/yeVrvhVqCzFICnariQP7tMYEo=", 72 | } 73 | } 74 | ) 75 | end 76 | end 77 | 78 | context "when parsing an ecdsa key string" do 79 | it "should parse it and have the right values for each attribute" do 80 | key_string = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyN" + 81 | "TYAAAAIbmlzdHAyNTYAAABBBC4gXA5naQtjcKu90NJ7A4jQ1U" + 82 | "gxYGdnndJyr4PSZJ59qJUzkoH3VgdTlseXbIZHwO4k2gNcFpa" + 83 | "Mq5gqVRobAwU=" 84 | key = SSHScan::Crypto::PublicKey.new(key_string) 85 | expect(key).to be_kind_of SSHScan::Crypto::PublicKey 86 | expect(key.valid?).to be true 87 | expect(key.type).to eq("ecdsa-sha2-nistp256") 88 | expect(key.length).to be 520 89 | expect(key.fingerprint_md5).to eq("be:04:32:74:c6:63:fa:24:c3:c6:78:c2:cd:d2:3e:f4") 90 | expect(key.fingerprint_sha1).to eq("00:67:e3:4d:78:2f:65:94:87:bf:54:5a:1e:92:af:67:0b:8d:b5:2c") 91 | expect(key.fingerprint_sha256).to eq("11:97:bc:66:84:b0:03:30:ce:2d:1e:39:1f:63:d9:d5:a1:86:7e:7b:79:f5:92:eb:2f:96:f0:9e:34:30:4d:42") 92 | expect(key.to_hash).to eq( 93 | { 94 | "ecdsa-sha2-nistp256" => { 95 | "fingerprints" => { 96 | "md5"=>"be:04:32:74:c6:63:fa:24:c3:c6:78:c2:cd:d2:3e:f4", 97 | "sha1"=>"00:67:e3:4d:78:2f:65:94:87:bf:54:5a:1e:92:af:67:0b:8d:b5:2c", 98 | "sha256"=>"11:97:bc:66:84:b0:03:30:ce:2d:1e:39:1f:63:d9:d5:a1:86:7e:7b:79:f5:92:eb:2f:96:f0:9e:34:30:4d:42" 99 | }, 100 | "length" => 520, 101 | "raw" => "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBC4gXA5naQtjcKu90NJ7A4jQ1UgxYGdnndJyr4PSZJ59qJUzkoH3VgdTlseXbIZHwO4k2gNcFpaMq5gqVRobAwU=", 102 | } 103 | } 104 | ) 105 | end 106 | end 107 | 108 | context "when parsing an ed25519 key string" do 109 | it "should parse it and have the right values for each attribute" do 110 | key_string = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINO+ybidO9DGOb1dDwyVvGcrCD/wILFWTYtWUQZVxXwH" 111 | key = SSHScan::Crypto::PublicKey.new(key_string) 112 | expect(key).to be_kind_of SSHScan::Crypto::PublicKey 113 | expect(key.valid?).to be true 114 | expect(key.type).to eq("ed25519") 115 | expect(key.length).to be 256 116 | expect(key.fingerprint_md5).to eq("0f:db:50:54:15:22:b3:6f:31:7c:ee:22:23:77:bc:77") 117 | expect(key.fingerprint_sha1).to eq("32:d1:e8:50:ae:1c:cb:11:c5:09:fa:02:6e:f4:e8:dc:11:11:4c:48") 118 | expect(key.fingerprint_sha256).to eq("a7:e3:fb:f3:04:7a:d4:a6:78:52:f4:19:a6:bf:38:12:ab:25:9d:19:21:67:bb:71:4f:56:cd:f2:f0:3f:a0:75") 119 | expect(key.to_hash).to eq( 120 | { 121 | "ed25519" => { 122 | "fingerprints" => { 123 | "md5"=>"0f:db:50:54:15:22:b3:6f:31:7c:ee:22:23:77:bc:77", 124 | "sha1"=>"32:d1:e8:50:ae:1c:cb:11:c5:09:fa:02:6e:f4:e8:dc:11:11:4c:48", 125 | "sha256"=>"a7:e3:fb:f3:04:7a:d4:a6:78:52:f4:19:a6:bf:38:12:ab:25:9d:19:21:67:bb:71:4f:56:cd:f2:f0:3f:a0:75" 126 | }, 127 | "length" => 256, 128 | "raw" => "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINO+ybidO9DGOb1dDwyVvGcrCD/wILFWTYtWUQZVxXwH", 129 | } 130 | } 131 | ) 132 | end 133 | end 134 | 135 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'coveralls' 2 | Coveralls.wear! 3 | -------------------------------------------------------------------------------- /spec/ssh_scan/attribute_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require 'ssh_scan/attribute' 4 | 5 | describe SSHScan::Attribute do 6 | 7 | context "when turning arrays into attribute arrays" do 8 | it "should create an array of attributes" do 9 | attribute_strings = ["foo", "bar", "baz"] 10 | 11 | SSHScan.make_attributes(attribute_strings).each do |attribute| 12 | expect(attribute).to be_kind_of(SSHScan::Attribute) 13 | end 14 | end 15 | 16 | it "should match on include? checks on exact match" do 17 | attribute_strings = ["foo", "bar", "baz"] 18 | attribute_array = SSHScan.make_attributes(attribute_strings) 19 | attribute = SSHScan::Attribute.new("foo") 20 | expect(attribute_array.include?(attribute)).to be true 21 | end 22 | 23 | it "should match on include? checks on base match" do 24 | attribute_strings = ["foo", "bar", "baz"] 25 | attribute_array = SSHScan.make_attributes(attribute_strings) 26 | attribute = SSHScan::Attribute.new("foo@foo.com") 27 | expect(attribute_array.include?(attribute)).to be true 28 | end 29 | 30 | it "should match on include? checks on reverse base match" do 31 | attribute_strings = ["foo@foo.com", "bar", "baz"] 32 | attribute_array = SSHScan.make_attributes(attribute_strings) 33 | attribute = SSHScan::Attribute.new("foo") 34 | expect(attribute_array.include?(attribute)).to be true 35 | end 36 | 37 | it "should match on include? checks on different implmentation, but same base match" do 38 | attribute_strings = ["foo@foo.com", "bar", "baz"] 39 | attribute_array = SSHScan.make_attributes(attribute_strings) 40 | attribute = SSHScan::Attribute.new("foo@baz.com") 41 | expect(attribute_array.include?(attribute)).to be true 42 | end 43 | 44 | it "should not match on include? checks" do 45 | attribute_strings = ["foo", "bar", "baz"] 46 | attribute_array = SSHScan.make_attributes(attribute_strings) 47 | attribute = SSHScan::Attribute.new("abc") 48 | expect(attribute_array.include?(attribute)).to be false 49 | end 50 | 51 | it "should not match on include? checks on base" do 52 | attribute_strings = ["foo", "bar", "baz"] 53 | attribute_array = SSHScan.make_attributes(attribute_strings) 54 | attribute = SSHScan::Attribute.new("abc@foo.com") 55 | expect(attribute_array.include?(attribute)).to be false 56 | end 57 | 58 | it "should not match on include? checks on reverse base" do 59 | attribute_strings = ["foo@foo.com", "bar", "baz"] 60 | attribute_array = SSHScan.make_attributes(attribute_strings) 61 | attribute = SSHScan::Attribute.new("abc") 62 | expect(attribute_array.include?(attribute)).to be false 63 | end 64 | 65 | it "should not match on include? checks on different implmentation" do 66 | attribute_strings = ["foo@foo.com", "bar", "baz"] 67 | attribute_array = SSHScan.make_attributes(attribute_strings) 68 | attribute = SSHScan::Attribute.new("abc@baz.com") 69 | expect(attribute_array.include?(attribute)).to be false 70 | end 71 | end 72 | 73 | context "when initializing an attribute" do 74 | it "should create a attribute object (non-implementation specific)" do 75 | attribute = SSHScan::Attribute.new("curve25519-sha256") 76 | expect(attribute).to be_kind_of(SSHScan::Attribute) 77 | expect(attribute.to_s).to eql("curve25519-sha256") 78 | expect(attribute.base).to eql("curve25519-sha256") 79 | end 80 | 81 | it "should create a attribute (implementation specific)" do 82 | attribute = SSHScan::Attribute.new("curve25519-sha256@foo.com") 83 | expect(attribute).to be_kind_of(SSHScan::Attribute) 84 | expect(attribute.to_s).to eql("curve25519-sha256@foo.com") 85 | expect(attribute.base).to eql("curve25519-sha256") 86 | end 87 | end 88 | 89 | context "when comparing attribute objects" do 90 | it "cipher obects from the same non-decriptive implementation should be equal" do 91 | attribute1 = SSHScan::Attribute.new("curve25519-sha256") 92 | attribute2 = SSHScan::Attribute.new("curve25519-sha256") 93 | expect(attribute1).to eq(attribute2) 94 | end 95 | 96 | it "attribute objects from a non-decriptive implementation and a decriptive implementation should be equal" do 97 | attribute1 = SSHScan::Attribute.new("curve25519-sha256") 98 | attribute2 = SSHScan::Attribute.new("curve25519-sha256@foo.com") 99 | expect(attribute1).to eq(attribute2) 100 | end 101 | 102 | it "attribute objects from same ciphers in two different decriptive implementations should be equal" do 103 | attribute1 = SSHScan::Attribute.new("curve25519-sha256@bar.com") 104 | attribute2 = SSHScan::Attribute.new("curve25519-sha256@foo.com") 105 | expect(attribute1).to eq(attribute2) 106 | end 107 | 108 | it "attribute comparison from the different non-decriptive implementations should not be equal" do 109 | attribute1 = SSHScan::Attribute.new("curve25519-sha256") 110 | attribute2 = SSHScan::Attribute.new("curve25519-sha1000") 111 | expect(attribute1).not_to eq(attribute2) 112 | end 113 | 114 | it "attribute comparison from different non-decriptive implementation and a decriptive implementation should not be equal" do 115 | attribute1 = SSHScan::Attribute.new("curve25519-sha256") 116 | attribute2 = SSHScan::Attribute.new("curve25519-sha1000@foo.com") 117 | expect(attribute1).not_to eq(attribute2) 118 | end 119 | 120 | it "attribute comparison from same ciphers in two different decriptive implementations should be ==" do 121 | attribute1 = SSHScan::Attribute.new("curve25519-sha256@bar.com") 122 | attribute2 = SSHScan::Attribute.new("curve25519-sha1000@foo.com") 123 | expect(attribute1).not_to eq(attribute2) 124 | end 125 | end 126 | end -------------------------------------------------------------------------------- /spec/ssh_scan/banner/banner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative 'helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when creating from scratch" do 7 | it "it should create a new Banner object" do 8 | banner_string = "SSH-2.0-server" 9 | banner = SSHScan::Banner.new(banner_string) 10 | expect(banner).to be_kind_of(SSHScan::Banner) 11 | expect(banner.to_s).to eql(banner_string) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/generic_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative 'helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when fingerprinting (generic examples)" do 7 | expectations = { 8 | "SSH-2.0-OpenSSH" => { 9 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 10 | }, 11 | "SSH-2.0-ubuntu" => { 12 | :os_class => SSHScan::OS::Ubuntu, 13 | }, 14 | "SSH-2.0-centos" => { 15 | :os_class => SSHScan::OS::CentOS, 16 | }, 17 | "SSH-2.0-debian" => { 18 | :os_class => SSHScan::OS::Debian, 19 | }, 20 | "SSH-2.0-freebsd" => { 21 | :os_class => SSHScan::OS::FreeBSD, 22 | }, 23 | "SSH-2.0-windows" => { 24 | :os_class => SSHScan::OS::Windows, 25 | }, 26 | "SSH-2.0-rhel" => { 27 | :os_class => SSHScan::OS::RedHat, 28 | }, 29 | "SSH-2.0-redhat" => { 30 | :os_class => SSHScan::OS::RedHat, 31 | }, 32 | "SSH-2.0-cisco" => { 33 | :os_class => SSHScan::OS::Cisco, 34 | }, 35 | "SSH-2.0-ros" => { 36 | :os_class => SSHScan::OS::ROS, 37 | }, 38 | "SSH-1.99-DOPRA" => { 39 | :os_class => SSHScan::OS::DOPRA, 40 | }, 41 | "SSH-2.0-bananas" => { 42 | :os_class => SSHScan::OS::Unknown, 43 | }, 44 | } 45 | checkFingerprints(expectations) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/helper.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require 'ssh_scan/banner' 4 | 5 | def checkFingerprints(fingerprint_expectations) 6 | fingerprint_expectations.each do |banner_string, expectations| 7 | it "should fingerprint #{banner_string} correctly" do 8 | banner = SSHScan::Banner.new(banner_string) 9 | 10 | if expectations[:os_class] 11 | expect(banner.os_guess).to be_kind_of(expectations[:os_class]) 12 | end 13 | 14 | if expectations[:os_version] 15 | expect(banner.os_guess.version.to_s).to eql( 16 | expectations[:os_version] 17 | ) 18 | end 19 | 20 | if expectations[:os_cpe] 21 | expect(banner.os_guess.cpe).to eql(expectations[:os_cpe]) 22 | end 23 | 24 | if expectations[:ssh_lib_class] 25 | expect(banner.ssh_lib_guess).to be_kind_of( 26 | expectations[:ssh_lib_class] 27 | ) 28 | end 29 | 30 | if expectations[:ssh_lib_version] 31 | expect(banner.ssh_lib_guess.version.to_s).to eql( 32 | expectations[:ssh_lib_version] 33 | ) 34 | end 35 | 36 | if expectations[:ssh_lib_cpe] 37 | expect(banner.ssh_lib_guess.cpe).to eql(expectations[:ssh_lib_cpe]) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/mixed_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative 'helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when fingerprinting (mixed examples)" do 7 | fingerprint_expectations = { 8 | "SSH-1.99-OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.7" => { 9 | :os_class => SSHScan::OS::Ubuntu, 10 | :os_version => "14.04", 11 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 12 | :ssh_lib_version => "6.6.1p1", 13 | }, 14 | "SSH-2.0-OpenSSH_6.9p1 Debian-2" => { 15 | :os_class => SSHScan::OS::Debian, 16 | :os_version => "", 17 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 18 | :ssh_lib_version => "6.9p1", 19 | }, 20 | "SSH-2.0-OpenSSH_7.3" => { 21 | :os_class => SSHScan::OS::Unknown, 22 | :os_version => "", 23 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 24 | :ssh_lib_version => "7.3", 25 | }, 26 | "SSH-2.0-OpenSSH_6.6.1p1-hpn14v2 FreeBSD-openssh-portable-6.6.p1_2,1" => { 27 | :os_class => SSHScan::OS::FreeBSD, 28 | :os_version => "", 29 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 30 | :ssh_lib_version => "6.6.1p1", 31 | }, 32 | "SSH-2.0-Cisco-1.25" => { 33 | :os_class => SSHScan::OS::Cisco, 34 | :os_version => "", 35 | :ssh_lib_class => SSHScan::SSHLib::CiscoSSH, 36 | :ssh_lib_version => "", 37 | }, 38 | "SSH-2.0-ROSSSH" => { 39 | :os_class => SSHScan::OS::ROS, 40 | :os_version => "", 41 | :ssh_lib_class => SSHScan::SSHLib::ROSSSH, 42 | :ssh_lib_version => "", 43 | }, 44 | } 45 | checkFingerprints(fingerprint_expectations) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/os/debian_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative '../helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when debian fingerprinting" do 7 | expectations = { 8 | "SSH-2.0-OpenSSH_7.3p1 Debian-1" => { 9 | :os_class => SSHScan::OS::Debian, 10 | :os_version => "", 11 | :os_cpe => "o:debian:debian", 12 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 13 | :ssh_lib_version => "7.3p1", 14 | }, 15 | "SSH-2.0-OpenSSH_7.2p2 Debian-2" => { 16 | :os_class => SSHScan::OS::Debian, 17 | :os_version => "", 18 | :os_cpe => "o:debian:debian", 19 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 20 | :ssh_lib_version => "7.2p2", 21 | }, 22 | "SSH-2.0-OpenSSH_7.2p2 Debian-5" => { 23 | :os_class => SSHScan::OS::Debian, 24 | :os_version => "", 25 | :os_cpe => "o:debian:debian", 26 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 27 | :ssh_lib_version => "7.2p2", 28 | }, 29 | "SSH-2.0-OpenSSH_7.2p2 Debian-8" => { 30 | :os_class => SSHScan::OS::Debian, 31 | :os_version => "", 32 | :os_cpe => "o:debian:debian", 33 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 34 | :ssh_lib_version => "7.2p2", 35 | }, 36 | "SSH-2.0-OpenSSH_6.7p1 Debian-5+deb8u3" => { 37 | :os_class => SSHScan::OS::Debian, 38 | :os_version => "", 39 | :os_cpe => "o:debian:debian", 40 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 41 | :ssh_lib_version => "6.7p1", 42 | }, 43 | "SSH-2.0-OpenSSH_6.7p1 Debian-5+deb8u2" => { 44 | :os_class => SSHScan::OS::Debian, 45 | :os_version => "", 46 | :os_cpe => "o:debian:debian", 47 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 48 | :ssh_lib_version => "6.7p1", 49 | }, 50 | "SSH-2.0-OpenSSH_6.0p1 Debian-4+deb7u4" => { 51 | :os_class => SSHScan::OS::Debian, 52 | :os_version => "", 53 | :os_cpe => "o:debian:debian", 54 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 55 | :ssh_lib_version => "6.0p1", 56 | }, 57 | } 58 | checkFingerprints(expectations) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/os/freebsd_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative '../helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when freebsd fingerprinting" do 7 | expectations = { 8 | "SSH-2.0-OpenSSH_5.4p1 FreeBSD-20100308" => { 9 | :os_class => SSHScan::OS::FreeBSD, 10 | :os_version => "", 11 | :os_cpe => "o:freebsd:freebsd", 12 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 13 | :ssh_lib_version => "5.4p1", 14 | }, 15 | } 16 | checkFingerprints(expectations) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/os/raspbian_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative '../helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when raspbian fingerprinting" do 7 | expectations = { 8 | "SSH-2.0-OpenSSH_6.7p1 Raspbian-5" => { 9 | :os_class => SSHScan::OS::Raspbian, 10 | # TODO fix me 11 | # :os_version => "5", 12 | :os_cpe => "o:raspbian:raspbian", 13 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 14 | :ssh_lib_version => "6.7p1", 15 | }, 16 | } 17 | checkFingerprints(expectations) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/os/windows_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative '../helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when windows fingerprinting" do 7 | expectations = { 8 | "SSH-2.0-OpenSSH_7.1p1 Microsoft_Win32_port_with_VS" => { 9 | :os_class => SSHScan::OS::Windows, 10 | :os_version => "", 11 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 12 | :ssh_lib_version => "7.1p1", 13 | }, 14 | } 15 | checkFingerprints(expectations) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/ssh_lib/cryptlib_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative '../helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when cryptlib fingerprinting" do 7 | expectations = { 8 | "SSH-2.0-cryptlib" => { 9 | :ssh_lib_class => SSHScan::SSHLib::Cryptlib, 10 | :ssh_lib_version => "", 11 | :ssh_lib_cpe => "a:cryptlib:cryptlib", 12 | }, 13 | } 14 | checkFingerprints(expectations) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/ssh_lib/dropbear_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative '../helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when dropbear fingerprinting" do 7 | expectations = {} 8 | 9 | # Scraped from Dropbear's Changelog 10 | VERSIONS = ["2016.74", "2016.73", "2016.72", "2015.71", "2015.70", 11 | "2015.69", "2015.68", "2015.67", "2014.66", "2014.65", 12 | "2014.64", "2014.63", "2013.62", "2013.61test", "2013.60", 13 | "2013.59", "2013.58", "2013.57", "2013.56", "2012.55", 14 | "2011.54", "0.53.1", "0.53", "0.52", "0.51", "0.50", 15 | "0.49", "0.48.1", "0.48", "0.47", "0.46", "0.45", "0.44", 16 | "0.44test4", "0.44test3", "0.44test2", "0.44test1", "0.43", 17 | "0.42", "0.41", "0.40", "0.39", "0.38", "0.37", "0.36", 18 | "0.35", "0.34", "0.33", "0.32", "0.31", "0.30", "0.29", 19 | "0.28"] 20 | VERSIONS.each do |dropbear_version| 21 | expectations["SSH-2.0-dropbear_#{dropbear_version}"] = { 22 | :ssh_lib_class => SSHScan::SSHLib::Dropbear, 23 | :ssh_lib_version => dropbear_version, 24 | :ssh_lib_cpe => "a:dropbear:dropbear:#{dropbear_version}", 25 | } 26 | end 27 | checkFingerprints(expectations) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/ssh_lib/flowssh_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative '../helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when flowssh fingerprinting" do 7 | expectations = { 8 | "SSH-2.0-5.32 FlowSsh" => { 9 | :ssh_lib_class => SSHScan::SSHLib::FlowSsh, 10 | :ssh_lib_version => "5.32", 11 | :ssh_lib_cpe => "a:bitvise:flowssh:5.32", 12 | }, 13 | } 14 | checkFingerprints(expectations) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/ssh_lib/ipssh_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative '../helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when ipssh fingerprinting" do 7 | expectations = { 8 | "SSH-2.0-IPSSH-6.6.0" => { 9 | :ssh_lib_class => SSHScan::SSHLib::IpSsh, 10 | :ssh_lib_version => "6.6.0", 11 | :ssh_lib_cpe => "a:ipssh:ipssh:6.6.0", 12 | }, 13 | } 14 | checkFingerprints(expectations) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/ssh_lib/mpssh_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative '../helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when mpssh fingerprinting" do 7 | expectations = { 8 | "SSH-2.0-mpSSH_0.2.1" => { 9 | :ssh_lib_class => SSHScan::SSHLib::Mpssh, 10 | :ssh_lib_version => "0.2.1", 11 | :ssh_lib_cpe => "a:mpssh:mpssh:0.2.1", 12 | }, 13 | } 14 | checkFingerprints(expectations) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/ssh_lib/nosssh_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative '../helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when nosssh fingerprinting" do 7 | expectations = { 8 | "SSH-2.0-NOS-SSH_2.0" => { 9 | :ssh_lib_class => SSHScan::SSHLib::NosSSH, 10 | :ssh_lib_version => "2.0", 11 | :ssh_lib_cpe => "a:nosssh:nosssh:2.0", 12 | }, 13 | } 14 | checkFingerprints(expectations) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/ssh_lib/openssh_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative '../helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when openssh fingerprinting" do 7 | expectations = { 8 | "SSH-2.0-OpenSSH_7.3" => { 9 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 10 | :ssh_lib_version => "7.3", 11 | :ssh_lib_cpe => "a:openssh:openssh:7.3", 12 | }, 13 | "SSH-2.0-OpenSSH_6.8p1-hpn14v6" => { 14 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 15 | :ssh_lib_version => "6.8p1", 16 | :ssh_lib_cpe => "a:openssh:openssh:6.8p1", 17 | }, 18 | "SSH-2.0-OpenSSH_6.6.1" => { 19 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 20 | :ssh_lib_version => "6.6.1", 21 | :ssh_lib_cpe => "a:openssh:openssh:6.6.1", 22 | }, 23 | "SSH-2.0-OpenSSH_6.2 FIPS" => { 24 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 25 | :ssh_lib_version => "6.2", 26 | :ssh_lib_cpe => "a:openssh:openssh:6.2", 27 | }, 28 | "SSH-2.0-OpenSSH_12.1" => { 29 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 30 | :ssh_lib_version => "12.1", 31 | :ssh_lib_cpe => "a:openssh:openssh:12.1", 32 | }, 33 | "SSH-1.99-OpenSSH_3.7.1p2" => { 34 | :ssh_lib_class => SSHScan::SSHLib::OpenSSH, 35 | :ssh_lib_version => "3.7.1p2", 36 | :ssh_lib_cpe => "a:openssh:openssh:3.7.1p2", 37 | }, 38 | } 39 | checkFingerprints(expectations) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/ssh_lib/pgp_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative '../helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when pgp fingerprinting" do 7 | expectations = { 8 | "SSH-2.0-PGP" => { 9 | :ssh_lib_class => SSHScan::SSHLib::PGP, 10 | :ssh_lib_version => "", 11 | :ssh_lib_cpe => "a:pgp:pgp", 12 | }, 13 | } 14 | checkFingerprints(expectations) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/ssh_lib/romsshell_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative '../helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when romsshell fingerprinting" do 7 | expectations = { 8 | "SSH-2.0-RomSShell_4.62" => { 9 | :ssh_lib_class => SSHScan::SSHLib::RomSShell, 10 | :ssh_lib_version => "4.62", 11 | :ssh_lib_cpe => "a:allegrosoft:romsshell:4.62", 12 | }, 13 | } 14 | checkFingerprints(expectations) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/ssh_scan/banner/ssh_lib/sentryssh_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require_relative '../helper' 4 | 5 | describe SSHScan::Banner do 6 | context "when sentryssh fingerprinting" do 7 | expectations = { 8 | "SSH-2.0-Mocana SSH" => { 9 | :ssh_lib_class => SSHScan::SSHLib::SentrySSH, 10 | :ssh_lib_version => "", 11 | :ssh_lib_cpe => "a:servertech:sentryssh", 12 | }, 13 | "SSH-2.0-ServerTech_SSH" => { 14 | :ssh_lib_class => SSHScan::SSHLib::SentrySSH, 15 | :ssh_lib_version => "", 16 | :ssh_lib_cpe => "a:servertech:sentryssh", 17 | }, 18 | } 19 | checkFingerprints(expectations) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/ssh_scan/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require 'timeout' 4 | require 'ssh_scan/client' 5 | require 'ssh_scan/constants' 6 | 7 | describe SSHScan::Client do 8 | context "when connecting as a client" do 9 | it "should follow a specific sequence on the Socket for connect()\ 10 | operation" do 11 | server_banner = SSHScan::Banner.read("SSH-2.0-server") 12 | 13 | # Override TCPSocket behavior in the name of unit-testing 14 | io = double("io", puts: nil) 15 | allow(io).to receive(:puts).and_return(nil) 16 | allow(io).to receive(:gets).and_return(server_banner.to_s) 17 | allow(Socket).to receive(:tcp) { io } 18 | 19 | # Do the client connect action 20 | client = SSHScan::Client.new("192.168.1.1", 22) 21 | client.connect 22 | 23 | # Verify the client behaved as expected 24 | expect(io).to have_received(:puts).with("SSH-2.0-ssh_scan") 25 | expect(client.instance_variable_get(:@server_banner)).to eq( 26 | server_banner 27 | ) 28 | end 29 | 30 | it "should follow a specific sequence on the TCPSocket for \ 31 | get_kex_result() operation" do 32 | server_banner = SSHScan::Banner.read("SSH-2.0-server") 33 | 34 | # Override TCPSocket behavior in the name of unit-testing 35 | io = double("io", puts: nil) 36 | allow(io).to receive(:puts).and_return(nil) 37 | allow(io).to receive(:gets).and_return(server_banner.to_s) 38 | allow(Socket).to receive(:tcp) { io } 39 | 40 | # Do the client connect action 41 | client = SSHScan::Client.new("192.168.1.1", 22) 42 | client.connect 43 | 44 | # Verify the client behaved as expected 45 | expect(io).to have_received(:puts).with("SSH-2.0-ssh_scan") 46 | expect(client.instance_variable_get(:@server_banner)).to eq( 47 | server_banner 48 | ) 49 | 50 | # Override more TCPSocket behavior in the name of unit-testing 51 | allow(io).to receive(:write).and_return(nil) 52 | allow(io).to receive(:read).and_return( 53 | "\x00\x00\x03D", 54 | "\n\x14\x17>>\xD1\xB5r\xBF\xA0jE\x19Y1p\x85\xEE\x00\x00\x00~\ 55 | diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,\ 56 | diffie-hellman-group14-sha1,diffie-hellman-group1-sha1\x00\x00\x00\x0Fssh-rsa\ 57 | ,ssh-dss\x00\x00\x00\x9Daes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,\ 58 | aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,aes192-cbc,aes256-cbc,arcfour,\ 59 | rijndael-cbc@lysator.liu.se\x00\x00\x00\x9Daes128-ctr,aes192-ctr,aes256-ctr,\ 60 | arcfour256,arcfour128,aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,aes192-cbc,\ 61 | aes256-cbc,arcfour,rijndael-cbc@lysator.liu.se\x00\x00\x00\x85hmac-md5,\ 62 | hmac-sha1,umac-64@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-ripemd160,\ 63 | hmac-ripemd160@openssh.com,hmac-sha1-96,hmac-md5-96\x00\x00\x00\x85hmac-md5,\ 64 | hmac-sha1,umac-64@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-ripemd160,\ 65 | hmac-ripemd160@openssh.com,hmac-sha1-96,hmac-md5-96\x00\x00\x00\x15none,\ 66 | zlib@openssh.com\x00\x00\x00\x15none,zlib@openssh.com\x00\x00\x00\x00\x00\ 67 | \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 68 | ) 69 | allow(io).to receive(:close).and_return(nil) 70 | 71 | # Do the client get_kex_result action 72 | result = client.get_kex_result 73 | 74 | # Verify the client behaved as expected 75 | expect(io).to have_received(:write).once.with( 76 | SSHScan::Constants::DEFAULT_KEY_INIT_RAW.unhexify 77 | ) 78 | expect(io).to have_received(:read).with(4) 79 | expect(io).to have_received(:read).with(836) 80 | expect(io).to have_received(:close).once 81 | expect(result).to be_kind_of(::Hash) 82 | end 83 | 84 | # TODO: this unit-test doesn't actually test anything in the client. 85 | # commenting this out until we add this feature to the client 86 | # it "should time out after 3 seconds when connecting to a bad IP" do 87 | # # Choose a bad IP (say broadcast address for 192.168.0.0/16) 88 | # bad_ip = "192.168.255.255" 89 | # 90 | # time_start = Time.now 91 | # time_passed = 0 92 | # begin 93 | # client = SSHScan::Client.new(bad_ip, 22) 94 | # Timeout.timeout(3) {client.connect} 95 | # rescue Timeout::Error 96 | # ensure 97 | # time_passed = Time.now-time_start 98 | # end 99 | # 100 | # expect(time_passed > 3).to eql(true) 101 | # end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /spec/ssh_scan/constants_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require 'ssh_scan/constants' 4 | 5 | describe SSHScan::Constants do 6 | it "should have the right value for DEFAULT_KEY_INIT_RAW" do 7 | default_key_init_raw = 8 | "000001640414e33f813f8cdcc6b00a3d852ec1aea4980000001a6\ 9 | 469666669652d68656c6c6d616e2d67726f7570312d7368613100\ 10 | 00000f7373682d6473732c7373682d72736100000057616573313\ 11 | 2382d6362632c336465732d6362632c626c6f77666973682d6362\ 12 | 632c6165733139322d6362632c6165733235362d6362632c61657\ 13 | 33132382d6374722c6165733139322d6374722c6165733235362d\ 14 | 637472000000576165733132382d6362632c336465732d6362632\ 15 | c626c6f77666973682d6362632c6165733139322d6362632c6165\ 16 | 733235362d6362632c6165733132382d6374722c6165733139322\ 17 | d6374722c6165733235362d63747200000021686d61632d6d6435\ 18 | 2c686d61632d736861312c686d61632d726970656d64313630000\ 19 | 00021686d61632d6d64352c686d61632d736861312c686d61632d\ 20 | 726970656d64313630000000046e6f6e65000000046e6f6e65000\ 21 | 000000000000000000000006e05b3b4" 22 | expect(SSHScan::Constants::DEFAULT_KEY_INIT_RAW.unhexify).to eql( 23 | default_key_init_raw.unhexify 24 | ) 25 | end 26 | 27 | it "should have the right values for DEFAULT_KEY_INIT" do 28 | expect(SSHScan::Constants::DEFAULT_KEY_INIT).to be_kind_of( 29 | SSHScan::KeyExchangeInit 30 | ) 31 | expect(SSHScan::Constants::DEFAULT_KEY_INIT.to_binary_s).to eql( 32 | SSHScan::Constants::DEFAULT_KEY_INIT_RAW.unhexify 33 | ) 34 | end 35 | 36 | it "should have the right value for DEFAULT_CLIENT_BANNER" do 37 | default_banner = "SSH-2.0-ssh_scan" 38 | expect(SSHScan::Constants::DEFAULT_CLIENT_BANNER).to be_kind_of( 39 | SSHScan::Banner 40 | ) 41 | expect(SSHScan::Constants::DEFAULT_CLIENT_BANNER.to_s).to eql( 42 | default_banner 43 | ) 44 | end 45 | 46 | it "should have the right value for DEFAULT_SERVER_BANNER" do 47 | default_banner = "SSH-2.0-server" 48 | expect(SSHScan::Constants::DEFAULT_SERVER_BANNER).to be_kind_of( 49 | SSHScan::Banner 50 | ) 51 | expect(SSHScan::Constants::DEFAULT_SERVER_BANNER.to_s).to eql( 52 | default_banner 53 | ) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/ssh_scan/fingerprint_database_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'tempfile' 3 | require 'ssh_scan/fingerprint_database' 4 | 5 | describe SSHScan::FingerprintDatabase do 6 | context "when initializing a new FingerprintDatabase" do 7 | it "should create a new DB on disk if it doesn't exist" do 8 | file_name = "test.db" 9 | 10 | #start with a known good state 11 | File.unlink(file_name) if File.exists?(file_name) 12 | expect(File.exist?(file_name)).to eql(false) 13 | 14 | database = SSHScan::FingerprintDatabase.new(file_name) 15 | expect(database).to be_kind_of(SSHScan::FingerprintDatabase) 16 | 17 | # The file isn't created until something is written 18 | expect(File.exist?(file_name)).to eql(false) 19 | 20 | # Write something, to trigger file creation 21 | database.add_fingerprint("hello_world", "192.168.1.1") 22 | 23 | # Verify the file exists now 24 | expect(File.exist?(file_name)).to eql(true) 25 | 26 | File.unlink(file_name) #clean up after ourselves 27 | end 28 | 29 | it "should use a pre-existing DB if it exists" do 30 | file_name = "test.db" 31 | 32 | #start with a known good state 33 | File.unlink(file_name) if File.exists?(file_name) 34 | 35 | # Create a pre-existing DB 36 | database = SSHScan::FingerprintDatabase.new(file_name) 37 | database.add_fingerprint("hello_world", "192.168.1.1") 38 | 39 | expect(File.exist?(file_name)).to eql(true) 40 | database2 = SSHScan::FingerprintDatabase.new(file_name) 41 | expect(database2).to be_kind_of(SSHScan::FingerprintDatabase) 42 | 43 | expect(database2.find_fingerprints("hello_world")).to eql(["192.168.1.1"]) 44 | File.unlink(file_name) #clean up after ourselves 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/ssh_scan/grader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require 'ssh_scan' 4 | 5 | describe SSHScan::Grader do 6 | it "should provide an F grade" do 7 | result = SSHScan::Result.new() 8 | result.set_compliance = { 9 | "policy" => "Test Result", 10 | "compliant" => false, 11 | "recommendations" => [ 12 | "Add these Key Exchange Algos: ecdh-sha2-nistp521,ecdh-sha2-nistp384,diffie-hellman-group-exchange-sha256", 13 | "Add these MAC Algos: hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,umac-128@openssh.com", 14 | "Add these Encryption Ciphers: aes256-gcm@openssh.com,aes128-gcm@openssh.com", 15 | "Remove these Key Exchange Algos: diffie-hellman-group14-sha1, diffie-hellman-group1-sha1", 16 | "Remove these MAC Algos: hmac-sha1", 17 | "Remove these Encryption Ciphers: aes256-cbc, aes192-cbc, aes128-cbc, blowfish-cbc", 18 | ] 19 | } 20 | grader = SSHScan::Grader.new(result) 21 | expect(grader.grade).to eql("F") 22 | end 23 | 24 | it "should provide an F grade" do 25 | result = SSHScan::Result.new() 26 | result.set_compliance = { 27 | "policy" => "Test Result", 28 | "compliant" => false, 29 | "recommendations" => [ 30 | "Add these Key Exchange Algos: ecdh-sha2-nistp521,ecdh-sha2-nistp384,diffie-hellman-group-exchange-sha256", 31 | "Add these MAC Algos: hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,umac-128@openssh.com", 32 | "Add these Encryption Ciphers: aes256-gcm@openssh.com,aes128-gcm@openssh.com", 33 | "Remove these Key Exchange Algos: diffie-hellman-group14-sha1, diffie-hellman-group1-sha1", 34 | ] 35 | } 36 | grader = SSHScan::Grader.new(result) 37 | expect(grader.grade).to eql("F") 38 | end 39 | 40 | it "should provide an D grade" do 41 | result = SSHScan::Result.new() 42 | result.set_compliance = { 43 | "policy" => "Test Result", 44 | "compliant" => false, 45 | "recommendations" => [ 46 | "Add these Key Exchange Algos: ecdh-sha2-nistp521,ecdh-sha2-nistp384,diffie-hellman-group-exchange-sha256", 47 | "Add these MAC Algos: hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,umac-128@openssh.com", 48 | "Add these Encryption Ciphers: aes256-gcm@openssh.com,aes128-gcm@openssh.com", 49 | ] 50 | } 51 | grader = SSHScan::Grader.new(result) 52 | expect(grader.grade).to eql("D") 53 | end 54 | 55 | it "should provide an C grade" do 56 | result = SSHScan::Result.new() 57 | result.set_compliance = { 58 | "policy" => "Test Result", 59 | "compliant" => false, 60 | "recommendations" => [ 61 | "Add these Key Exchange Algos: ecdh-sha2-nistp521,ecdh-sha2-nistp384,diffie-hellman-group-exchange-sha256", 62 | "Add these MAC Algos: hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,umac-128@openssh.com", 63 | ] 64 | } 65 | grader = SSHScan::Grader.new(result) 66 | expect(grader.grade).to eql("C") 67 | end 68 | 69 | it "should provide an B grade" do 70 | result = SSHScan::Result.new() 71 | result.set_compliance = { 72 | "policy" => "Test Result", 73 | "compliant" => false, 74 | "recommendations" => [ 75 | "Add these Key Exchange Algos: ecdh-sha2-nistp521,ecdh-sha2-nistp384,diffie-hellman-group-exchange-sha256", 76 | ] 77 | } 78 | grader = SSHScan::Grader.new(result) 79 | expect(grader.grade).to eql("B") 80 | end 81 | 82 | it "should provide an A grade" do 83 | result = SSHScan::Result.new() 84 | result.set_compliance = { 85 | "policy" => "Test Result", 86 | "compliant" => false, 87 | "recommendations" => [ 88 | ] 89 | } 90 | grader = SSHScan::Grader.new(result) 91 | expect(grader.grade).to eql("A") 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/ssh_scan/integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | command_exists () { 4 | type "$1" &> /dev/null ; 5 | } 6 | 7 | if gem list | grep 'ssh_scan' 8 | then 9 | echo "Running integration tests via ssh_scan RubyGem" 10 | SSH_SCAN_BINARY="ssh_scan" 11 | else 12 | echo "Running integration tests via ssh_scan source" 13 | SSH_SCAN_BINARY="ruby -I lib bin/ssh_scan" 14 | fi 15 | 16 | # Change permissions so the shell script will run 17 | chmod 755 ./spec/ssh_scan/integration.sh 18 | 19 | # Integration Test #1 (Basic) 20 | $SSH_SCAN_BINARY -t ssh.mozilla.com > /dev/null 21 | if [ $? -eq 0 ] 22 | then 23 | echo "Integration Test #1: Pass (Basic)" 24 | else 25 | echo "Integration Test #1: Fail (Basic)" 26 | exit 1 27 | fi 28 | 29 | # Integration Test #2 (Basic + Port) 30 | $SSH_SCAN_BINARY -t ssh.mozilla.com -p 22 > /dev/null 31 | if [ $? -eq 0 ] 32 | then 33 | echo "Integration Test #2: Pass (Basic + Port)" 34 | else 35 | echo "Integration Test #2: Fail (Basic + Port)" 36 | exit 1 37 | fi 38 | 39 | # Integration Test #3 (Basic + File Output) 40 | $SSH_SCAN_BINARY -t ssh.mozilla.com -p 22 -o output.json 41 | if [ $? -eq 0 ] 42 | then 43 | echo "Integration Test #3: Pass (Basic + File Output)" 44 | else 45 | echo "Integration Test #3: Fail (Basic + File Output)" 46 | exit 1 47 | fi 48 | 49 | # Integration Test #4 (Basic + File Input) 50 | echo "ssh.mozilla.com" >> input.txt 51 | echo "github.com" >> input.txt 52 | $SSH_SCAN_BINARY -t ssh.mozilla.com -p 22 -f input.txt > /dev/null 53 | if [ $? -eq 0 ] 54 | then 55 | echo "Integration Test #4: Pass (Basic + File Input)" 56 | else 57 | echo "Integration Test #4: Fail (Basic + File Input)" 58 | exit 1 59 | fi 60 | 61 | # Integration Test #5 (File Input + File Output Rescan) 62 | $SSH_SCAN_BINARY -t ssh.mozilla.com -p 22 -o output.json 63 | $SSH_SCAN_BINARY -O output.json -o rescan_output.json 64 | if [ $? -eq 0 ] 65 | then 66 | echo "Integration Test #5: Pass (File Input + File Output Rescan)" 67 | else 68 | echo "Integration Test #5: Fail (File Input + File Output Rescan)" 69 | exit 1 70 | fi 71 | 72 | # Integration Test #6 (Help Output) 73 | $SSH_SCAN_BINARY -h > /dev/null 74 | if [ $? -eq 0 ] 75 | then 76 | echo "Integration Test #6: Pass (Help Output)" 77 | else 78 | echo "Integration Test #6: Fail (Help Output)" 79 | exit 1 80 | fi 81 | 82 | # Integration Test #7 (verbose scan) 83 | $SSH_SCAN_BINARY -t ssh.mozilla.com -V DEBUG 84 | if [ $? -eq 0 ] 85 | then 86 | echo "Integration Test #7: Pass (verbose scan)" 87 | else 88 | echo "Integration Test #7: Fail (verbose scan)" 89 | exit 1 90 | fi 91 | -------------------------------------------------------------------------------- /spec/ssh_scan/policy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require 'ssh_scan/policy' 4 | require 'tempfile' 5 | 6 | describe SSHScan::Policy do 7 | context "when parsing a policy via YAML string" do 8 | yaml_string = 9 | "---\nname: Mozilla Intermediate\nkex:\n\ 10 | - diffie-hellman-group-exchange-sha256\n\ 11 | encryption:\n- aes256-ctr\n- aes192-ctr\n\ 12 | - aes128-ctr\nmacs:\n- hmac-sha2-512\n\ 13 | - hmac-sha2-256\ncompression:\n- none\n\ 14 | - zlib@openssh.com\n\ 15 | references:\n- https://wiki.mozilla.org/Security/Guidelines/OpenSSH\n" 16 | 17 | it "should load all the attributes properly" do 18 | policy = SSHScan::Policy.from_string(yaml_string) 19 | 20 | expect(policy.name).to eql("Mozilla Intermediate") 21 | expect(policy.encryption).to eql( 22 | ["aes256-ctr", "aes192-ctr", "aes128-ctr"] 23 | ) 24 | expect(policy.kex).to eql(["diffie-hellman-group-exchange-sha256"]) 25 | expect(policy.macs).to eql(["hmac-sha2-512", "hmac-sha2-256"]) 26 | expect(policy.compression).to eql(["none", "zlib@openssh.com"]) 27 | expect(policy.references).to eql( 28 | ["https://wiki.mozilla.org/Security/Guidelines/OpenSSH"] 29 | ) 30 | end 31 | end 32 | 33 | context "when parsing a policy via YAML file" do 34 | yaml_string = 35 | "---\nname: Mozilla Intermediate\nkex:\n\ 36 | - diffie-hellman-group-exchange-sha256\n\ 37 | encryption:\n- aes256-ctr\n- aes192-ctr\n\ 38 | - aes128-ctr\nmacs:\n- hmac-sha2-512\n\ 39 | - hmac-sha2-256\ncompression:\n- none\n\ 40 | - zlib@openssh.com\n\ 41 | references:\n- https://wiki.mozilla.org/Security/Guidelines/OpenSSH\n" 42 | 43 | it "should load all the attributes properly" do 44 | file = Tempfile.new('foo') 45 | file.write(yaml_string) 46 | file.close 47 | 48 | policy = SSHScan::Policy.from_file(file.path) 49 | 50 | file.unlink 51 | 52 | expect(policy.name).to eql("Mozilla Intermediate") 53 | expect(policy.encryption).to eql( 54 | ["aes256-ctr", "aes192-ctr", "aes128-ctr"] 55 | ) 56 | expect(policy.kex).to eql(["diffie-hellman-group-exchange-sha256"]) 57 | expect(policy.macs).to eql(["hmac-sha2-512", "hmac-sha2-256"]) 58 | expect(policy.compression).to eql(["none", "zlib@openssh.com"]) 59 | expect(policy.references).to eql( 60 | ["https://wiki.mozilla.org/Security/Guidelines/OpenSSH"] 61 | ) 62 | end 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /spec/ssh_scan/result_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require 'ssh_scan/version' 4 | require 'ssh_scan/result' 5 | 6 | describe SSHScan::Result do 7 | it "should have sane defaults" do 8 | result = SSHScan::Result.new() 9 | 10 | expect(result).to be_kind_of(SSHScan::Result) 11 | expect(result.version).to eql(SSHScan::VERSION) 12 | expect(result.ip).to be_nil 13 | expect(result.port).to be_nil 14 | expect(result.banner).to be_kind_of(SSHScan::Banner) 15 | expect(result.banner.to_s).to eql("") 16 | expect(result.hostname).to eql("") 17 | expect(result.ssh_version).to eql("unknown") 18 | expect(result.os_guess_common).to eql("unknown") 19 | expect(result.os_guess_cpe).to eql("o:unknown") 20 | expect(result.ssh_lib_guess_common).to eql("unknown") 21 | expect(result.ssh_lib_guess_cpe).to eql("a:unknown") 22 | expect(result.cookie).to eql("") 23 | expect(result.key_algorithms).to eql([]) 24 | expect(result.server_host_key_algorithms).to eql([]) 25 | expect(result.encryption_algorithms_client_to_server).to eql([]) 26 | expect(result.encryption_algorithms_server_to_client).to eql([]) 27 | expect(result.mac_algorithms_client_to_server).to eql([]) 28 | expect(result.mac_algorithms_server_to_client).to eql([]) 29 | expect(result.compression_algorithms_client_to_server).to eql([]) 30 | expect(result.compression_algorithms_server_to_client).to eql([]) 31 | expect(result.languages_client_to_server).to eql([]) 32 | expect(result.languages_server_to_client).to eql([]) 33 | expect(result.start_time).to be_nil 34 | expect(result.end_time).to be_nil 35 | expect(result.scan_duration).to be_nil 36 | expect(result.keys).to eq({}) 37 | end 38 | 39 | context "when setting IP" do 40 | it "should allow setting result.ip" do 41 | result = SSHScan::Result.new() 42 | expect(result.ip).to be_nil 43 | 44 | result.ip = "192.168.1.1" 45 | expect(result.ip).to eql("192.168.1.1") 46 | expect(result.to_hash).to be_kind_of(Hash) 47 | end 48 | 49 | it "should prevent setting result.ip to invalid values" do 50 | result = SSHScan::Result.new() 51 | expect(result.ip).to be_nil 52 | 53 | invalid_inputs = [ 54 | "hello", 55 | 123, 56 | "192.168.10.265" 57 | ] 58 | 59 | invalid_inputs.each do |invalid_input| 60 | expect { result.ip = invalid_input}.to raise_error( 61 | ArgumentError, 62 | "Invalid attempt to set IP to a non-IP address value" 63 | ) 64 | expect(result.ip).to be_nil 65 | end 66 | end 67 | end 68 | 69 | context "when setting Port" do 70 | it "should allow setting result.port" do 71 | result = SSHScan::Result.new() 72 | expect(result.port).to be_nil 73 | 74 | result.port = 31337 75 | expect(result.port).to eql(31337) 76 | expect(result.to_hash).to be_kind_of(Hash) 77 | end 78 | 79 | it "should prevent setting result.port to invalid values" do 80 | result = SSHScan::Result.new() 81 | expect(result.port).to be_nil 82 | 83 | invalid_inputs = [ 84 | 65537, 85 | -1, 86 | "", 87 | "22" 88 | ] 89 | 90 | invalid_inputs.each do |invalid_input| 91 | expect { result.port = invalid_input}.to raise_error( 92 | ArgumentError, 93 | "Invalid attempt to set port to a non-port value" 94 | ) 95 | expect(result.ip).to be_nil 96 | end 97 | end 98 | end 99 | 100 | context "when setting banner" do 101 | it "should allow setting result.banner" do 102 | banner = SSHScan::Banner.new("This is my SSH Banner") 103 | result = SSHScan::Result.new() 104 | expect(result.banner).to be_kind_of(SSHScan::Banner) 105 | expect(result.banner.to_s).to eql("") 106 | 107 | result.banner = banner 108 | expect(result.banner).to be_kind_of(SSHScan::Banner) 109 | expect(result.banner.to_s).to eql("This is my SSH Banner") 110 | expect(result.to_hash).to be_kind_of(Hash) 111 | end 112 | 113 | it "should prevent setting result.banner to invalid values" do 114 | result = SSHScan::Result.new() 115 | expect(result.banner).to be_kind_of(SSHScan::Banner) 116 | expect(result.banner.to_s).to eql("") 117 | 118 | invalid_inputs = [ 119 | 65537, 120 | -1, 121 | "", 122 | "22", 123 | ] 124 | 125 | invalid_inputs.each do |invalid_input| 126 | expect { result.banner = invalid_input}.to raise_error( 127 | ArgumentError, 128 | "Invalid attempt to set banner with a non-banner object" 129 | ) 130 | expect(result.banner).to be_kind_of(SSHScan::Banner) 131 | expect(result.banner.to_s).to eql("") 132 | end 133 | end 134 | end 135 | 136 | context "when setting ssh_version via banner" do 137 | it "should allow setting result.ssh_version" do 138 | result = SSHScan::Result.new() 139 | expect(result.ssh_version).to eql("unknown") 140 | 141 | result.banner = SSHScan::Banner.new("SSH-2.0-server") 142 | expect(result.ssh_version).to eql(2.0) 143 | expect(result.to_hash).to be_kind_of(Hash) 144 | end 145 | end 146 | 147 | context "when setting hostname" do 148 | it "should allow setting result.hostname" do 149 | result = SSHScan::Result.new() 150 | expect(result.hostname).to eql("") 151 | 152 | result.hostname = "bananas.example.com" 153 | expect(result.hostname).to eql("bananas.example.com") 154 | expect(result.to_hash).to be_kind_of(Hash) 155 | end 156 | end 157 | 158 | context "when exporting the Result object to different Objects" do 159 | it "should translate the result into a valid hash" do 160 | result = SSHScan::Result.new() 161 | result.set_start_time 162 | result.set_end_time 163 | 164 | result_hash = result.to_hash 165 | expect(result.to_hash).to be_kind_of(Hash) 166 | end 167 | 168 | it "should translate the result into a valid JSON string" do 169 | result = SSHScan::Result.new() 170 | result.set_start_time 171 | result.set_end_time 172 | 173 | result_json_string = result.to_json 174 | expect(result_json_string).to be_kind_of(String) 175 | 176 | # Make sure we're generating valid JSON documents 177 | expect(JSON.parse(result_json_string)).to be_kind_of(Hash) 178 | end 179 | end 180 | 181 | context "when setting compliance" do 182 | it "should allow setting of the compliance information" do 183 | compliance = { 184 | "policy" => "Test Policy", 185 | "compliant" => true, 186 | "recommendations" => ["do this", "do that"], 187 | "references" => ["https://reference.example.com"], 188 | } 189 | result = SSHScan::Result.new() 190 | result.set_compliance = compliance 191 | 192 | expect(result.compliance_policy).to eql(compliance["policy"]) 193 | expect(result.compliant?).to eql(compliance["compliant"]) 194 | expect(result.compliance_recommendations).to eql(compliance["recommendations"]) 195 | expect(result.compliance_references).to eql(compliance["references"]) 196 | expect(result.to_hash).to be_kind_of(Hash) 197 | end 198 | end 199 | 200 | context "when setting grade" do 201 | it "should allow setting of the grade information" do 202 | compliance = { 203 | "policy" => "Test Policy", 204 | "compliant" => true, 205 | "recommendations" => ["do this", "do that"], 206 | "references" => ["https://reference.example.com"], 207 | } 208 | result = SSHScan::Result.new() 209 | result.set_compliance = compliance 210 | result.grade = "D" 211 | 212 | expect(result.compliance_policy).to eql(compliance["policy"]) 213 | expect(result.compliant?).to eql(compliance["compliant"]) 214 | expect(result.compliance_recommendations).to eql(compliance["recommendations"]) 215 | expect(result.compliance_references).to eql(compliance["references"]) 216 | expect(result.grade).to eql("D") 217 | end 218 | end 219 | 220 | context "when dealing with errors" do 221 | it "should append errors " do 222 | compliance = { 223 | "policy" => "Test Policy", 224 | "compliant" => true, 225 | "recommendations" => ["do this", "do that"], 226 | "references" => ["https://reference.example.com"], 227 | } 228 | result = SSHScan::Result.new() 229 | result.set_compliance = compliance 230 | result.error = "This is an error" 231 | 232 | expect(result.to_hash).to be_kind_of(Hash) 233 | expect(result.to_hash["error"]).to eql("This is an error") 234 | end 235 | end 236 | 237 | 238 | end -------------------------------------------------------------------------------- /spec/ssh_scan/ssh_fp_spec.rb: -------------------------------------------------------------------------------- 1 | # require 'spec_helper' 2 | # require 'rspec' 3 | # require 'ssh_scan/ssh_fp' 4 | 5 | # describe SSHScan::SshFp do 6 | # context "when querying for an SSHFP record" do 7 | # it "should query the record and return fptype, algo, and hex" do 8 | # fqdn = "myserverplace.de" 9 | # sshfp = SSHScan::SshFp.new() 10 | 11 | # expect(sshfp.query(fqdn)).to eq( 12 | # [ 13 | # { "algo"=>"ed25519", 14 | # "fptype"=>"sha1", 15 | # "hex"=>"69:ac:08:0c:cf:6c:d5:2f:47:88:37:3b:d4:dc:a2:17:31:e6:97:13"}, 16 | # { "algo"=>"ecdsa", 17 | # "fptype"=>"sha1", 18 | # "hex"=>"7c:4b:9b:91:05:d6:a0:d7:aa:cf:44:53:4a:78:00:fc:10:46:66:83"}, 19 | # { "algo"=>"ed25519", 20 | # "fptype"=>"sha256", 21 | # "hex"=> "7c:ae:4f:f9:42:89:9f:8e:15:5b:fc:67:5e:72:e4:14:6a:1b:f4:10:79:77:fe:73:c6:cf:fa:8f:3f:da:8f:c3"}, 22 | # { "algo"=>"ecdsa", 23 | # "fptype"=>"sha256", 24 | # "hex"=> "cb:64:93:b1:0e:11:03:ff:1d:ba:b8:69:89:cf:a9:6f:a5:23:70:ac:33:ef:e6:d4:68:a5:f7:0b:8d:32:38:69"} 25 | # ].sort_by { |k| k["hex"] } 26 | # ) 27 | # end 28 | 29 | # it "should query the record and return nil" do 30 | # fqdn = "ssh.mozilla.com" 31 | # sshfp = SSHScan::SshFp.new() 32 | # expect(sshfp.query(fqdn)).to eq([]) 33 | # end 34 | # end 35 | # end -------------------------------------------------------------------------------- /spec/ssh_scan/string_ext_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require 'pathname' 4 | require 'string_ext' 5 | require 'resolv' 6 | 7 | describe String do 8 | context "when unhexing a string" do 9 | testing_sample = "48656C6C6F20576F726C6421" 10 | it "should load all the attributes properly" do 11 | expected_unhexedstr = "Hello World!" 12 | test_result = testing_sample.unhexify 13 | expect(test_result).to eql(expected_unhexedstr) 14 | end 15 | end 16 | 17 | context "when hexing a string" do 18 | testing_sample = "Hello World!" 19 | it "should load all the attributes properly" do 20 | expected_hexedstr = "48656c6c6f20576f726c6421" 21 | test_result = testing_sample.hexify 22 | expect(test_result).to eql(expected_hexedstr) 23 | end 24 | end 25 | 26 | context "when verifying an IP address" do 27 | testing_target = "127.0.0.1" 28 | it "should load all the attributes properly" do 29 | test_result = testing_target.ip_addr? 30 | expect(test_result).to eql(true) 31 | end 32 | end 33 | 34 | context "when resolving a DNS name as IPv4" do 35 | testing_dns = "github.com" 36 | it "should load all the attributes properly" do 37 | test_result = testing_dns.resolve_fqdn_as_ipv4.to_s 38 | expect(test_result).to match(Resolv::IPv4::Regex256) 39 | end 40 | end 41 | 42 | context "when resolving a DNS name into IP address" do 43 | testing_host = "localhost" 44 | it "should load all the attributes properly" do 45 | expected_result = /^::1$|^127.0.0.1$/ 46 | test_result = testing_host.resolve_fqdn 47 | expect(test_result).to match(expected_result) 48 | end 49 | end 50 | 51 | context "when verifying a DNS name and IP address" do 52 | testing_dns = "google.com" 53 | it "should load all the attributes properly" do 54 | test_result = testing_dns.fqdn? 55 | expect(test_result).to eql(true) 56 | end 57 | end 58 | 59 | end -------------------------------------------------------------------------------- /spec/ssh_scan/target_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require 'ssh_scan/target_parser' 4 | 5 | describe SSHScan::TargetParser do 6 | context "FQDN without port" do 7 | it "should return an array containing that URL" do 8 | target_parser = SSHScan::TargetParser.new() 9 | expect(target_parser.enumerateIPRange("github.com", nil)).to eq( 10 | ["github.com"] 11 | ) 12 | end 13 | end 14 | 15 | context "FQDN with port" do 16 | it "should return an array containing that URL" do 17 | target_parser = SSHScan::TargetParser.new() 18 | expect(target_parser.enumerateIPRange("github.com", 33)).to eq( 19 | ["github.com:33"] 20 | ) 21 | end 22 | end 23 | 24 | context "IPv4 without port" do 25 | it "should return an array containing that IPv4" do 26 | target_parser = SSHScan::TargetParser.new() 27 | expect(target_parser.enumerateIPRange("192.168.1.1", nil)).to eq( 28 | ["192.168.1.1"] 29 | ) 30 | end 31 | end 32 | 33 | context "IPv4 with port" do 34 | it "should return an array containing that IPv4" do 35 | target_parser = SSHScan::TargetParser.new() 36 | expect(target_parser.enumerateIPRange("192.168.1.1", 33)).to eq( 37 | ["192.168.1.1:33"] 38 | ) 39 | end 40 | end 41 | 42 | context "IPv4 with subnet mask specified without port" do 43 | it "should return an array containing all the IPv4 in that range" do 44 | target_parser = SSHScan::TargetParser.new() 45 | expect(target_parser.enumerateIPRange("192.168.1.0/30", nil)).to eq( 46 | ["192.168.1.1", "192.168.1.2"] 47 | ) 48 | end 49 | end 50 | 51 | context "IPv4 with subnet mask specified with port" do 52 | it "should return an array containing all the IPv4 in that range" do 53 | target_parser = SSHScan::TargetParser.new() 54 | expect(target_parser.enumerateIPRange("192.168.1.0/30", 33)).to eq( 55 | ["192.168.1.1:33", "192.168.1.2:33"] 56 | ) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/ssh_scan/version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec' 3 | require 'ssh_scan/version' 4 | 5 | describe SSHScan::VERSION do 6 | it "SSHScan::VERSION should be a string" do 7 | expect(SSHScan::VERSION).to be_kind_of(::String) 8 | end 9 | 10 | it "SSHScan::VERSION should have 3 levels" do 11 | expect(SSHScan::VERSION.split('.').size).to eql(3) 12 | end 13 | 14 | it "SSHScan::VERSION should have a number between 1-20 for each octet" do 15 | SSHScan::VERSION.split('.').each do |octet| 16 | expect(octet.to_i).to be >= 0 17 | expect(octet.to_i).to be <= 60 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /ssh_scan.gemspec: -------------------------------------------------------------------------------- 1 | $: << "lib" 2 | require 'ssh_scan/version' 3 | require 'date' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'ssh_scan' 7 | s.version = SSHScan::VERSION 8 | s.authors = ["Jonathan Claudius", "Jinank Jain", "Harsh Vardhan", "Rishabh Saxena", "Ashish Gaurav"] 9 | s.date = Date.today.to_s 10 | s.email = 'jclaudius@mozilla.com' 11 | s.platform = Gem::Platform::RUBY 12 | s.files = Dir.glob("lib/**/*") + 13 | Dir.glob("bin/**/*") + 14 | Dir.glob("config/**/*") + 15 | Dir.glob("data/**/*") + 16 | Dir.glob("policies/**/*") + 17 | [".gitignore", 18 | ".rspec", 19 | ".travis.yml", 20 | "CONTRIBUTING.md", 21 | "Gemfile", 22 | "Rakefile", 23 | "README.md", 24 | "ssh_scan.gemspec"] 25 | s.license = "ruby" 26 | s.require_paths = ["lib"] 27 | s.executables = s.files.grep(%r{^bin/[^\/]+$}) { |f| File.basename(f) } 28 | s.summary = 'Ruby-based SSH Scanner' 29 | s.description = 'A Ruby-based SSH scanner for configuration and policy scanning' 30 | s.homepage = 'http://rubygems.org/gems/ssh_scan' 31 | s.metadata["yard.run"] = "yri" # use "yard" to build full HTML docs 32 | 33 | s.add_dependency('bindata', '2.4.3') 34 | s.add_dependency('netaddr', '2.0.4') 35 | s.add_dependency('net-ssh', '6.0.2') 36 | s.add_dependency('ed25519', '1.2.4') 37 | s.add_dependency('bcrypt_pbkdf', '1.0.1') 38 | s.add_dependency('sshkey') 39 | s.add_development_dependency('pry', '0.11.3') 40 | s.add_development_dependency('rspec', '3.7.0') 41 | s.add_development_dependency('rspec-its', '1.2.0') 42 | s.add_development_dependency "rake", ">= 12.3.3" 43 | s.add_development_dependency('rubocop') 44 | end 45 | --------------------------------------------------------------------------------