├── .coveragerc ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci-python.yml │ ├── codeql-analysis-python.yml │ ├── deploy-pypi.yml │ └── release-pontos-manually.yml ├── .gitignore ├── .mergify.yml ├── .pylintrc ├── CHANGELOG.md ├── COPYING ├── MANIFEST.in ├── README.md ├── RELEASE.md ├── doc ├── HTML.xsl ├── INSTALL-ospd-scanner ├── INSTALL-ospd-scanner.md ├── OSP.xml ├── USAGE-ospd-scanner ├── USAGE-ospd-scanner.md ├── example-ospd-logging.conf ├── example-ospd.conf ├── generate └── rnc.xsl ├── ospd ├── __init__.py ├── __version__.py ├── command │ ├── __init__.py │ ├── command.py │ ├── initsubclass.py │ └── registry.py ├── config.py ├── cvss.py ├── datapickler.py ├── errors.py ├── logger.py ├── main.py ├── misc.py ├── network.py ├── ospd.py ├── ospd_ssh.py ├── parser.py ├── protocol.py ├── resultlist.py ├── scan.py ├── server.py ├── timer.py ├── vtfilter.py ├── vts.py └── xml.py ├── poetry.lock ├── poetry.toml ├── pyproject.toml ├── setup.py └── tests ├── __init__.py ├── command ├── __init__.py ├── test_command.py ├── test_commands.py └── test_registry.py ├── helper.py ├── test_argument_parser.py ├── test_cvss.py ├── test_datapickler.py ├── test_errors.py ├── test_port_convert.py ├── test_protocol.py ├── test_scan_and_result.py ├── test_ssh_daemon.py ├── test_target_convert.py ├── test_vts.py └── test_xml.py /.coveragerc: -------------------------------------------------------------------------------- 1 | # Configuration for coverage.py 2 | [run] 3 | omit = 4 | tests/* 5 | */__init__.py 6 | source = 7 | ospd 8 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # default reviewers 2 | * @greenbone/ospd-maintainers 3 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **What**: 2 | 3 | 7 | 8 | **Why**: 9 | 10 | 11 | 12 | **How**: 13 | 14 | 23 | 24 | **Checklist**: 25 | 26 | 27 | 28 | 29 | 30 | - [ ] Tests 31 | - [ ] [CHANGELOG](https://github.com/greenbone/ospd/blob/main/CHANGELOG.md) Entry 32 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/ci-python.yml: -------------------------------------------------------------------------------- 1 | name: Build and test Python package 2 | 3 | on: 4 | push: 5 | branches: [ main, stable, oldstable, middleware ] 6 | pull_request: 7 | branches: [ main, stable, oldstable, middleware ] 8 | 9 | jobs: 10 | linting: 11 | name: Linting 12 | runs-on: 'ubuntu-latest' 13 | strategy: 14 | matrix: 15 | python-version: 16 | - 3.7 17 | - 3.8 18 | - 3.9 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install poetry and dependencies 26 | uses: greenbone/actions/poetry@v1 27 | - name: Check with black, pylint and pontos.version 28 | uses: greenbone/actions/lint-python@v1 29 | with: 30 | packages: ospd tests 31 | 32 | test: 33 | name: Run all tests 34 | runs-on: 'ubuntu-latest' 35 | strategy: 36 | matrix: 37 | python-version: 38 | - 3.7 39 | - 3.8 40 | - 3.9 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: Set up Python ${{ matrix.python-version }} 44 | uses: actions/setup-python@v2 45 | with: 46 | python-version: ${{ matrix.python-version }} 47 | - name: Install poetry and dependencies 48 | uses: greenbone/actions/poetry@v1 49 | - name: Run unit tests 50 | run: poetry run python -m unittest 51 | 52 | codecov: 53 | name: Upload coverage to codecov.io 54 | needs: test 55 | runs-on: 'ubuntu-latest' 56 | steps: 57 | - uses: actions/checkout@v2 58 | - name: Set up Python 59 | uses: actions/setup-python@v2 60 | with: 61 | python-version: 3.8 62 | - name: Install poetry and dependencies 63 | uses: greenbone/actions/poetry@v1 64 | - name: Calculate and upload coverage to codecov.io 65 | uses: greenbone/actions/coverage-python@v1 66 | with: 67 | test-command: -m unittest -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis-python.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ main, stable, oldstable, middleware ] 6 | pull_request: 7 | branches: [ main, stable, oldstable, middleware ] 8 | schedule: 9 | - cron: '30 5 * * 0' # 5:30h on Sundays 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'python' ] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v2 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v1 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | - name: Perform CodeQL Analysis 35 | uses: github/codeql-action/analyze@v1 -------------------------------------------------------------------------------- /.github/workflows/deploy-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Deploy on PyPI 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: 3.8 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | python -m pip install --upgrade poetry 20 | python -m pip install --upgrade twine 21 | - name: Build and publish 22 | env: 23 | TWINE_USERNAME: __token__ 24 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 25 | run: | 26 | poetry build 27 | twine upload dist/* -------------------------------------------------------------------------------- /.github/workflows/release-pontos-manually.yml: -------------------------------------------------------------------------------- 1 | name: Manually release gvm-libs with pontos 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | release-patch: 8 | env: 9 | GITHUB_USER: ${{ secrets.GREENBONE_BOT }} 10 | GITHUB_MAIL: ${{ secrets.GREENBONE_BOT_MAIL }} 11 | GITHUB_TOKEN: ${{ secrets.GREENBONE_BOT_TOKEN }} 12 | GPG_KEY: ${{ secrets.GPG_KEY }} 13 | GPG_FINGERPRINT: ${{ secrets.GPG_FINGERPRINT }} 14 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 15 | name: Release patch with pontos 16 | runs-on: 'ubuntu-latest' 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | persist-credentials: false 21 | - name: Set up Python 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: 3.8 25 | - name: Install poetry and dependencies 26 | uses: greenbone/actions/poetry@v1 27 | - name: Tell git who I am 28 | run: | 29 | git config --global user.name "${{ env.GITHUB_USER }}" 30 | git config --global user.email "${{ env.GITHUB_MAIL }}" 31 | git remote set-url origin https://${{ env.GITHUB_TOKEN }}@github.com/${{ github.repository }} 32 | - run: echo "Current Branch is $GITHUB_BASE_REF" 33 | - name: Prepare patch release with pontos 34 | run: | 35 | poetry run pontos-release prepare --patch 36 | echo "VERSION=$(poetry run pontos-version show)" >> $GITHUB_ENV 37 | - name: Release with pontos 38 | run: | 39 | poetry run pontos-release release 40 | - name: Import key from secrets 41 | run: | 42 | echo -e "${{ env.GPG_KEY }}" >> tmp.file 43 | gpg \ 44 | --pinentry-mode loopback \ 45 | --passphrase ${{ env.GPG_PASSPHRASE }} \ 46 | --import tmp.file 47 | rm tmp.file 48 | - name: Sign with pontos-release sign 49 | run: | 50 | echo "Signing assets for ${{env.VERSION}}" 51 | poetry run pontos-release sign \ 52 | --signing-key ${{ env.GPG_FINGERPRINT }} \ 53 | --passphrase ${{ env.GPG_PASSPHRASE }} \ 54 | --release-version ${{ env.VERSION }} 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | *.log 4 | .egg 5 | *.egg-info 6 | dist 7 | build 8 | _build 9 | .idea 10 | .vscode 11 | .coverage 12 | .venv 13 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | # backports from main branch 3 | - name: backport main patches to stable branch 4 | conditions: 5 | - base=main 6 | - label=backport-to-stable 7 | actions: 8 | backport: 9 | branches: 10 | - stable 11 | 12 | - name: backport main patches to oldstable branch 13 | conditions: 14 | - base=main 15 | - label=backport-to-oldstable 16 | actions: 17 | backport: 18 | branches: 19 | - oldstable 20 | 21 | # backports from upcoming release branch 22 | - name: backport stable patches to main branch 23 | conditions: 24 | - base=stable 25 | - label=backport-to-main 26 | actions: 27 | backport: 28 | branches: 29 | - main 30 | 31 | - name: backport stable patches to oldstable branch 32 | conditions: 33 | - base=stable 34 | - label=backport-to-oldstable 35 | actions: 36 | backport: 37 | branches: 38 | - oldstable 39 | 40 | # backports from current release branch 41 | - name: backport oldstable patches to main branch 42 | conditions: 43 | - base=oldstable 44 | - label=backport-to-main 45 | actions: 46 | backport: 47 | branches: 48 | - main 49 | 50 | - name: backport oldstable patches to stable branch 51 | conditions: 52 | - base=oldstable 53 | - label=backport-to-stable 54 | actions: 55 | backport: 56 | branches: 57 | - stable -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code 6 | extension-pkg-whitelist=lxml 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns=docs 15 | 16 | # Pickle collected data for later comparisons. 17 | persistent=yes 18 | 19 | # When enabled, pylint would attempt to guess common misconfiguration and emit 20 | # user-friendly hints instead of false-positive error messages 21 | suggestion-mode=yes 22 | 23 | # Allow loading of arbitrary C extensions. Extensions are imported into the 24 | # active Python interpreter and may run arbitrary code. 25 | unsafe-load-any-extension=no 26 | 27 | 28 | [MESSAGES CONTROL] 29 | 30 | # Only show warnings with the listed confidence levels. Leave empty to show 31 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 32 | confidence= 33 | 34 | # Disable the message, report, category or checker with the given id(s). You 35 | # can either give multiple identifiers separated by comma (,) or put this 36 | # option multiple times (only on the command line, not in the configuration 37 | # file where it should appear only once).You can also use "--disable=all" to 38 | # disable everything first and then reenable specific checks. For example, if 39 | # you want to run only the similarities checker, you can use "--disable=all 40 | # --enable=similarities". If you want to run only the classes checker, but have 41 | # no Warning level messages displayed, use"--disable=all --enable=classes 42 | # --disable=W" 43 | 44 | # bad-continuation is disabled because of a bug in pylint. 45 | # See https://github.com/ambv/black/issues/48 and https://github.com/PyCQA/pylint/issues/289 46 | 47 | disable=len-as-condition, 48 | attribute-defined-outside-init, 49 | missing-docstring, 50 | bad-continuation, 51 | R 52 | 53 | #disable=print-statement, 54 | # parameter-unpacking, 55 | # unpacking-in-except, 56 | # old-raise-syntax, 57 | # backtick, 58 | # long-suffix, 59 | # old-ne-operator, 60 | # old-octal-literal, 61 | # import-star-module-level, 62 | # non-ascii-bytes-literal, 63 | # raw-checker-failed, 64 | # bad-inline-option, 65 | # locally-disabled, 66 | # locally-enabled, 67 | # file-ignored, 68 | # suppressed-message, 69 | # useless-suppression, 70 | # deprecated-pragma, 71 | # apply-builtin, 72 | # basestring-builtin, 73 | # buffer-builtin, 74 | # cmp-builtin, 75 | # coerce-builtin, 76 | # execfile-builtin, 77 | # file-builtin, 78 | # long-builtin, 79 | # raw_input-builtin, 80 | # reduce-builtin, 81 | # standarderror-builtin, 82 | # unicode-builtin, 83 | # xrange-builtin, 84 | # coerce-method, 85 | # delslice-method, 86 | # getslice-method, 87 | # setslice-method, 88 | # no-absolute-import, 89 | # old-division, 90 | # dict-iter-method, 91 | # dict-view-method, 92 | # next-method-called, 93 | # metaclass-assignment, 94 | # indexing-exception, 95 | # raising-string, 96 | # reload-builtin, 97 | # oct-method, 98 | # hex-method, 99 | # nonzero-method, 100 | # cmp-method, 101 | # input-builtin, 102 | # round-builtin, 103 | # intern-builtin, 104 | # unichr-builtin, 105 | # map-builtin-not-iterating, 106 | # zip-builtin-not-iterating, 107 | # range-builtin-not-iterating, 108 | # filter-builtin-not-iterating, 109 | # using-cmp-argument, 110 | # eq-without-hash, 111 | # div-method, 112 | # idiv-method, 113 | # rdiv-method, 114 | # exception-message-attribute, 115 | # invalid-str-codec, 116 | # sys-max-int, 117 | # bad-python3-import, 118 | # deprecated-string-function, 119 | # deprecated-str-translate-call, 120 | # deprecated-itertools-function, 121 | # deprecated-types-field, 122 | # next-method-defined, 123 | # dict-items-not-iterating, 124 | # dict-keys-not-iterating, 125 | # dict-values-not-iterating 126 | 127 | # Enable the message, report, category or checker with the given id(s). You can 128 | # either give multiple identifier separated by comma (,) or put this option 129 | # multiple time (only on the command line, not in the configuration file where 130 | # it should appear only once). See also the "--disable" option for examples. 131 | enable=c-extension-no-member 132 | 133 | 134 | [REPORTS] 135 | 136 | # Python expression which should return a note less than 10 (10 is the highest 137 | # note). You have access to the variables errors warning, statement which 138 | # respectively contain the number of errors / warnings messages and the total 139 | # number of statements analyzed. This is used by the global evaluation report 140 | # (RP0004). 141 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 142 | 143 | # Template used to display messages. This is a python new-style format string 144 | # used to format the message information. See doc for all details 145 | #msg-template= 146 | 147 | # Set the output format. Available formats are text, parseable, colorized, json 148 | # and msvs (visual studio).You can also give a reporter class, eg 149 | # mypackage.mymodule.MyReporterClass. 150 | output-format=text 151 | 152 | # Tells whether to display a full report or only the messages 153 | reports=no 154 | 155 | # Deactivate the evaluation score. 156 | score=no 157 | 158 | 159 | [REFACTORING] 160 | 161 | # Maximum number of nested blocks for function / method body 162 | max-nested-blocks=5 163 | 164 | # Complete name of functions that never returns. When checking for 165 | # inconsistent-return-statements if a never returning function is called then 166 | # it will be considered as an explicit return statement and no message will be 167 | # printed. 168 | never-returning-functions=optparse.Values,sys.exit 169 | 170 | 171 | [VARIABLES] 172 | 173 | # List of additional names supposed to be defined in builtins. Remember that 174 | # you should avoid to define new builtins when possible. 175 | additional-builtins= 176 | 177 | # Tells whether unused global variables should be treated as a violation. 178 | allow-global-unused-variables=yes 179 | 180 | # List of strings which can identify a callback function by name. A callback 181 | # name must start or end with one of those strings. 182 | callbacks=cb_, 183 | _cb 184 | 185 | # A regular expression matching the name of dummy variables (i.e. expectedly 186 | # not used). 187 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 188 | 189 | # Argument names that match this expression will be ignored. Default to name 190 | # with leading underscore 191 | ignored-argument-names=_.*|^ignored_|^unused_ 192 | 193 | # Tells whether we should check for unused import in __init__ files. 194 | init-import=no 195 | 196 | # List of qualified module names which can have objects that can redefine 197 | # builtins. 198 | redefining-builtins-modules=six.moves,past.builtins,future.builtins 199 | 200 | 201 | [MISCELLANEOUS] 202 | 203 | # List of note tags to take in consideration, separated by a comma. 204 | notes=FIXME, 205 | XXX, 206 | TODO 207 | 208 | 209 | [BASIC] 210 | 211 | # Regular expression which should only match correct argument names 212 | argument-rgx=[a-z_][a-z0-9_]{1,40}$ 213 | 214 | # Regular expression which should only match correct instance attribute names 215 | attr-rgx=[a-z_][a-z0-9_]{1,40}$ 216 | 217 | # Bad variable names which should always be refused, separated by a comma 218 | bad-names=foo, 219 | bar, 220 | baz, 221 | toto, 222 | tutu, 223 | tata 224 | 225 | # Regular expression matching correct class attribute names. 226 | class-attribute-rgx=([a-z_][a-z0-9_]{1,40})|([A-Z_][A-Z0-9_]{1,30})$ 227 | 228 | # Naming style matching correct class names 229 | class-naming-style=PascalCase 230 | 231 | # Naming style matching correct constant names 232 | const-naming-style=UPPER_CASE 233 | 234 | # Minimum line length for functions/classes that require docstrings, shorter 235 | # ones are exempt. 236 | docstring-min-length=3 237 | 238 | # Regular expression which should only match correct function names 239 | function-rgx=[a-z_][a-z0-9_]+$ 240 | 241 | # Good variable names which should always be accepted, separated by a comma 242 | good-names=e, 243 | f, 244 | i, 245 | j, 246 | k, 247 | ex, 248 | Run, 249 | logger, 250 | _ 251 | 252 | # Include a hint for the correct naming format with invalid-name 253 | include-naming-hint=yes 254 | 255 | # Regular expression matching correct inline iteration names. 256 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 257 | 258 | # Regular expression which should only match correct method names 259 | method-rgx=[a-z_][a-z0-9_]+$ 260 | 261 | # Regular expression which should only match correct module names 262 | module-rgx=([a-z]+)|(test_*)$ 263 | 264 | # Regular expression which should only match function or class names that do 265 | # not require a docstring. 266 | no-docstring-rgx=^_ 267 | 268 | # List of decorators that produce properties, such as abc.abstractproperty. Add 269 | # to this list to register other decorators that produce valid properties. 270 | property-classes=abc.abstractproperty 271 | 272 | # Regular expression which should only match correct variable names 273 | variable-rgx=[a-z_][a-z0-9_]+$ 274 | 275 | 276 | [SIMILARITIES] 277 | 278 | # Ignore comments when computing similarities. 279 | ignore-comments=yes 280 | 281 | # Ignore docstrings when computing similarities. 282 | ignore-docstrings=yes 283 | 284 | # Ignore imports when computing similarities. 285 | ignore-imports=no 286 | 287 | # Minimum lines number of a similarity. 288 | min-similarity-lines=4 289 | 290 | 291 | [LOGGING] 292 | 293 | # Logging modules to check that the string format arguments are in logging 294 | # function parameter format 295 | logging-modules=logging 296 | 297 | 298 | [TYPECHECK] 299 | 300 | # List of decorators that produce context managers, such as 301 | # contextlib.contextmanager. Add to this list to register other decorators that 302 | # produce valid context managers. 303 | contextmanager-decorators=contextlib.contextmanager 304 | 305 | # List of members which are set dynamically and missed by pylint inference 306 | # system, and so shouldn't trigger E1101 when accessed. Python regular 307 | # expressions are accepted. 308 | generated-members= 309 | 310 | # Tells whether missing members accessed in mixin class should be ignored. A 311 | # mixin class is detected if its name ends with "mixin" (case insensitive). 312 | ignore-mixin-members=yes 313 | 314 | # This flag controls whether pylint should warn about no-member and similar 315 | # checks whenever an opaque object is returned when inferring. The inference 316 | # can return multiple potential results while evaluating a Python object, but 317 | # some branches might not be evaluated, which results in partial inference. In 318 | # that case, it might be useful to still emit no-member and other checks for 319 | # the rest of the inferred objects. 320 | ignore-on-opaque-inference=yes 321 | 322 | # List of class names for which member attributes should not be checked (useful 323 | # for classes with dynamically set attributes). This supports the use of 324 | # qualified names. 325 | ignored-classes=optparse.Values,thread._local,_thread._local 326 | 327 | # List of module names for which member attributes should not be checked 328 | # (useful for modules/projects where namespaces are manipulated during runtime 329 | # and thus existing member attributes cannot be deduced by static analysis. It 330 | # supports qualified module names, as well as Unix pattern matching. 331 | ignored-modules= 332 | 333 | # Show a hint with possible names when a member name was not found. The aspect 334 | # of finding the hint is based on edit distance. 335 | missing-member-hint=yes 336 | 337 | # The minimum edit distance a name should have in order to be considered a 338 | # similar match for a missing member name. 339 | missing-member-hint-distance=1 340 | 341 | # The total number of similar names that should be taken in consideration when 342 | # showing a hint for a missing member. 343 | missing-member-max-choices=1 344 | 345 | 346 | [FORMAT] 347 | 348 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 349 | expected-line-ending-format=LF 350 | 351 | # Regexp for a line that is allowed to be longer than the limit. 352 | ignore-long-lines=^\s*(# )??$ 353 | 354 | # Number of spaces of indent required inside a hanging or continued line. 355 | indent-after-paren=4 356 | 357 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 358 | # tab). 359 | indent-string=' ' 360 | 361 | # Maximum number of characters on a single line. 362 | max-line-length=80 363 | 364 | # Maximum number of lines in a module 365 | max-module-lines=1000 366 | 367 | # List of optional constructs for which whitespace checking is disabled. `dict- 368 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 369 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 370 | # `empty-line` allows space-only lines. 371 | no-space-check=trailing-comma, 372 | dict-separator 373 | 374 | # Allow the body of a class to be on the same line as the declaration if body 375 | # contains single statement. 376 | single-line-class-stmt=no 377 | 378 | # Allow the body of an if to be on the same line as the test if there is no 379 | # else. 380 | single-line-if-stmt=no 381 | 382 | 383 | [IMPORTS] 384 | 385 | # Allow wildcard imports from modules that define __all__. 386 | allow-wildcard-with-all=no 387 | 388 | # Analyse import fallback blocks. This can be used to support both Python 2 and 389 | # 3 compatible code, which means that the block might have code that exists 390 | # only in one or another interpreter, leading to false positives when analysed. 391 | analyse-fallback-blocks=no 392 | 393 | # Deprecated modules which should not be used, separated by a comma 394 | deprecated-modules=optparse,tkinter.tix 395 | 396 | # Create a graph of external dependencies in the given file (report RP0402 must 397 | # not be disabled) 398 | ext-import-graph= 399 | 400 | # Create a graph of every (i.e. internal and external) dependencies in the 401 | # given file (report RP0402 must not be disabled) 402 | import-graph= 403 | 404 | # Create a graph of internal dependencies in the given file (report RP0402 must 405 | # not be disabled) 406 | int-import-graph= 407 | 408 | # Force import order to recognize a module as part of the standard 409 | # compatibility libraries. 410 | known-standard-library= 411 | 412 | # Force import order to recognize a module as part of a third party library. 413 | known-third-party=enchant 414 | 415 | 416 | [DESIGN] 417 | 418 | # Maximum number of arguments for function / method 419 | max-args=15 420 | 421 | # Maximum number of attributes for a class (see R0902). 422 | max-attributes=20 423 | 424 | # Maximum number of boolean expressions in a if statement 425 | max-bool-expr=5 426 | 427 | # Maximum number of branch for function / method body 428 | max-branches=12 429 | 430 | # Maximum number of locals for function / method body 431 | max-locals=15 432 | 433 | # Maximum number of parents for a class (see R0901). 434 | max-parents=7 435 | 436 | # Maximum number of public methods for a class (see R0904). 437 | max-public-methods=30 438 | 439 | # Maximum number of return / yield for function / method body 440 | max-returns=6 441 | 442 | # Maximum number of statements in function / method body 443 | max-statements=50 444 | 445 | # Minimum number of public methods for a class (see R0903). 446 | min-public-methods=0 447 | 448 | 449 | [CLASSES] 450 | 451 | # List of method names used to declare (i.e. assign) instance attributes. 452 | defining-attr-methods=__init__, 453 | __new__, 454 | setUp 455 | 456 | # List of member names, which should be excluded from the protected access 457 | # warning. 458 | exclude-protected=_asdict, 459 | _fields, 460 | _replace, 461 | _source, 462 | _make 463 | 464 | # List of valid names for the first argument in a class method. 465 | valid-classmethod-first-arg=cls 466 | 467 | # List of valid names for the first argument in a metaclass class method. 468 | valid-metaclass-classmethod-first-arg=mcs 469 | 470 | 471 | [EXCEPTIONS] 472 | 473 | # Exceptions that will emit a warning when being caught. Defaults to 474 | # "Exception" 475 | overgeneral-exceptions=Exception 476 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | ## [21.10.0] (unreleased) 8 | 9 | ### Added 10 | - Add method to validate port lists. 11 | [#393](https://github.com/greenbone/ospd/pull/393) 12 | [#395](https://github.com/greenbone/ospd/pull/395) 13 | ### Changed 14 | - Use better defaults for file paths and permissions [#429](https://github.com/greenbone/ospd/pull/429) 15 | - Downgrade required version for psutil to 5.5.1 [#453](https://github.com/greenbone/ospd/pull/453) 16 | 17 | ### Deprecated 18 | ### Removed 19 | ### Fixed 20 | - Fix resume scan. [#464](https://github.com/greenbone/ospd/pull/464) 21 | - Fix get_status. Backport #471.[#472](https://github.com/greenbone/ospd/pull/472) 22 | 23 | [Unreleased]: https://github.com/greenbone/ospd/compare/v21.4.3...HEAD 24 | 25 | ## [21.4.3] - 2021-08-04 26 | 27 | ### Changed 28 | - Set Log Timestamp to UTC. [#394](https://github.com/greenbone/ospd-openvas/pull/394) 29 | - Stopping scan. [#432](https://github.com/greenbone/ospd/pull/432) 30 | 31 | ### Deprecated 32 | ### Fixed 33 | ### Removed 34 | - Remove dry run from ospd. [#402](https://github.com/greenbone/ospd-openvas/pull/402) 35 | 36 | [21.10.0]: https://github.com/greenbone/ospd/compare/stable...main 37 | 38 | ## [21.4.0] (unreleased) 39 | 40 | ### Added 41 | - Add target option for supplying dedicated port list for alive detection (Boreas only) via OSP. [#323](https://github.com/greenbone/ospd/pull/323) 42 | - Add target option for supplying alive test methods via separate elements. [#329](https://github.com/greenbone/ospd/pull/329) 43 | 44 | ### Changed 45 | - Use better defaults for file paths and permissions [#429](https://github.com/greenbone/ospd/pull/429) 46 | 47 | ### Removed 48 | - Remove python3.5 support and deprecated methods. [#316](https://github.com/greenbone/ospd/pull/316) 49 | 50 | [21.4.0]: https://github.com/greenbone/ospd/compare/oldstable...main 51 | 52 | ## [20.8.3] (Unreleased) 53 | ### Added 54 | ### Changed 55 | ### Deprecated 56 | ### Removed 57 | ### Fixed 58 | - Do not start all queued scans simultaneously once available memory is enough. [#401](https://github.com/greenbone/ospd/pull/401) 59 | - Remove the pid file if there is no process for the pid or the process name does not match. [#405](https://github.com/greenbone/ospd/pull/405) 60 | - Fix regex of gvmcg titles for get_performance command. [#413](https://github.com/greenbone/ospd/pull/413) 61 | 62 | [Unreleased]: https://github.com/greenbone/ospd/compare/v20.8.2...HEAD 63 | 64 | ## [20.8.2] (2021-02-01) 65 | 66 | ### Added 67 | - Allow the scanner to update total count of hosts. [#332](https://github.com/greenbone/ospd/pull/332) 68 | - Add more debug logging. [#352](https://github.com/greenbone/ospd/pull/352) 69 | - Set end_time for interrupted scans. [#353](https://github.com/greenbone/ospd/pull/353) 70 | - Add method to get a single host scan progress. [#363](https://github.com/greenbone/ospd/pull/363) 71 | 72 | ### Fixed 73 | - Fix OSP version. [#326](https://github.com/greenbone/ospd/pull/326) 74 | - Use empty string instead of None for credential. [#335](https://github.com/greenbone/ospd/pull/335) 75 | - Fix target_to_ipv4_short(). [#338](https://github.com/greenbone/ospd/pull/338) 76 | - Fix malformed target. [#341](https://github.com/greenbone/ospd/pull/341) 77 | - Initialize end_time with create_scan. [#354](https://github.com/greenbone/ospd/pull/354) 78 | - Fix get_count_total(). Accept -1 value set by the server. [#355](https://github.com/greenbone/ospd/pull/355) 79 | - Fix get_count_total(). Consider 0 value set by the server. [#366](https://github.com/greenbone/ospd/pull/366) 80 | - Remove exclude hosts which do not belong to the target for the scan progress calculation. 81 | [#377](https://github.com/greenbone/ospd/pull/377) 82 | [#380](https://github.com/greenbone/ospd/pull/380) 83 | 84 | [20.8.2]: https://github.com/greenbone/ospd/compare/v20.8.1...oldstable 85 | 86 | ## [20.8.1] (2020-08-12) 87 | 88 | ### Fixed 89 | - Fix deploy and upload to pypi. [#312](https://github.com/greenbone/ospd/pull/312) 90 | - Fix metadata for Python wheel distributable [#313](https://github.com/greenbone/ospd/pull/313) 91 | 92 | [20.8.1]: https://github.com/greenbone/ospd/compare/v20.8.0...v20.8.1 93 | 94 | ## [20.8.0] (2020-08-11) 95 | 96 | ### Added 97 | - Add solution method to solution of vt object. [#166](https://github.com/greenbone/ospd/pull/166) 98 | - Add wait_for_children(). [#167](https://github.com/greenbone/ospd/pull/167) 99 | - Extend osp to accept target options. [#194](https://github.com/greenbone/ospd/pull/194) 100 | - Accept reverse_lookup_only and reverse_lookup_unify target's options. [#195](https://github.com/greenbone/ospd/pull/195) 101 | - Add 'total' and 'sent' attributes to element for cmd response. [#206](https://github.com/greenbone/ospd/pull/206) 102 | - Add new get_memory_usage command. [#207](https://github.com/greenbone/ospd/pull/207) 103 | - Add lock-file-dir configuration option. [#218](https://github.com/greenbone/ospd/pull/218) 104 | - Add details attribute to get_vts command. [#222](https://github.com/greenbone/ospd/pull/222) 105 | - Add [pontos](https://github.com/greenbone/pontos) as dev dependency for 106 | managing the version information in ospd [#254](https://github.com/greenbone/ospd/pull/254) 107 | - Add more info about scan progress with progress attribute in get_scans cmd. [#266](https://github.com/greenbone/ospd/pull/266) 108 | - Add support for scan queuing 109 | [#278](https://github.com/greenbone/ospd/pull/278) 110 | [#279](https://github.com/greenbone/ospd/pull/279) 111 | [#281](https://github.com/greenbone/ospd/pull/281) 112 | - Extend results with optional argument URI [#282](https://github.com/greenbone/ospd/pull/282) 113 | - Add new scan status INTERRUPTED. 114 | [#288](https://github.com/greenbone/ospd/pull/288) 115 | [#289](https://github.com/greenbone/ospd/pull/289) 116 | - Extend get_vts with attribute version_only and return the version [#291](https://github.com/greenbone/ospd/pull/291) 117 | - Allow to set all openvas parameters which are not strict openvas only parameters via osp. [#301](https://github.com/greenbone/ospd/pull/301) 118 | 119 | ### Changes 120 | - Modify __init__() method and use new syntax for super(). [#186](https://github.com/greenbone/ospd/pull/186) 121 | - Create data manager and spawn new process to keep the vts dictionary. [#191](https://github.com/greenbone/ospd/pull/191) 122 | - Update daemon start sequence. Run daemon.check before daemon.init now. [#197](https://github.com/greenbone/ospd/pull/197) 123 | - Improve get_vts cmd response, sending the vts piece by piece.[#201](https://github.com/greenbone/ospd/pull/201) 124 | - Start the server before initialize to respond to the client.[#209](https://github.com/greenbone/ospd/pull/209) 125 | - Use an iterator to get the vts when get_vts cmd is called. [#216](https://github.com/greenbone/ospd/pull/216) 126 | - Update license to AGPL-3.0+ [#241](https://github.com/greenbone/ospd/pull/241) 127 | - Replaced pipenv with poetry for dependency management. `poetry install` works 128 | a bit different then `pipenv install`. It installs dev packages by default and 129 | also ospd in editable mode. This means after running poetry install ospd will 130 | directly be importable in the virtual python environment. [#252](https://github.com/greenbone/ospd/pull/252) 131 | - Progress bar calculation does not take in account the dead hosts. [#266](https://github.com/greenbone/ospd/pull/266) 132 | - Show progress as integer for get_scans. [#269](https://github.com/greenbone/ospd/pull/269) 133 | - Make scan_id attribute mandatory for get_scans. [#270](https://github.com/greenbone/ospd/pull/270) 134 | - Ignore subsequent SIGINT once inside exit_cleanup(). [#273](https://github.com/greenbone/ospd/pull/273) 135 | - Simplify start_scan() [#275](https://github.com/greenbone/ospd/pull/275) 136 | - Make ospd-openvas to shut down gracefully 137 | [#302](https://github.com/greenbone/ospd/pull/302) 138 | [#307](https://github.com/greenbone/ospd/pull/307) 139 | - Do not add all params which are in the OSPD_PARAMS dict to the params which are set as scan preferences. [#305](https://github.com/greenbone/ospd/pull/305) 140 | 141 | ### Fixed 142 | - Fix stop scan. Wait for the scan process to be stopped before delete it from the process table. [#204](https://github.com/greenbone/ospd/pull/204) 143 | - Fix get_scanner_details(). [#210](https://github.com/greenbone/ospd/pull/210) 144 | - Fix thread lib leak using daemon mode for python 3.7. [#272](https://github.com/greenbone/ospd/pull/272) 145 | - Fix scan progress in which all hosts are dead or excluded. [#295](https://github.com/greenbone/ospd/pull/295) 146 | - Stop all running scans before exiting [#303](https://github.com/greenbone/ospd/pull/303) 147 | - Fix start of parallel queued task. [#304](https://github.com/greenbone/ospd/pull/304) 148 | - Strip trailing commas from the target list. [#306](https://github.com/greenbone/ospd/pull/306) 149 | 150 | ### Removed 151 | - Remove support for resume task. [#266](https://github.com/greenbone/ospd/pull/266) 152 | 153 | [20.8.0]: https://github.com/greenbone/ospd/compare/ospd-2.0...oldstable 154 | 155 | ## [2.0.1] 156 | 157 | ### Added 158 | - Add clean_forgotten_scans(). [#171](https://github.com/greenbone/ospd/pull/171) 159 | - Extend OSP with finished_hosts to improve resume task. [#177](https://github.com/greenbone/ospd/pull/177) 160 | 161 | ### Changed 162 | - Set loglevel to debug for some message. [#159](https://github.com/greenbone/ospd/pull/159) 163 | - Improve error handling when stop a scan. [#163](https://github.com/greenbone/ospd/pull/163) 164 | - Check the existence and status of an scan_id. [#179](https://github.com/greenbone/ospd/pull/179) 165 | 166 | ### Fixed 167 | - Fix set permission in unix socket. [#157](https://github.com/greenbone/ospd/pull/157) 168 | - Fix VT filter. [#165](https://github.com/greenbone/ospd/pull/165) 169 | - Remove from exclude_host list the hosts passed as finished too. [#183](https://github.com/greenbone/ospd/pull/183) 170 | 171 | [2.0.1]: https://github.com/greenbone/ospd/compare/v2.0.0...ospd-2.0 172 | 173 | ## [2.0.0] (2019-10-11) 174 | 175 | ### Added 176 | - Add OSP command get_vts and the vts dictionary. [#12](https://github.com/greenbone/ospd/pull/12) [#60](https://github.com/greenbone/ospd/pull/60) [#72](https://github.com/greenbone/ospd/pull/72) [#73](https://github.com/greenbone/ospd/pull/73) [#93](https://github.com/greenbone/ospd/pull/93) 177 | - Add optional custom elements for VT information. [#15](https://github.com/greenbone/ospd/pull/15) 178 | - Allow clients to choose TLS versions > 1.0. [#18](https://github.com/greenbone/ospd/pull/18) 179 | - Add element "vts" to parameters for starting scans. [#19](https://github.com/greenbone/ospd/pull/19) [#26](https://github.com/greenbone/ospd/pull/26) 180 | - Add dummy stop_scan method to be implemented in the wrapper. [#24](https://github.com/greenbone/ospd/pull/24) [#53](https://github.com/greenbone/ospd/pull/53) [#129](https://github.com/greenbone/ospd/pull/129) 181 | - Extend OSP command get_vts with vt_params. [#28](https://github.com/greenbone/ospd/pull/28) 182 | - Add vt_selection to start_scan command. [#31](https://github.com/greenbone/ospd/pull/31) [#58](https://github.com/greenbone/ospd/pull/58) [#105](https://github.com/greenbone/ospd/pull/105) 183 | - Add support for multi-target task adding targets with their own port list, credentials and host list to start_scan command. [#34](https://github.com/greenbone/ospd/pull/34) [#38](https://github.com/greenbone/ospd/pull/38) [#39](https://github.com/greenbone/ospd/pull/39) [#41](https://github.com/greenbone/ospd/pull/41)) [#127](https://github.com/greenbone/ospd/pull/127) [#134](https://github.com/greenbone/ospd/pull/134) 184 | - Add support for parallel scans. [#42](https://github.com/greenbone/ospd/pull/42) [#142](https://github.com/greenbone/ospd/pull/142) 185 | - Add functions for port manipulation. [#44](https://github.com/greenbone/ospd/pull/44) 186 | - Add as subelement of in . [#45](https://github.com/greenbone/ospd/pull/45) 187 | - Add pop_results attribute to . [#46](https://github.com/greenbone/ospd/pull/46) 188 | - Add methods to set and get the vts feed version. [#79](https://github.com/greenbone/ospd/pull/79) 189 | - Add cvss module. [#88](https://github.com/greenbone/ospd/pull/88) 190 | - Add filter option to OSP get_vts command. [#94](https://github.com/greenbone/ospd/pull/94) 191 | - Allows to set the logging domain from the wrapper. [#97](https://github.com/greenbone/ospd/pull/97) 192 | - Add option for logging into a specified log file. [#98](https://github.com/greenbone/ospd/pull/98) 193 | - Add option for logging into a specified log file. [#98](https://github.com/greenbone/ospd/pull/98) 194 | - Add scans status to improve the progress and add support to resume tasks. [#100](https://github.com/greenbone/ospd/pull/) [#101](https://github.com/greenbone/ospd/pull/101) [#102](https://github.com/greenbone/ospd/pull/102) [#103](https://github.com/greenbone/ospd/pull/103) 195 | - Add support for exclude hosts. [#107](https://github.com/greenbone/ospd/pull/107) 196 | - Add hostname attribute to results. [#108](https://github.com/greenbone/ospd/pull/108) 197 | - Add the --niceness option. [#109](https://github.com/greenbone/ospd/pull/109) 198 | - Add support for configuration file. [#122](https://github.com/greenbone/ospd/pull/122) 199 | - Add option to set unix socket mode permission. [#123](https://github.com/greenbone/ospd/pull/123) 200 | - Add pid file creation to avoid having two daemons. [#126](https://github.com/greenbone/ospd/pull/126) [#128](https://github.com/greenbone/ospd/pull/128) 201 | - Add OSP command. [#131](https://github.com/greenbone/ospd/pull/131) [#137](https://github.com/greenbone/ospd/pull/137) 202 | - Add method to check if a target finished cleanly or crashed. [#133](https://github.com/greenbone/ospd/pull/133) 203 | - Add the --stream-timeout option to configure the socket timeout. [#136](https://github.com/greenbone/ospd/pull/136) 204 | - Add support to handle multiple requests simultaneously. 205 | [#136](https://github.com/greenbone/ospd/pull/136), [#139](https://github.com/greenbone/ospd/pull/139) 206 | 207 | ### Changed 208 | - Improve documentation. 209 | - Improve Unittest. 210 | - Send the response data in block of given length instead of sending all at once. [#35](https://github.com/greenbone/ospd/pull/35) 211 | - Makes the socket a non-blocking socket. [#78](https://github.com/greenbone/ospd/pull/78) 212 | - Refactor misc. [#111](https://github.com/greenbone/ospd/pull/111) 213 | - Refactor error module. [#95](https://github.com/greenbone/ospd/pull/95) [#112](https://github.com/greenbone/ospd/pull/112) 214 | - Refactor ospd connection handling. [#114](https://github.com/greenbone/ospd/pull/114) 215 | - Use ordered dictionary to maintain the results order. [#119](https://github.com/greenbone/ospd/pull/119) 216 | - Refactor ospd. [#120](https://github.com/greenbone/ospd/pull/120) 217 | - Set default unix socket path to /var/run/ospd/ospd.sock and default pid file path to /var/run/ospd.pid. [#140](https://github.com/greenbone/ospd/pull/140) 218 | - Do not add a host detail result with the host status. [#145](https://github.com/greenbone/ospd/pull/145) 219 | - Do not log the received command. [#151](https://github.com/greenbone/ospd/pull/151) 220 | 221 | ### Fixed 222 | - Fix scan progress. [#47](https://github.com/greenbone/ospd/pull/47) 223 | - Documentation has been improved. 224 | - Improve connection handling. [#80](https://github.com/greenbone/ospd/pull/80) 225 | - Fix target_to_ipv4_short(). [#99](https://github.com/greenbone/ospd/pull/99) 226 | - Handle write error if the client disconnects abruptly. [#135](https://github.com/greenbone/ospd/pull/135) 227 | - Improve error handling when sending data. [#147](https://github.com/greenbone/ospd/pull/147) 228 | - Fix classifier in setup.py. [#154](https://github.com/greenbone/ospd/pull/154) 229 | 230 | [2.0]: https://github.com/greenbone/ospd/compare/ospd-1.3...main 231 | 232 | 233 | ## [1.3] (2018-06-05) 234 | 235 | ### Added 236 | - Support for unix sockets has been added. 237 | 238 | ### Removed 239 | - OSP has been renamed to Open Scanner Protocol. 240 | 241 | ### Changed 242 | - Support Python 3 only. 243 | - Documentation has been updated. 244 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md COPYING Poetry.toml Poetry.lock pyproject.toml README.md setup.py 2 | recursive-include tests *.py 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Greenbone Logo](https://www.greenbone.net/wp-content/uploads/gb_logo_resilience_horizontal.png) 2 | 3 | # ospd 4 | 5 | :warning: ospd got merged into [ospd-openvas](https://github.com/greenbone/ospd-openvas). Therefore this repository is unmaintained and will not get any further changes! 6 | 7 | [![GitHub releases](https://img.shields.io/github/release/greenbone/ospd.svg)](https://github.com/greenbone/ospd/releases) 8 | [![PyPI](https://img.shields.io/pypi/v/ospd.svg)](https://pypi.org/project/ospd/) 9 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/greenbone/ospd/badges/quality-score.png?b=main)](https://scrutinizer-ci.com/g/greenbone/ospd/?branch=main) 10 | [![code test coverage](https://codecov.io/gh/greenbone/ospd/branch/main/graphs/badge.svg)](https://codecov.io/gh/greenbone/ospd) 11 | [![CircleCI](https://circleci.com/gh/greenbone/ospd/tree/main.svg?style=svg)](https://circleci.com/gh/greenbone/ospd/tree/main) 12 | 13 | ospd is a base class for scanner wrappers which share the same communication 14 | protocol: OSP (Open Scanner Protocol). OSP creates a unified interface for 15 | different security scanners and makes their control flow and scan results 16 | consistently available under the central Greenbone Vulnerability Manager service. 17 | 18 | OSP is similar in many ways to GMP (Greenbone Management Protocol): XML-based, 19 | stateless and non-permanent connection. 20 | 21 | The design supports wrapping arbitrary scanners with same protocol OSP, 22 | sharing the core daemon options while adding scanner specific parameters and 23 | options. 24 | 25 | ## Table of Contents 26 | 27 | - [Table of Contents](#table-of-contents) 28 | - [Releases](#releases) 29 | - [Installation](#installation) 30 | - [Requirements](#requirements) 31 | - [Install using pip](#install-using-pip) 32 | - [How to write your own OSP Scanner Wrapper](#how-to-write-your-own-osp-scanner-wrapper) 33 | - [Support](#support) 34 | - [Maintainer](#maintainer) 35 | - [Contributing](#contributing) 36 | - [License](#license) 37 | 38 | ## Releases 39 |  40 | All [release files](https://github.com/greenbone/ospd/releases) are signed with 41 | the [Greenbone Community Feed integrity key](https://community.greenbone.net/t/gcf-managing-the-digital-signatures/101). 42 | This gpg key can be downloaded at https://www.greenbone.net/GBCommunitySigningKey.asc 43 | and the fingerprint is `8AE4 BE42 9B60 A59B 311C 2E73 9823 FAA6 0ED1 E580`. 44 | 45 | ## Installation 46 | 47 | ### Requirements 48 | 49 | ospd requires Python >= 3.7 along with the following libraries: 50 | 51 | - paramiko 52 | - lxml 53 | - defusedxml 54 | - deprecated 55 | - psutil 56 | 57 | ### Install using pip 58 | 59 | You can install ospd from the Python Package Index using [pip](https://pip.pypa.io/): 60 | 61 | python3 -m pip install ospd 62 | 63 | Alternatively download or clone this repository and install the latest development version: 64 | 65 | python3 -m pip install . 66 | 67 | ## How to write your own OSP Scanner Wrapper 68 | 69 | As a core you need to derive from the class OSPDaemon from ospd.py. 70 | See the documentation there for the single steps to establish the 71 | full wrapper. 72 | 73 | See the file [doc/INSTALL-ospd-scanner.md](doc/INSTALL-ospd-scanner.md) about how to register a OSP scanner at 74 | the Greenbone Vulnerability Manager which will automatically establish a full 75 | GUI integration for the Greenbone Security Assistant (GSA). 76 | 77 | For an example implementation see [ospd-example-scanner](https://github.com/greenbone/ospd-example-scanner). 78 | 79 | ## Support 80 | 81 | For any question on the usage of OSPD please use the [Greenbone Community Portal](https://community.greenbone.net/c/osp). If you found a problem with the software, please [create an issue](https://github.com/greenbone/ospd/issues) on GitHub. 82 | 83 | ## Maintainer 84 | 85 | This project is maintained by [Greenbone Networks GmbH](https://www.greenbone.net/). 86 | 87 | ## Contributing 88 | 89 | Your contributions are highly appreciated. Please [create a pull request](https://github.com/greenbone/ospd/pulls) on GitHub. For bigger changes, please discuss it first in the [issues](https://github.com/greenbone/ospd/issues). 90 | 91 | For development you should use [poetry](https://python-poetry.org) 92 | to keep you python packages separated in different environments. First install 93 | poetry via pip 94 | 95 | python3 -m pip install --user poetry 96 | 97 | Afterwards run 98 | 99 | poetry install 100 | 101 | in the checkout directory of ospd (the directory containing the 102 | `pyproject.toml` file) to install all dependencies including the packages only 103 | required for development. 104 | 105 | The ospd repository uses [autohooks](https://github.com/greenbone/autohooks) 106 | to apply linting and auto formatting via git hooks. Please ensure the git hooks 107 | are active. 108 | 109 | poetry install 110 | poetry run autohooks activate --force 111 | 112 | ## License 113 | 114 | Copyright (C) 2009-2020 [Greenbone Networks GmbH](https://www.greenbone.net/) 115 | 116 | Licensed under the [GNU Affero General Public License v3.0 or later](COPYING). 117 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Instructions 2 | 3 | Before creating a new release carefully consider the version number of the new 4 | release. We are following [Semantic Versioning](https://semver.org/) and 5 | [PEP440](https://www.python.org/dev/peps/pep-0440/). 6 | 7 | ## Preparing the Required Python Packages 8 | 9 | * Install twine for pypi package uploads 10 | 11 | ```sh 12 | python3 -m pip install --user --upgrade twine 13 | ``` 14 | 15 | ## Configuring the Access to the Python Package Index (PyPI) 16 | 17 | *Note:* This is only necessary for users performing the release process for the 18 | first time. 19 | 20 | * Create an account at [Test PyPI](https://packaging.python.org/guides/using-testpypi/). 21 | 22 | * Create an account at [PyPI](https://pypi.org/). 23 | 24 | * Create a pypi configuration file `~/.pypirc` with the following content (Note: 25 | `` must be replaced): 26 | 27 | ```ini 28 | [distutils] 29 | index-servers = 30 | pypi 31 | testpypi 32 | 33 | [pypi] 34 | username = 35 | 36 | [testpypi] 37 | repository = https://test.pypi.org/legacy/ 38 | username = 39 | ``` 40 | 41 | ## Prepare testing the Release 42 | 43 | * Fetch upstream changes and create a branch: 44 | 45 | ```sh 46 | git fetch upstream 47 | git checkout -b create-new-release upstream/main 48 | ``` 49 | 50 | * Get the current version number 51 | 52 | ```sh 53 | poetry run python -m pontos.version show 54 | ``` 55 | 56 | * Update the version number to some alpha version e.g. 57 | 58 | ```sh 59 | poetry run python -m pontos.version update 1.2.3a1 60 | ``` 61 | 62 | ## Uploading to the PyPI Test Instance 63 | 64 | * Create a source and wheel distribution: 65 | 66 | ```sh 67 | rm -rf dist build ospd.egg-info 68 | poetry build 69 | ``` 70 | 71 | * Upload the archives in `dist` to [Test PyPI](https://test.pypi.org/): 72 | 73 | ```sh 74 | twine upload -r testpypi dist/* 75 | ``` 76 | 77 | * Check if the package is available at . 78 | 79 | ## Testing the Uploaded Package 80 | 81 | * Create a test directory: 82 | 83 | ```sh 84 | mkdir ospd-install-test 85 | cd ospd-install-test 86 | python3 -m venv test-env 87 | source test-env/bin/activate 88 | pip install -U pip # ensure the environment uses a recent version of pip 89 | pip install --pre -I --extra-index-url https://test.pypi.org/simple/ ospd 90 | ``` 91 | 92 | * Check install version with a Python script: 93 | 94 | ```sh 95 | python3 -c "from ospd import __version__; print(__version__)" 96 | ``` 97 | 98 | * Remove test environment: 99 | 100 | ```sh 101 | deactivate 102 | cd .. 103 | rm -rf ospd-install-test 104 | ``` 105 | 106 | ## Prepare the Release 107 | 108 | * Determine new release version number 109 | 110 | If the output is something like `1.2.3.dev1` or `1.2.3a1`, the new version 111 | should be `1.2.3`. 112 | 113 | * Update to new version number (`` must be replaced by the version 114 | from the last step) 115 | 116 | ```sh 117 | cd path/to/git/clone/of/ospd 118 | poetry run python -m pontos.version update 119 | ``` 120 | 121 | * Update the `CHANGELOG.md` file: 122 | * Change `[unreleased]` to new release version. 123 | * Add a release date. 124 | * Update reference to Github diff. 125 | * Remove empty sub sections like *Deprecated*. 126 | 127 | * Create a git commit: 128 | 129 | ```sh 130 | git add . 131 | git commit -m "Prepare release " 132 | ``` 133 | 134 | ## Performing the Release on GitHub 135 | 136 | * Create a pull request (PR) for the earlier commit: 137 | 138 | ```sh 139 | git push origin 140 | ``` 141 | Open GitHub and create a PR against . 142 | 143 | * Ask another developer/maintainer to review and merge the PR. 144 | 145 | * Once the PR is merged, update the local `main` branch: 146 | 147 | ```sh 148 | git fetch upstream 149 | git rebase upstream/main main 150 | ``` 151 | 152 | * Create a git tag: 153 | 154 | ```sh 155 | git tag v 156 | ``` 157 | 158 | Or even a tag signed with a personal GPG key: 159 | 160 | ```sh 161 | git tag --sign --message "Tagging the release" v 162 | ``` 163 | 164 | * Push changes and tag to Github: 165 | 166 | ```sh 167 | git push --tags upstream 168 | ``` 169 | 170 | ## Uploading to the 'real' PyPI 171 | 172 | * Uploading to PyPI is done automatically by pushing a git tag via CircleCI 173 | 174 | * Check if new version is available at . 175 | 176 | ## Bumping `main` Branch to the Next Version 177 | 178 | 179 | * Update to a Development Version 180 | 181 | The next version should contain an incremented minor version and a dev suffix 182 | e.g. 2.3.0.dev1 183 | 184 | ```sh 185 | poetry run python -m pontos.version update 186 | ``` 187 | 188 | * Create a commit for the version bump: 189 | 190 | ```sh 191 | git add . 192 | git commit -m "Update version after release" 193 | git push upstream 194 | ``` 195 | 196 | ## Announcing the Release 197 | 198 | * Create a Github release: 199 | 200 | See https://help.github.com/articles/creating-releases/ 201 | -------------------------------------------------------------------------------- /doc/INSTALL-ospd-scanner: -------------------------------------------------------------------------------- 1 | INSTALL-ospd-scanner.md -------------------------------------------------------------------------------- /doc/INSTALL-ospd-scanner.md: -------------------------------------------------------------------------------- 1 | General Installation Instructions for OSPD-based Scanners 2 | ========================================================= 3 | 4 | This is a general description about installing an ospd-based scanner wrapper 5 | implementation. 6 | 7 | The actual scanner implementation usually has individual installation 8 | instructions and may refer to this general guide. 9 | 10 | In the following guide, replace `ospd-scanner` with the name of the actual OSPD 11 | scanner. 12 | 13 | 14 | Install in a Virtual Environment 15 | -------------------------------- 16 | 17 | The recommended way to install `ospd-scanner` is to do so inside a virtual 18 | environment (`virtualenv` or `venv`). 19 | 20 | This way, the server and its dependency are well isolated from system-wide 21 | updates, making it easier to upgrade it, delete it, or install dependencies 22 | only for it. 23 | 24 | Refer to the Python documentation for setting up virtual environments for 25 | further information. 26 | 27 | First you need to create a virtual environment somewhere on your system, for 28 | example with the following command: 29 | 30 | virtualenv ospd-scanner 31 | 32 | Installing `ospd-scanner` inside your newly created virtual environment could 33 | then be done with the following command: 34 | 35 | ospd-scanner/bin/pip install ospd_scanner-x.y.z.tar.gz 36 | 37 | Note: As `ospd` is not (yet) available through PyPI, you probably want to 38 | install it manually first inside your virtual environment prior to installing 39 | `ospd-scanner`. 40 | 41 | To run `ospd-scanner`, just start the Python script installed inside the 42 | virtual environment: 43 | 44 | ospd-scanner/bin/ospd-scanner 45 | 46 | 47 | Install (Sub-)System-wide 48 | ------------------------- 49 | 50 | To install `ospd-scanner` into directory `` run this command: 51 | 52 | python3 setup.py install --prefix= 53 | 54 | The default for `` is `/usr/local`. 55 | 56 | Be aware that this might automatically download and install missing 57 | Python packages. To prevent this, you should install the prerequisites 58 | first with the mechanism of your system (for example via `apt` or `rpm`). 59 | 60 | You may need to set the `PYTHONPATH` like this before running 61 | the install command: 62 | 63 | export PYTHONPATH=/lib/python3.7/site-packages/ 64 | 65 | The actual value for `PYTHONPATH` depends on your Python version. 66 | 67 | Creating certificates 68 | --------------------- 69 | 70 | An OSPD service can be started using a Unix domain socket (only on 71 | respective systems) or using a TCP socket. The latter uses TLS-based 72 | encryption and authorization while the first is not encrypted and uses 73 | the standard file access rights for authorization. 74 | 75 | For the TCP socket communication it is mandatory to use adequate 76 | TLS certificates which you need for each of your OSPD service. You may use 77 | the same certificates for all services if you like. 78 | 79 | By default, those certificates are used which are also used by GVM 80 | (see paths with `ospd-scanner --help`). Of course this works only 81 | if installed in the same environment. 82 | 83 | In case you do not have already a certificate to use, you may quickly 84 | create your own one (can be used for multiple ospd daemons) using the 85 | `gvm-manage-certs` tool provided with `gvmd` 86 | (): 87 | 88 | gvm-manage-certs -s 89 | 90 | And sign it with the CA checked for by the client. The client is usually 91 | Greenbone Vulnerability Manager for which a global trusted CA certificate 92 | can be configured. 93 | 94 | 95 | Registering an OSP daemon at Greenbone Vulnerability Manager 96 | ------------------------------------------------------------ 97 | 98 | The file [README](../README.md) explains how to control the OSP daemon via 99 | command line. 100 | 101 | It is also possible to register an OSP daemon at the Greenbone Vulnerability 102 | Manager and then use GMP clients to control the OSP daemon, for example the 103 | web interface GSA. 104 | 105 | You can register either via the GUI (`Configuration -> Scanners`) and create 106 | a new Scanner there. 107 | 108 | Or you can create a scanner via `gvmd` command line (adjust host, 109 | port, paths, etc. for your daemon): 110 | 111 | gvmd --create-scanner="OSP Scanner" --scanner-host=127.0.0.1 --scanner-port=1234 \ 112 | --scanner-type="OSP" --scanner-ca-pub=/usr/var/lib/gvm/CA/cacert.pem \ 113 | --scanner-key-pub=/usr/var/lib/gvm/CA/clientcert.pem \ 114 | --scanner-key-priv=/usr/var/lib/gvm/private/CA/clientkey.pem 115 | 116 | or for local running ospd-scanner via file socket: 117 | 118 | gvmd --create-scanner="OSP Scanner" --scanner-type="OSP" --scanner-host=/var/run/ospd-scanner.sock 119 | 120 | Please note that the scanner created via `gvmd` like above will be created with 121 | read permissions to all pre-configured roles. 122 | 123 | Check whether Greenbone Vulnerability Manager can connect to the OSP daemon: 124 | 125 | $ gvmd --get-scanners 126 | 08b69003-5fc2-4037-a479-93b440211c73 OpenVAS Default 127 | 3566ddf1-cecf-4491-8bcc-5d62a87404c3 OSP Scanner 128 | 129 | $ gvmd --verify-scanner=3566ddf1-cecf-4491-8bcc-5d62a87404c3 130 | Scanner version: 1.0. 131 | 132 | Of course, using GMP via command line tools provided by 133 | [gvm-tools](https://github.com/greenbone/gvm-tools) to register an OSP Scanner 134 | is also possible as a third option. 135 | 136 | 137 | Documentation 138 | ------------- 139 | 140 | Source code documentation can be accessed over the usual methods, 141 | for example (replace "scanner" by the scanner name): 142 | 143 | $ python3 144 | >>> import ospd_scanner.wrapper 145 | >>> help (ospd_scanner.wrapper) 146 | 147 | An equivalent to this is: 148 | 149 | pydoc3 ospd_scanner.wrapper 150 | 151 | To explore the code documentation in a web browser: 152 | 153 | $ pydoc3 -p 12345 154 | pydoc server ready at http://localhost:12345/ 155 | 156 | For further options see the `man` page of `pydoc`. 157 | 158 | 159 | Creating a source archive 160 | ------------------------- 161 | 162 | To create a .tar.gz file for the `ospd-scanner` module run this command: 163 | 164 | python3 setup.py sdist 165 | 166 | This will create the archive file in the subdirectory `dist`. 167 | -------------------------------------------------------------------------------- /doc/USAGE-ospd-scanner: -------------------------------------------------------------------------------- 1 | USAGE-ospd-scanner.md -------------------------------------------------------------------------------- /doc/USAGE-ospd-scanner.md: -------------------------------------------------------------------------------- 1 | General Usage Instructions for ospd-based Scanners 2 | -------------------------------------------------- 3 | 4 | This is a general description about using an ospd-based scanner wrapper 5 | implementation. 6 | 7 | The actual scanner implementation has individual usage instructions for anything 8 | that goes beyond this general guide. 9 | 10 | In the following description replace `ospd-scanner` with the name of the actual 11 | OSPD scanner. 12 | 13 | See the documentation of your ospd-based scanner and the general instructions in 14 | the [INSTALL-ospd-scanner.md](INSTALL-ospd-scanner.md) file on how to hand over 15 | full control to the Greenbone Vulnerability Manager. 16 | 17 | This usage guide explains how to use an OSP scanner independently of Greenbone 18 | Vulnerability Manager, for example when developing a new ospd-based scanner or 19 | for testing purposes. 20 | 21 | 22 | Open Scanner Protocol 23 | --------------------- 24 | 25 | Using an ospd-based scanner means using the Open Scanner Protocol (OSP). This is 26 | what Greenbone Vulnerability Manager does. See the ospd module for the original 27 | specification available in [ospd/doc/OSP.xml](OSP.xml). 28 | 29 | There is also an online version available at 30 | . 31 | 32 | 33 | gvm-tools 34 | --------- 35 | 36 | The `gvm-tools` help to make accessing the OSP interface easier. 37 | They can be obtained from . 38 | 39 | This module provides the commands `gvm-cli` and `gvm-pyshell`. 40 | 41 | 42 | Starting an ospd-based scanner 43 | ------------------------------ 44 | 45 | All ospd-based scanners share a set of command-line options such as 46 | `--help`, `--bind-address`, `--port`, `--key-file`, `--timeout`, etc. 47 | 48 | For example, to see the command line options you can run: 49 | 50 | ospd-scanner --help 51 | 52 | To run an instance of `ospd-scanner` listening on Unix domain socket: 53 | 54 | ospd-scanner -u /var/run/ospd-scanner.sock & 55 | 56 | To run a test instance of `ospd-scanner` on local TCP port 1234: 57 | 58 | ospd-scanner -b 127.0.0.1 -p 1234 & 59 | 60 | Add `--log-level=DEBUG` to enable maximum debugging output. 61 | 62 | Parameter for `--log-level` can be one of `DEBUG`, `INFO`, `WARNING`, `ERROR` or 63 | `CRITICAL` (in order of priority). 64 | 65 | 66 | Controlling an OSP scanner 67 | -------------------------- 68 | 69 | You can use command line tools provided by the `gvm-tools` module to interact 70 | with an OSP scanner. 71 | 72 | To get a description of the interface: 73 | 74 | gvm-cli socket --sockpath /var/run/ospd-scanner.sock --xml "" 75 | 76 | 77 | Starting a scan (scanner parameters can be added according to the description 78 | printed as response to the `` command): 79 | 80 | gvm-cli socket --sockpath /var/run/ospd-scanner.sock --xml="" 81 | 82 | 83 | Start a scan for ospd-based scanners that use the builtin-support for SSH 84 | authentication: 85 | 86 | gvm-cli socket --sockpath /var/run/ospd-scanner.sock --xml="myuser:mypassword" 87 | 88 | 89 | Start a scan for two vulnerability tests `vt_id_1` and `vt_id_2` of an ospd-based 90 | scanner: 91 | 92 | gvm-cli socket --sockpath /var/run/ospd-scanner.sock --xml="vt_id_1, vt_id_2" 93 | 94 | 95 | Show the list of scans with status and results: 96 | 97 | gvm-cli socket --sockpath /var/run/ospd-scanner.sock --xml="" 98 | 99 | 100 | Delete a scan from this list (only finished scans can be deleted): 101 | 102 | gvm-cli socket --sockpath /var/run/ospd-scanner.sock --xml="" 103 | -------------------------------------------------------------------------------- /doc/example-ospd-logging.conf: -------------------------------------------------------------------------------- 1 | ### If a custom logging configuration is required, the following 2 | ### items under General must be present in the configuration file. 3 | ### Examples for customization are given. Just comment/uncomment 4 | ### the corresponding lines and do necessary adjustments. 5 | ### For official documentation visit 6 | ### https://docs.python.org/3.7/library/logging.config.html#configuration-file-format 7 | 8 | ## General 9 | #################### 10 | 11 | [loggers] 12 | keys=root 13 | #keys=root, ospd_openvas 14 | 15 | [logger_root] 16 | level=NOTSET 17 | handlers=default_handler 18 | 19 | 20 | ### There is already an existen default_handler. 21 | ### Uncomment the following to extend the existent handler list 22 | #################### 23 | 24 | #[handlers] 25 | #keys=default_handler, custom_syslog 26 | 27 | 28 | ### Example for a custom handler. Custom must be added to the handlers list, 29 | #################### 30 | 31 | #[handler_custom] 32 | #class=FileHandler 33 | #level=DEBUG 34 | #formatter=file 35 | #args=('some_path_to_log_file.log', 'a') 36 | 37 | #[handler_custom_syslog] 38 | #class=handlers.SysLogHandler 39 | #level=DEBUG 40 | #formatter=file 41 | #args=('/dev/log', handlers.SysLogHandler.LOG_USER) 42 | 43 | 44 | ### Specific logging configuration for a single module. In the following 45 | ### example, the ospd_openvas.lock module will log with debug level. 46 | #################### 47 | 48 | #[logger_ospd_openvas] 49 | #level=DEBUG 50 | #handlers=custom_syslog 51 | #qualname=ospd_openvas.lock 52 | #propagate=0 -------------------------------------------------------------------------------- /doc/example-ospd.conf: -------------------------------------------------------------------------------- 1 | [OSPD - openvas] 2 | ## General 3 | pid_file = install-prefix/var/run/ospd/openvas.pid 4 | lock_file_dir = install-prefix/var/run/ 5 | stream_timeout = 1 6 | max_scans = 3 7 | min_free_mem_scan_queue = 1000 8 | max_queued_scans = 0 9 | 10 | # Log config 11 | log_level = DEBUG 12 | log_file = install-prefix/var/log/gvm/openvas.log 13 | log_config = install-prefix/.config/ospd-logging.conf 14 | 15 | ## Unix socket settings 16 | socket_mode = 0o770 17 | unix_socket = install-prefix/var/run/ospd/openvas.sock 18 | 19 | ## TLS socket settings and certificates. 20 | #port = 9390 21 | #bind_address = 0.0.0.0 22 | #key_file = install-prefix/var/lib/gvm/private/CA/serverkey.pem 23 | #cert_file = install-prefix/var/lib/gvm/CA/servercert.pem 24 | #ca_file = install-prefix/var/lib/gvm/CA/cacert.pem 25 | 26 | [OSPD - some wrapper] 27 | log_level = DEBUG 28 | socket_mode = 0o770 29 | unix_socket = install-prefix/var/run/ospd/ospd-wrapper.sock 30 | pid_file = install-prefix/var/run/ospd/ospd-wrapper.pid 31 | log_file = install-prefix/var/log/gvm/ospd-wrapper.log 32 | -------------------------------------------------------------------------------- /doc/generate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 3 | # 4 | # SPDX-License-Identifier: GPL-2.0-or-later 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU General Public License 8 | # as published by the Free Software Foundation; either version 2 9 | # of the License, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 19 | 20 | # Schema generator script: HTML. 21 | 22 | DOCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 23 | xsltproc ${DOCDIR}/HTML.xsl ${DOCDIR}/OSP.xml > ${DOCDIR}/osp.html 24 | -------------------------------------------------------------------------------- /ospd/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 3 | # 4 | # SPDX-License-Identifier: AGPL-3.0-or-later 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as 8 | # published by the Free Software Foundation, either version 3 of the 9 | # License, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | """ OSPd core module. """ 20 | 21 | from .__version__ import __version__ 22 | -------------------------------------------------------------------------------- /ospd/__version__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | # THIS IS AN AUTOGENERATED FILE. DO NOT TOUCH! 4 | 5 | __version__ = "21.10.0.dev1" 6 | -------------------------------------------------------------------------------- /ospd/command/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | import ospd.command.command # required to register all commands 19 | 20 | from .registry import get_commands 21 | -------------------------------------------------------------------------------- /ospd/command/initsubclass.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | # pylint: disable=bad-mcs-classmethod-argument, no-member 19 | 20 | _has_init_subclass = hasattr( # pylint: disable=invalid-name 21 | type, "__init_subclass__" 22 | ) 23 | 24 | if not _has_init_subclass: 25 | 26 | class InitSubclassMeta(type): 27 | """Metaclass that implements PEP 487 protocol""" 28 | 29 | def __new__(cls, name, bases, ns, **kwargs): 30 | __init_subclass__ = ns.pop("__init_subclass__", None) 31 | if __init_subclass__: 32 | __init_subclass__ = classmethod(__init_subclass__) 33 | ns["__init_subclass__"] = __init_subclass__ 34 | return super().__new__(cls, name, bases, ns, **kwargs) 35 | 36 | def __init__(cls, name, bases, ns, **kwargs): 37 | super().__init__(name, bases, ns) 38 | super_class = super(cls, cls) 39 | if hasattr(super_class, "__init_subclass__"): 40 | super_class.__init_subclass__.__func__(cls, **kwargs) 41 | 42 | 43 | else: 44 | InitSubclassMeta = type # type: ignore 45 | -------------------------------------------------------------------------------- /ospd/command/registry.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | from typing import List 19 | 20 | __COMMANDS = [] 21 | 22 | 23 | def register_command(command: object) -> None: 24 | """Register a command class""" 25 | __COMMANDS.append(command) 26 | 27 | 28 | def remove_command(command: object) -> None: 29 | """Unregister a command class""" 30 | __COMMANDS.remove(command) 31 | 32 | 33 | def get_commands() -> List[object]: 34 | """Return the list of registered command classes""" 35 | return __COMMANDS 36 | -------------------------------------------------------------------------------- /ospd/config.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ 19 | Module to store ospd configuration settings 20 | """ 21 | 22 | import configparser 23 | import logging 24 | 25 | from pathlib import Path 26 | from typing import Dict 27 | 28 | logger = logging.getLogger(__name__) 29 | 30 | 31 | class Config: 32 | def __init__(self, section: str = 'main') -> None: 33 | self._parser = configparser.ConfigParser(default_section=section) 34 | self._config = {} # type: Dict 35 | self._defaults = {} # type: Dict 36 | 37 | def load(self, filepath: Path, def_section: str = 'main') -> None: 38 | path = filepath.expanduser() 39 | parser = configparser.ConfigParser(default_section=def_section) 40 | 41 | with path.open() as f: 42 | parser.read_file(f) 43 | 44 | self._defaults.update(parser.defaults()) 45 | 46 | for key, value in parser.items(def_section): 47 | self._config.setdefault(def_section, dict())[key] = value 48 | 49 | def defaults(self) -> Dict: 50 | return self._defaults 51 | -------------------------------------------------------------------------------- /ospd/cvss.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ Common Vulnerability Scoring System handling class. """ 19 | 20 | import math 21 | from typing import List, Dict, Optional 22 | 23 | CVSS_V2_METRICS = { 24 | 'AV': {'L': 0.395, 'A': 0.646, 'N': 1.0}, 25 | 'AC': {'H': 0.35, 'M': 0.61, 'L': 0.71}, 26 | 'Au': {'M': 0.45, 'S': 0.56, 'N': 0.704}, 27 | 'C': {'N': 0.0, 'P': 0.275, 'C': 0.660}, 28 | 'I': {'N': 0.0, 'P': 0.275, 'C': 0.660}, 29 | 'A': {'N': 0.0, 'P': 0.275, 'C': 0.660}, 30 | } # type: Dict 31 | 32 | CVSS_V3_METRICS = { 33 | 'AV': {'N': 0.85, 'A': 0.62, 'L': 0.55, 'P': 0.2}, 34 | 'AC': {'L': 0.77, 'H': 0.44}, 35 | 'PR_SU': {'N': 0.85, 'L': 0.62, 'H': 0.27}, 36 | 'PR_SC': {'N': 0.85, 'L': 0.68, 'H': 0.50}, 37 | 'UI': {'N': 0.85, 'R': 0.62}, 38 | 'S': {'U': False, 'C': True}, 39 | 'C': {'H': 0.56, 'L': 0.22, 'N': 0}, 40 | 'I': {'H': 0.56, 'L': 0.22, 'N': 0}, 41 | 'A': {'H': 0.56, 'L': 0.22, 'N': 0}, 42 | } # type: Dict 43 | 44 | 45 | class CVSS(object): 46 | """Handle cvss vectors and calculate the cvss scoring""" 47 | 48 | @staticmethod 49 | def roundup(value: float) -> float: 50 | """It rounds up to 1 decimal.""" 51 | return math.ceil(value * 10) / 10 52 | 53 | @staticmethod 54 | def _parse_cvss_base_vector(cvss_vector: str) -> List: 55 | """Parse a string containing a cvss base vector. 56 | 57 | Arguments: 58 | cvss_vector (str): cvss base vector to be parsed. 59 | 60 | Return list with the string values of each vector element. 61 | """ 62 | vector_as_list = cvss_vector.split('/') 63 | return [item.split(':')[1] for item in vector_as_list] 64 | 65 | @classmethod 66 | def cvss_base_v2_value(cls, cvss_base_vector: str) -> Optional[float]: 67 | """Calculate the cvss base score from a cvss base vector 68 | for cvss version 2. 69 | Arguments: 70 | cvss_base_vector (str) Cvss base vector v2. 71 | 72 | Return the calculated score 73 | """ 74 | if not cvss_base_vector: 75 | return None 76 | 77 | _av, _ac, _au, _c, _i, _a = cls._parse_cvss_base_vector( 78 | cvss_base_vector 79 | ) 80 | 81 | _impact = 10.41 * ( 82 | 1 83 | - (1 - CVSS_V2_METRICS['C'].get(_c)) 84 | * (1 - CVSS_V2_METRICS['I'].get(_i)) 85 | * (1 - CVSS_V2_METRICS['A'].get(_a)) 86 | ) 87 | 88 | _exploitability = ( 89 | 20 90 | * CVSS_V2_METRICS['AV'].get(_av) 91 | * CVSS_V2_METRICS['AC'].get(_ac) 92 | * CVSS_V2_METRICS['Au'].get(_au) 93 | ) 94 | 95 | f_impact = 0 if _impact == 0 else 1.176 96 | 97 | cvss_base = ((0.6 * _impact) + (0.4 * _exploitability) - 1.5) * f_impact 98 | 99 | return round(cvss_base, 1) 100 | 101 | @classmethod 102 | def cvss_base_v3_value(cls, cvss_base_vector: str) -> Optional[float]: 103 | """Calculate the cvss base score from a cvss base vector 104 | for cvss version 3. 105 | Arguments: 106 | cvss_base_vector (str) Cvss base vector v3. 107 | 108 | Return the calculated score, None on fail. 109 | """ 110 | if not cvss_base_vector: 111 | return None 112 | _ver, _av, _ac, _pr, _ui, _s, _c, _i, _a = cls._parse_cvss_base_vector( 113 | cvss_base_vector 114 | ) 115 | 116 | scope_changed = CVSS_V3_METRICS['S'].get(_s) 117 | 118 | isc_base = 1 - ( 119 | (1 - CVSS_V3_METRICS['C'].get(_c)) 120 | * (1 - CVSS_V3_METRICS['I'].get(_i)) 121 | * (1 - CVSS_V3_METRICS['A'].get(_a)) 122 | ) 123 | 124 | if scope_changed: 125 | _priv_req = CVSS_V3_METRICS['PR_SC'].get(_pr) 126 | else: 127 | _priv_req = CVSS_V3_METRICS['PR_SU'].get(_pr) 128 | 129 | _exploitability = ( 130 | 8.22 131 | * CVSS_V3_METRICS['AV'].get(_av) 132 | * CVSS_V3_METRICS['AC'].get(_ac) 133 | * _priv_req 134 | * CVSS_V3_METRICS['UI'].get(_ui) 135 | ) 136 | 137 | if scope_changed: 138 | _impact = 7.52 * (isc_base - 0.029) - 3.25 * pow( 139 | isc_base - 0.02, 15 140 | ) 141 | _base_score = min(1.08 * (_impact + _exploitability), 10) 142 | else: 143 | _impact = 6.42 * isc_base 144 | _base_score = min(_impact + _exploitability, 10) 145 | 146 | if _impact > 0: 147 | return cls.roundup(_base_score) 148 | 149 | return 0 150 | -------------------------------------------------------------------------------- /ospd/datapickler.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ Pickle Handler class 19 | """ 20 | 21 | import logging 22 | import pickle 23 | import os 24 | 25 | from hashlib import sha256 26 | from pathlib import Path 27 | from typing import BinaryIO, Any 28 | 29 | from ospd.errors import OspdCommandError 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | OWNER_ONLY_RW_PERMISSION = 0o600 34 | 35 | 36 | class DataPickler: 37 | def __init__(self, storage_path: str): 38 | self._storage_path = storage_path 39 | self._storage_fd = None 40 | 41 | def _fd_opener(self, path: str, flags: int) -> BinaryIO: 42 | os.umask(0) 43 | flags = os.O_CREAT | os.O_WRONLY 44 | self._storage_fd = os.open(path, flags, mode=OWNER_ONLY_RW_PERMISSION) 45 | return self._storage_fd 46 | 47 | def _fd_close(self) -> None: 48 | try: 49 | self._storage_fd.close() 50 | self._storage_fd = None 51 | except Exception: # pylint: disable=broad-except 52 | pass 53 | 54 | def remove_file(self, filename: str) -> None: 55 | """Remove the file containing a scan_info pickled object""" 56 | storage_file_path = Path(self._storage_path) / filename 57 | try: 58 | storage_file_path.unlink() 59 | except Exception as e: # pylint: disable=broad-except 60 | logger.error('Not possible to delete %s. %s', filename, e) 61 | 62 | def store_data(self, filename: str, data_object: Any) -> str: 63 | """Pickle a object and store it in a file named""" 64 | storage_file_path = Path(self._storage_path) / filename 65 | 66 | try: 67 | # create parent directories recursively 68 | parent_dir = storage_file_path.parent 69 | parent_dir.mkdir(parents=True, exist_ok=True) 70 | except Exception as e: 71 | raise OspdCommandError( 72 | f'Not possible to access dir for {filename}. {e}', 73 | 'start_scan', 74 | ) from e 75 | 76 | try: 77 | pickled_data = pickle.dumps(data_object) 78 | except pickle.PicklingError as e: 79 | raise OspdCommandError( 80 | f'Not possible to pickle scan info for {filename}. {e}', 81 | 'start_scan', 82 | ) from e 83 | 84 | try: 85 | with open( 86 | str(storage_file_path), 'wb', opener=self._fd_opener 87 | ) as scan_info_f: 88 | scan_info_f.write(pickled_data) 89 | except Exception as e: # pylint: disable=broad-except 90 | self._fd_close() 91 | raise OspdCommandError( 92 | f'Not possible to store scan info for {filename}. {e}', 93 | 'start_scan', 94 | ) from e 95 | self._fd_close() 96 | 97 | return self._pickled_data_hash_generator(pickled_data) 98 | 99 | def load_data(self, filename: str, original_data_hash: str) -> Any: 100 | """Unpickle the stored data in the filename. Perform an 101 | intengrity check of the read data with the the hash generated 102 | with the original data. 103 | 104 | Return: 105 | Dictionary containing the scan info. None otherwise. 106 | """ 107 | 108 | storage_file_path = Path(self._storage_path) / filename 109 | pickled_data = None 110 | try: 111 | with storage_file_path.open('rb') as scan_info_f: 112 | pickled_data = scan_info_f.read() 113 | except Exception as e: # pylint: disable=broad-except 114 | logger.error( 115 | 'Not possible to read pickled data from %s. %s', filename, e 116 | ) 117 | return 118 | 119 | unpickled_scan_info = None 120 | try: 121 | unpickled_scan_info = pickle.loads(pickled_data) 122 | except pickle.UnpicklingError as e: 123 | logger.error( 124 | 'Not possible to read pickled data from %s. %s', filename, e 125 | ) 126 | return 127 | 128 | pickled_scan_info_hash = self._pickled_data_hash_generator(pickled_data) 129 | 130 | if original_data_hash != pickled_scan_info_hash: 131 | logger.error('Unpickled data from %s corrupted.', filename) 132 | return 133 | 134 | return unpickled_scan_info 135 | 136 | def _pickled_data_hash_generator(self, pickled_data: bytes) -> str: 137 | """Calculate the sha256 hash of a pickled data""" 138 | if not pickled_data: 139 | return 140 | 141 | hash_sha256 = sha256() 142 | hash_sha256.update(pickled_data) 143 | 144 | return hash_sha256.hexdigest() 145 | -------------------------------------------------------------------------------- /ospd/errors.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ OSP class for handling errors. 19 | """ 20 | 21 | from ospd.xml import simple_response_str 22 | 23 | 24 | class OspdError(Exception): 25 | """Base error class for all Ospd related errors""" 26 | 27 | 28 | class RequiredArgument(OspdError): 29 | """Raised if a required argument/parameter is missing 30 | 31 | Derives from :py:class:`OspdError` 32 | """ 33 | 34 | def __init__(self, function: str, argument: str) -> None: 35 | # pylint: disable=super-init-not-called 36 | self.function = function 37 | self.argument = argument 38 | 39 | def __str__(self) -> str: 40 | return f"{self.function}: Argument {self.argument} is required" 41 | 42 | 43 | class OspdCommandError(OspdError): 44 | 45 | """This is an exception that will result in an error message to the 46 | client""" 47 | 48 | def __init__( 49 | self, message: str, command: str = 'osp', status: int = 400 50 | ) -> None: 51 | super().__init__(message) 52 | self.message = message 53 | self.command = command 54 | self.status = status 55 | 56 | def as_xml(self) -> str: 57 | """Return the error in xml format.""" 58 | return simple_response_str(self.command, self.status, self.message) 59 | -------------------------------------------------------------------------------- /ospd/logger.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | import logging 19 | import os 20 | import configparser 21 | import time 22 | 23 | from logging.config import fileConfig 24 | from pathlib import Path 25 | from typing import Optional 26 | 27 | 28 | DEFAULT_HANDLER_CONSOLE = { 29 | 'class': 'logging.StreamHandler', 30 | 'level': 'INFO', 31 | 'formatter': 'file', 32 | 'args': 'sys.stdout,', 33 | } 34 | 35 | DEFAULT_HANDLER_FILE = { 36 | 'class': 'FileHandler', 37 | 'level': 'INFO', 38 | 'formatter': 'file', 39 | } 40 | 41 | DEFAULT_HANDLER_SYSLOG = { 42 | 'class': 'handlers.SysLogHandler', 43 | 'level': 'INFO', 44 | 'formatter': 'syslog', 45 | 'args': '("/dev/log", handlers.SysLogHandler.LOG_USER)', 46 | } 47 | 48 | DEFAULT_HANDLERS = {'keys': 'default_handler'} 49 | DEFAULT_FORMATTERS = {'keys': 'file,syslog'} 50 | DEFAULT_FORMATTER_FILE = { 51 | 'format': 'OSPD[' 52 | + str(os.getpid()) 53 | + '] %(asctime)s: %(levelname)s: (%(name)s) %(message)s', 54 | 'datefmt': '', 55 | } 56 | DEFAULT_FORMATTER_SYSLOG = { 57 | 'format': 'OSPD[' 58 | + str(os.getpid()) 59 | + '] %(levelname)s: (%(name)s) %(message)s', 60 | 'datefmt': '', 61 | } 62 | DEFAULT_LOGGERS = {'keys': 'root'} 63 | DEFAULT_ROOT_LOGGER = { 64 | 'level': 'NOTSET', 65 | 'handlers': 'default_handler', 66 | 'propagate': '0', 67 | } 68 | 69 | 70 | def init_logging( 71 | log_level: int, 72 | *, 73 | log_file: Optional[str] = None, 74 | log_config: Optional[str] = None, 75 | foreground: Optional[bool] = False, 76 | ): 77 | config = configparser.ConfigParser() 78 | config['handlers'] = DEFAULT_HANDLERS 79 | config['formatters'] = DEFAULT_FORMATTERS 80 | config['formatter_file'] = DEFAULT_FORMATTER_FILE 81 | config['formatter_syslog'] = DEFAULT_FORMATTER_SYSLOG 82 | 83 | if foreground: 84 | config['handler_default_handler'] = DEFAULT_HANDLER_CONSOLE 85 | elif log_file: 86 | config['handler_default_handler'] = DEFAULT_HANDLER_FILE 87 | config['handler_default_handler']['args'] = "('" + log_file + "', 'a')" 88 | else: 89 | config['handler_default_handler'] = DEFAULT_HANDLER_SYSLOG 90 | 91 | config['handler_default_handler']['level'] = log_level 92 | log_config_path = Path(log_config) 93 | if log_config_path.exists(): 94 | config.read(log_config) 95 | else: 96 | config['loggers'] = DEFAULT_LOGGERS 97 | config['logger_root'] = DEFAULT_ROOT_LOGGER 98 | 99 | fileConfig(config, disable_existing_loggers=False) 100 | logging.Formatter.converter = time.gmtime 101 | logging.getLogger() 102 | -------------------------------------------------------------------------------- /ospd/main.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | import logging 19 | 20 | import os 21 | import sys 22 | import atexit 23 | import signal 24 | 25 | from functools import partial 26 | 27 | from typing import Type, Optional 28 | from pathlib import Path 29 | 30 | from ospd.misc import go_to_background, create_pid 31 | from ospd.ospd import OSPDaemon 32 | from ospd.parser import create_parser, ParserType 33 | from ospd.server import TlsServer, UnixSocketServer, BaseServer 34 | from ospd.logger import init_logging 35 | 36 | 37 | COPYRIGHT = """Copyright (C) 2014-2021 Greenbone Networks GmbH 38 | License GPLv2+: GNU GPL version 2 or later 39 | This is free software: you are free to change and redistribute it. 40 | There is NO WARRANTY, to the extent permitted by law.""" 41 | 42 | logger = logging.getLogger(__name__) 43 | 44 | 45 | def print_version(daemon: OSPDaemon, file=sys.stdout): 46 | """Prints the server version and license information.""" 47 | 48 | scanner_name = daemon.get_scanner_name() 49 | server_version = daemon.get_server_version() 50 | protocol_version = daemon.get_protocol_version() 51 | daemon_name = daemon.get_daemon_name() 52 | daemon_version = daemon.get_daemon_version() 53 | 54 | print( 55 | f"OSP Server for {scanner_name}: {server_version}", 56 | file=file, 57 | ) 58 | print(f"OSP: {protocol_version}", file=file) 59 | print(f"{daemon_name}: {daemon_version}", file=file) 60 | print(file=file) 61 | print(COPYRIGHT, file=file) 62 | 63 | 64 | def exit_cleanup( 65 | pidfile: str, 66 | server: BaseServer, 67 | daemon: OSPDaemon, 68 | _signum=None, 69 | _frame=None, 70 | ) -> None: 71 | """Removes the pidfile before ending the daemon.""" 72 | signal.signal(signal.SIGINT, signal.SIG_IGN) 73 | pidpath = Path(pidfile) 74 | 75 | if not pidpath.is_file(): 76 | return 77 | 78 | with pidpath.open(encoding='utf-8') as f: 79 | if int(f.read()) == os.getpid(): 80 | logger.debug("Performing exit clean up") 81 | daemon.daemon_exit_cleanup() 82 | logger.info("Shutting-down server ...") 83 | server.close() 84 | logger.debug("Finishing daemon process") 85 | pidpath.unlink() 86 | sys.exit() 87 | 88 | 89 | def main( 90 | name: str, 91 | daemon_class: Type[OSPDaemon], 92 | parser: Optional[ParserType] = None, 93 | ): 94 | """OSPD Main function.""" 95 | 96 | if not parser: 97 | parser = create_parser(name) 98 | args = parser.parse_arguments() 99 | 100 | if args.version: 101 | args.foreground = True 102 | 103 | init_logging( 104 | args.log_level, 105 | log_file=args.log_file, 106 | log_config=args.log_config, 107 | foreground=args.foreground, 108 | ) 109 | 110 | if args.port == 0: 111 | server = UnixSocketServer( 112 | args.unix_socket, 113 | args.socket_mode, 114 | args.stream_timeout, 115 | ) 116 | else: 117 | server = TlsServer( 118 | args.address, 119 | args.port, 120 | args.cert_file, 121 | args.key_file, 122 | args.ca_file, 123 | args.stream_timeout, 124 | ) 125 | 126 | daemon = daemon_class(**vars(args)) 127 | 128 | if args.version: 129 | print_version(daemon) 130 | sys.exit() 131 | 132 | if args.list_commands: 133 | print(daemon.get_help_text()) 134 | sys.exit() 135 | 136 | if not args.foreground: 137 | go_to_background() 138 | 139 | if not create_pid(args.pid_file): 140 | sys.exit() 141 | 142 | # Set signal handler and cleanup 143 | atexit.register( 144 | exit_cleanup, pidfile=args.pid_file, server=server, daemon=daemon 145 | ) 146 | signal.signal( 147 | signal.SIGTERM, partial(exit_cleanup, args.pid_file, server, daemon) 148 | ) 149 | signal.signal( 150 | signal.SIGINT, partial(exit_cleanup, args.pid_file, server, daemon) 151 | ) 152 | if not daemon.check(): 153 | return 1 154 | 155 | logger.info( 156 | "Starting %s version %s.", 157 | daemon.daemon_info['name'], 158 | daemon.daemon_info['version'], 159 | ) 160 | 161 | daemon.init(server) 162 | daemon.run() 163 | 164 | return 0 165 | -------------------------------------------------------------------------------- /ospd/misc.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | # pylint: disable=too-many-lines 19 | 20 | """ Miscellaneous classes and functions related to OSPD. 21 | """ 22 | 23 | import logging 24 | import os 25 | import sys 26 | import uuid 27 | import multiprocessing 28 | 29 | from typing import Any, Callable, Iterable 30 | from pathlib import Path 31 | 32 | import psutil 33 | 34 | logger = logging.getLogger(__name__) 35 | 36 | 37 | def create_process( 38 | func: Callable, *, args: Iterable[Any] = None 39 | ) -> multiprocessing.Process: 40 | return multiprocessing.Process(target=func, args=args) 41 | 42 | 43 | class ResultType(object): 44 | 45 | """Various scan results types values.""" 46 | 47 | ALARM = 0 48 | LOG = 1 49 | ERROR = 2 50 | HOST_DETAIL = 3 51 | 52 | @classmethod 53 | def get_str(cls, result_type: int) -> str: 54 | """Return string name of a result type.""" 55 | if result_type == cls.ALARM: 56 | return "Alarm" 57 | elif result_type == cls.LOG: 58 | return "Log Message" 59 | elif result_type == cls.ERROR: 60 | return "Error Message" 61 | elif result_type == cls.HOST_DETAIL: 62 | return "Host Detail" 63 | else: 64 | assert False, f"Erroneous result type {result_type}." 65 | 66 | @classmethod 67 | def get_type(cls, result_name: str) -> int: 68 | """Return string name of a result type.""" 69 | if result_name == "Alarm": 70 | return cls.ALARM 71 | elif result_name == "Log Message": 72 | return cls.LOG 73 | elif result_name == "Error Message": 74 | return cls.ERROR 75 | elif result_name == "Host Detail": 76 | return cls.HOST_DETAIL 77 | else: 78 | assert False, f"Erroneous result name {result_name}." 79 | 80 | 81 | def valid_uuid(value) -> bool: 82 | """Check if value is a valid UUID.""" 83 | 84 | try: 85 | uuid.UUID(value, version=4) 86 | return True 87 | except (TypeError, ValueError, AttributeError): 88 | return False 89 | 90 | 91 | def go_to_background() -> None: 92 | """Daemonize the running process.""" 93 | try: 94 | if os.fork(): 95 | sys.exit() 96 | except OSError as errmsg: 97 | logger.error('Fork failed: %s', errmsg) 98 | sys.exit(1) 99 | 100 | 101 | def create_pid(pidfile: str) -> bool: 102 | """Check if there is an already running daemon and creates the pid file. 103 | Otherwise gives an error.""" 104 | 105 | pid = str(os.getpid()) 106 | new_process = psutil.Process(int(pid)) 107 | new_process_name = new_process.name() 108 | 109 | pidpath = Path(pidfile) 110 | 111 | if pidpath.is_file(): 112 | process_name = None 113 | with pidpath.open('r', encoding='utf-8') as pidfile: 114 | current_pid = pidfile.read() 115 | try: 116 | process = psutil.Process(int(current_pid)) 117 | process_name = process.name() 118 | except psutil.NoSuchProcess: 119 | pass 120 | 121 | if process_name == new_process_name: 122 | logger.error( 123 | "There is an already running process. See %s.", 124 | str(pidpath.absolute()), 125 | ) 126 | return False 127 | else: 128 | logger.debug( 129 | "There is an existing pid file '%s', but the PID %s belongs to " 130 | "the process %s. It seems that %s was abruptly stopped. " 131 | "Removing the pid file.", 132 | str(pidpath.absolute()), 133 | current_pid, 134 | process_name, 135 | new_process_name, 136 | ) 137 | 138 | try: 139 | with pidpath.open(mode='w', encoding='utf-8') as f: 140 | f.write(pid) 141 | except (FileNotFoundError, PermissionError) as e: 142 | logger.error( 143 | "Failed to create pid file %s. %s", str(pidpath.absolute()), e 144 | ) 145 | return False 146 | 147 | return True 148 | -------------------------------------------------------------------------------- /ospd/ospd_ssh.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ OSP Daemon class for simple remote SSH-based command execution. 19 | """ 20 | 21 | 22 | # This is needed for older pythons as our current module is called the same 23 | # as the ospd package 24 | # Another solution would be to rename that file. 25 | from __future__ import absolute_import 26 | 27 | import socket 28 | 29 | from typing import Optional, Dict 30 | from ospd.ospd import OSPDaemon 31 | 32 | try: 33 | import paramiko 34 | except ImportError: 35 | paramiko = None 36 | 37 | SSH_SCANNER_PARAMS = { 38 | 'username_password': { 39 | 'type': 'credential_up', 40 | 'name': 'SSH credentials', 41 | 'default': '', 42 | 'mandatory': 0, 43 | 'description': 'The SSH credentials in username:password format. Used' 44 | ' to log into the target and to run the commands on' 45 | ' that target. This should not be a privileged user' 46 | ' like "root", a regular privileged user account' 47 | ' should be sufficient in most cases.', 48 | }, 49 | 'port': { 50 | 'type': 'integer', 51 | 'name': 'SSH Port', 52 | 'default': 22, 53 | 'mandatory': 0, 54 | 'description': 'The SSH port which to use for logging in with the' 55 | ' given username_password.', 56 | }, 57 | 'ssh_timeout': { 58 | 'type': 'integer', 59 | 'name': 'SSH timeout', 60 | 'default': 30, 61 | 'mandatory': 0, 62 | 'description': 'Timeout when communicating with the target via SSH.', 63 | }, 64 | } # type: Dict 65 | 66 | # pylint: disable=abstract-method 67 | class OSPDaemonSimpleSSH(OSPDaemon): 68 | 69 | """ 70 | OSP Daemon class for simple remote SSH-based command execution. 71 | 72 | This class automatically adds scanner parameters to handle remote 73 | ssh login into the target systems: username, password, port and 74 | ssh_timout 75 | 76 | The method run_command can be used to execute a single command 77 | on the given remote system. The stdout result is returned as 78 | an array. 79 | """ 80 | 81 | def __init__(self, **kwargs): 82 | """Initializes the daemon and add parameters needed to remote SSH 83 | execution.""" 84 | super().__init__(**kwargs) 85 | 86 | self._niceness = kwargs.get('niceness', None) 87 | 88 | if paramiko is None: 89 | raise ImportError( 90 | 'paramiko needs to be installed in order to use' 91 | f' the {self.__class__.__name__} class.' 92 | ) 93 | 94 | for name, param in SSH_SCANNER_PARAMS.items(): 95 | self.set_scanner_param(name, param) 96 | 97 | def run_command(self, scan_id: str, host: str, cmd: str) -> Optional[str]: 98 | """ 99 | Run a single command via SSH and return the content of stdout or 100 | None in case of an Error. A scan error is issued in the latter 101 | case. 102 | 103 | For logging into 'host', the scan options 'port', 'username', 104 | 'password' and 'ssh_timeout' are used. 105 | """ 106 | 107 | ssh = paramiko.SSHClient() 108 | ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 109 | 110 | options = self.get_scan_options(scan_id) 111 | 112 | port = int(options['port']) 113 | timeout = int(options['ssh_timeout']) 114 | 115 | # For backward compatibility, consider the legacy mode to get 116 | # credentials as scan_option. 117 | # First and second modes should be removed in future releases. 118 | # On the third case it receives the credentials as a subelement of 119 | # the . 120 | credentials = self.get_scan_credentials(scan_id) 121 | if ( 122 | 'username_password' in options 123 | and ':' in options['username_password'] 124 | ): 125 | username, password = options['username_password'].split(':', 1) 126 | elif 'username' in options and options['username']: 127 | username = options['username'] 128 | password = options['password'] 129 | elif credentials: 130 | cred_params = credentials.get('ssh') 131 | username = cred_params.get('username', '') 132 | password = cred_params.get('password', '') 133 | else: 134 | self.add_scan_error( 135 | scan_id, host=host, value='Erroneous username_password value' 136 | ) 137 | raise ValueError('Erroneous username_password value') 138 | 139 | try: 140 | ssh.connect( 141 | hostname=host, 142 | username=username, 143 | password=password, 144 | timeout=timeout, 145 | port=port, 146 | ) 147 | except ( 148 | paramiko.ssh_exception.AuthenticationException, 149 | socket.error, 150 | ) as err: 151 | # Errors: No route to host, connection timeout, authentication 152 | # failure etc,. 153 | self.add_scan_error(scan_id, host=host, value=str(err)) 154 | return None 155 | 156 | if self._niceness is not None: 157 | cmd = f"nice -n {self._niceness} {cmd}" 158 | _, stdout, _ = ssh.exec_command(cmd) 159 | result = stdout.readlines() 160 | ssh.close() 161 | 162 | return result 163 | -------------------------------------------------------------------------------- /ospd/parser.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | import argparse 19 | import logging 20 | from pathlib import Path 21 | 22 | from ospd.config import Config 23 | 24 | # Default file locations as used by a OpenVAS default installation 25 | DEFAULT_KEY_FILE = "/var/lib/gvm/private/CA/serverkey.pem" 26 | DEFAULT_CERT_FILE = "/var/lib/gvm/CA/servercert.pem" 27 | DEFAULT_CA_FILE = "/var/lib/gvm/CA/cacert.pem" 28 | 29 | DEFAULT_PORT = 0 30 | DEFAULT_ADDRESS = "0.0.0.0" 31 | DEFAULT_NICENESS = 10 32 | DEFAULT_UNIX_SOCKET_MODE = "0o770" 33 | DEFAULT_CONFIG_PATH = "~/.config/ospd.conf" 34 | DEFAULT_LOG_CONFIG_PATH = "~/.config/ospd-logging.conf" 35 | DEFAULT_UNIX_SOCKET_PATH = "/run/ospd/ospd.sock" 36 | DEFAULT_PID_PATH = "/run/ospd/ospd.pid" 37 | DEFAULT_LOCKFILE_DIR_PATH = "/run/ospd" 38 | DEFAULT_STREAM_TIMEOUT = 10 # ten seconds 39 | DEFAULT_SCANINFO_STORE_TIME = 0 # in hours 40 | DEFAULT_MAX_SCAN = 0 # 0 = disable 41 | DEFAULT_MIN_FREE_MEM_SCAN_QUEUE = 0 # 0 = Disable 42 | DEFAULT_MAX_QUEUED_SCANS = 0 # 0 = Disable 43 | 44 | ParserType = argparse.ArgumentParser 45 | Arguments = argparse.Namespace 46 | 47 | logger = logging.getLogger(__name__) 48 | 49 | 50 | class CliParser: 51 | def __init__(self, description: str) -> None: 52 | """Create a command-line arguments parser for OSPD.""" 53 | self._name = description 54 | parser = argparse.ArgumentParser(description=description) 55 | 56 | parser.add_argument( 57 | '--version', action='store_true', help='Print version then exit.' 58 | ) 59 | 60 | parser.add_argument( 61 | '-s', 62 | '--config', 63 | nargs='?', 64 | default=DEFAULT_CONFIG_PATH, 65 | help='Configuration file path (default: %(default)s)', 66 | ) 67 | parser.add_argument( 68 | '--log-config', 69 | nargs='?', 70 | default=DEFAULT_LOG_CONFIG_PATH, 71 | help='Log configuration file path (default: %(default)s)', 72 | ) 73 | parser.add_argument( 74 | '-p', 75 | '--port', 76 | default=DEFAULT_PORT, 77 | type=self.network_port, 78 | help='TCP Port to listen on. Default: %(default)s', 79 | ) 80 | parser.add_argument( 81 | '-b', 82 | '--bind-address', 83 | default=DEFAULT_ADDRESS, 84 | dest='address', 85 | help='Address to listen on. Default: %(default)s', 86 | ) 87 | parser.add_argument( 88 | '-u', 89 | '--unix-socket', 90 | default=DEFAULT_UNIX_SOCKET_PATH, 91 | help='Unix file socket to listen on. Default: %(default)s', 92 | ) 93 | parser.add_argument( 94 | '--pid-file', 95 | default=DEFAULT_PID_PATH, 96 | help='Location of the file for the process ID. ' 97 | 'Default: %(default)s', 98 | ) 99 | parser.add_argument( 100 | '--lock-file-dir', 101 | default=DEFAULT_LOCKFILE_DIR_PATH, 102 | help='Directory where lock files are placed. Default: %(default)s', 103 | ) 104 | parser.add_argument( 105 | '-m', 106 | '--socket-mode', 107 | default=DEFAULT_UNIX_SOCKET_MODE, 108 | help='Unix file socket mode. Default: %(default)s', 109 | ) 110 | parser.add_argument( 111 | '-k', 112 | '--key-file', 113 | default=DEFAULT_KEY_FILE, 114 | help='Server key file. Default: %(default)s', 115 | ) 116 | parser.add_argument( 117 | '-c', 118 | '--cert-file', 119 | default=DEFAULT_CERT_FILE, 120 | help='Server cert file. Default: %(default)s', 121 | ) 122 | parser.add_argument( 123 | '--ca-file', 124 | default=DEFAULT_CA_FILE, 125 | help='CA cert file. Default: %(default)s', 126 | ) 127 | parser.add_argument( 128 | '-L', 129 | '--log-level', 130 | default='INFO', 131 | type=self.log_level, 132 | help='Wished level of logging. Default: %(default)s', 133 | ) 134 | parser.add_argument( 135 | '-f', 136 | '--foreground', 137 | action='store_true', 138 | help='Run in foreground and logs all messages to console.', 139 | ) 140 | parser.add_argument( 141 | '-t', 142 | '--stream-timeout', 143 | default=DEFAULT_STREAM_TIMEOUT, 144 | type=int, 145 | help='Stream timeout. Default: %(default)s', 146 | ) 147 | parser.add_argument( 148 | '-l', '--log-file', help='Path to the logging file.' 149 | ) 150 | parser.add_argument( 151 | '--niceness', 152 | default=DEFAULT_NICENESS, 153 | type=int, 154 | help='Start the scan with the given niceness. Default %(default)s', 155 | ) 156 | parser.add_argument( 157 | '--scaninfo-store-time', 158 | default=DEFAULT_SCANINFO_STORE_TIME, 159 | type=int, 160 | help='Time in hours a scan is stored before being considered ' 161 | 'forgotten and being delete from the scan table. ' 162 | 'Default %(default)s, disabled.', 163 | ) 164 | parser.add_argument( 165 | '--list-commands', 166 | action='store_true', 167 | help='Display all protocol commands', 168 | ) 169 | parser.add_argument( 170 | '--max-scans', 171 | default=DEFAULT_MAX_SCAN, 172 | type=int, 173 | help='Max. amount of parallel task that can be started. ' 174 | 'Default %(default)s, disabled', 175 | ) 176 | parser.add_argument( 177 | '--min-free-mem-scan-queue', 178 | default=DEFAULT_MIN_FREE_MEM_SCAN_QUEUE, 179 | type=int, 180 | help='Minimum free memory in MB required to run the scan. ' 181 | 'If no enough free memory is available, the scan queued. ' 182 | 'Default %(default)s, disabled', 183 | ) 184 | parser.add_argument( 185 | '--max-queued-scans', 186 | default=DEFAULT_MAX_QUEUED_SCANS, 187 | type=int, 188 | help='Maximum number allowed of queued scans before ' 189 | 'starting to reject new scans. ' 190 | 'Default %(default)s, disabled', 191 | ) 192 | 193 | self.parser = parser 194 | 195 | def network_port(self, string: str) -> int: 196 | """Check if provided string is a valid network port.""" 197 | 198 | value = int(string) 199 | if not 0 < value <= 65535: 200 | raise argparse.ArgumentTypeError( 201 | 'port must be in ]0,65535] interval' 202 | ) 203 | return value 204 | 205 | def log_level(self, string: str) -> str: 206 | """Check if provided string is a valid log level.""" 207 | 208 | if not hasattr(logging, string.upper()): 209 | raise argparse.ArgumentTypeError( 210 | 'log level must be one of {debug,info,warning,error,critical}' 211 | ) 212 | return string.upper() 213 | 214 | def _set_defaults(self, configfilename=None) -> None: 215 | self._config = self._load_config(configfilename) 216 | self.parser.set_defaults(**self._config.defaults()) 217 | 218 | def _load_config(self, configfile: str) -> Config: 219 | config = Config() 220 | 221 | if not configfile: 222 | return config 223 | 224 | configpath = Path(configfile) 225 | 226 | if not configpath.expanduser().resolve().exists(): 227 | logger.debug('Ignoring non existing config file %s', configfile) 228 | return config 229 | 230 | try: 231 | config.load(configpath, def_section=self._name) 232 | logger.debug('Loaded config %s', configfile) 233 | except Exception as e: # pylint: disable=broad-except 234 | raise RuntimeError( 235 | f'Error while parsing config file {configfile}. Error was ' 236 | f'{e}' 237 | ) from None 238 | 239 | return config 240 | 241 | def parse_arguments(self, args=None): 242 | # Parse args to get the config file path passed as option 243 | _args, _ = self.parser.parse_known_args(args) 244 | 245 | # Load the defaults from the config file if it exists. 246 | # This override also what it was passed as cmd option. 247 | self._set_defaults(_args.config) 248 | args, _ = self.parser.parse_known_args(args) 249 | 250 | return args 251 | 252 | 253 | def create_parser(description: str) -> CliParser: 254 | return CliParser(description) 255 | -------------------------------------------------------------------------------- /ospd/protocol.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ Helper classes for parsing and creating OSP XML requests and responses 19 | """ 20 | 21 | from typing import Dict, Union, List, Any 22 | 23 | from xml.etree.ElementTree import SubElement, Element, XMLPullParser 24 | 25 | from ospd.errors import OspdError 26 | 27 | 28 | class RequestParser: 29 | def __init__(self): 30 | self._parser = XMLPullParser(['start', 'end']) 31 | self._root_element = None 32 | 33 | def has_ended(self, data: bytes) -> bool: 34 | self._parser.feed(data) 35 | 36 | for event, element in self._parser.read_events(): 37 | if event == 'start' and self._root_element is None: 38 | self._root_element = element 39 | elif event == 'end' and self._root_element is not None: 40 | if element.tag == self._root_element.tag: 41 | return True 42 | 43 | return False 44 | 45 | 46 | class OspRequest: 47 | @staticmethod 48 | def process_vts_params( 49 | scanner_vts: Element, 50 | ) -> Dict[str, Union[Dict[str, str], List]]: 51 | """Receive an XML object with the Vulnerability Tests an their 52 | parameters to be use in a scan and return a dictionary. 53 | 54 | @param: XML element with vt subelements. Each vt has an 55 | id attribute. Optional parameters can be included 56 | as vt child. 57 | Example form: 58 | 59 | 60 | 61 | value 62 | 63 | 64 | 65 | 66 | 67 | @return: Dictionary containing the vts attribute and subelements, 68 | like the VT's id and VT's parameters. 69 | Example form: 70 | {'vt1': {}, 71 | 'vt2': {'value_id': 'value'}, 72 | 'vt_groups': ['family=debian', 'family=general']} 73 | """ 74 | vt_selection = {} # type: Dict 75 | filters = [] 76 | 77 | for vt in scanner_vts: 78 | if vt.tag == 'vt_single': 79 | vt_id = vt.attrib.get('id') 80 | vt_selection[vt_id] = {} 81 | 82 | for vt_value in vt: 83 | if not vt_value.attrib.get('id'): 84 | raise OspdError( 85 | 'Invalid VT preference. No attribute id' 86 | ) 87 | 88 | vt_value_id = vt_value.attrib.get('id') 89 | vt_value_value = vt_value.text if vt_value.text else '' 90 | vt_selection[vt_id][vt_value_id] = vt_value_value 91 | 92 | if vt.tag == 'vt_group': 93 | vts_filter = vt.attrib.get('filter', None) 94 | 95 | if vts_filter is None: 96 | raise OspdError('Invalid VT group. No filter given.') 97 | 98 | filters.append(vts_filter) 99 | 100 | vt_selection['vt_groups'] = filters 101 | 102 | return vt_selection 103 | 104 | @staticmethod 105 | def process_credentials_elements(cred_tree: Element) -> Dict: 106 | """Receive an XML object with the credentials to run 107 | a scan against a given target. 108 | 109 | @param: 110 | 111 | 112 | scanuser 113 | mypass 114 | 115 | 116 | smbuser 117 | mypass 118 | 119 | 120 | 121 | @return: Dictionary containing the credentials for a given target. 122 | Example form: 123 | {'ssh': {'type': type, 124 | 'port': port, 125 | 'username': username, 126 | 'password': pass, 127 | }, 128 | 'smb': {'type': type, 129 | 'username': username, 130 | 'password': pass, 131 | }, 132 | } 133 | """ 134 | credentials = {} # type: Dict 135 | 136 | for credential in cred_tree: 137 | service = credential.attrib.get('service') 138 | credentials[service] = {} 139 | credentials[service]['type'] = credential.attrib.get('type') 140 | 141 | if service == 'ssh': 142 | credentials[service]['port'] = credential.attrib.get('port') 143 | 144 | for param in credential: 145 | credentials[service][param.tag] = ( 146 | param.text if param.text else "" 147 | ) 148 | 149 | return credentials 150 | 151 | @staticmethod 152 | def process_alive_test_methods( 153 | alive_test_tree: Element, options: Dict 154 | ) -> None: 155 | """Receive an XML object with the alive test methods to run 156 | a scan with. Methods are added to the options Dict. 157 | 158 | @param 159 | 160 | boolean(1 or 0) 161 | boolean(1 or 0) 162 | boolean(1 or 0) 163 | boolean(1 or 0) 164 | boolean(1 or 0) 165 | 166 | """ 167 | for child in alive_test_tree: 168 | if child.tag == 'icmp': 169 | if child.text is not None: 170 | options['icmp'] = child.text 171 | if child.tag == 'tcp_ack': 172 | if child.text is not None: 173 | options['tcp_ack'] = child.text 174 | if child.tag == 'tcp_syn': 175 | if child.text is not None: 176 | options['tcp_syn'] = child.text 177 | if child.tag == 'arp': 178 | if child.text is not None: 179 | options['arp'] = child.text 180 | if child.tag == 'consider_alive': 181 | if child.text is not None: 182 | options['consider_alive'] = child.text 183 | 184 | @classmethod 185 | def process_target_element(cls, scanner_target: Element) -> Dict: 186 | """Receive an XML object with the target, ports and credentials to run 187 | a scan against. 188 | 189 | Arguments: 190 | Single XML target element. The target has and 191 | subelements. Hosts can be a single host, a host range, a 192 | comma-separated host list or a network address. 193 | and are optional. Therefore each 194 | ospd-scanner should check for a valid ones if needed. 195 | 196 | Example form: 197 | 198 | 199 | 192.168.0.0/24 200 | 22 201 | 202 | 203 | scanuser 204 | mypass 205 | 206 | 207 | smbuser 208 | mypass 209 | 210 | 211 | 212 | 213 | 1 214 | 0 215 | 216 | 217 | Return: 218 | A Dict hosts, port, {credentials}, exclude_hosts, options]. 219 | 220 | Example form: 221 | 222 | { 223 | 'hosts': '192.168.0.0/24', 224 | 'port': '22', 225 | 'credentials': {'smb': {'type': type, 226 | 'port': port, 227 | 'username': username, 228 | 'password': pass, 229 | } 230 | }, 231 | 232 | 'exclude_hosts': '', 233 | 'finished_hosts': '', 234 | 'options': {'alive_test': 'ALIVE_TEST_CONSIDER_ALIVE', 235 | 'alive_test_ports: '22,80,123', 236 | 'reverse_lookup_only': '1', 237 | 'reverse_lookup_unify': '0', 238 | }, 239 | } 240 | """ 241 | if scanner_target: 242 | exclude_hosts = '' 243 | finished_hosts = '' 244 | ports = '' 245 | hosts = None 246 | credentials = {} # type: Dict 247 | options = {} 248 | 249 | for child in scanner_target: 250 | if child.tag == 'hosts': 251 | hosts = child.text 252 | if child.tag == 'exclude_hosts': 253 | exclude_hosts = child.text 254 | if child.tag == 'finished_hosts': 255 | finished_hosts = child.text 256 | if child.tag == 'ports': 257 | ports = child.text 258 | if child.tag == 'credentials': 259 | credentials = cls.process_credentials_elements(child) 260 | if child.tag == 'alive_test_methods': 261 | options['alive_test_methods'] = '1' 262 | cls.process_alive_test_methods(child, options) 263 | if child.tag == 'alive_test': 264 | options['alive_test'] = child.text 265 | if child.tag == 'alive_test_ports': 266 | options['alive_test_ports'] = child.text 267 | if child.tag == 'reverse_lookup_unify': 268 | options['reverse_lookup_unify'] = child.text 269 | if child.tag == 'reverse_lookup_only': 270 | options['reverse_lookup_only'] = child.text 271 | 272 | if hosts: 273 | return { 274 | 'hosts': hosts, 275 | 'ports': ports, 276 | 'credentials': credentials, 277 | 'exclude_hosts': exclude_hosts, 278 | 'finished_hosts': finished_hosts, 279 | 'options': options, 280 | } 281 | else: 282 | raise OspdError('No target to scan') 283 | 284 | 285 | class OspResponse: 286 | @staticmethod 287 | def create_scanner_params_xml(scanner_params: Dict[str, Any]) -> Element: 288 | """Returns the OSP Daemon's scanner params in xml format.""" 289 | scanner_params_xml = Element('scanner_params') 290 | 291 | for param_id, param in scanner_params.items(): 292 | param_xml = SubElement(scanner_params_xml, 'scanner_param') 293 | 294 | for name, value in [('id', param_id), ('type', param['type'])]: 295 | param_xml.set(name, value) 296 | 297 | for name, value in [ 298 | ('name', param['name']), 299 | ('description', param['description']), 300 | ('default', param['default']), 301 | ('mandatory', param['mandatory']), 302 | ]: 303 | elem = SubElement(param_xml, name) 304 | elem.text = str(value) 305 | 306 | return scanner_params_xml 307 | -------------------------------------------------------------------------------- /ospd/resultlist.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | # pylint: disable=too-many-lines 19 | 20 | """ Class for handling list of resutls. 21 | """ 22 | 23 | from collections import OrderedDict 24 | from typing import Dict 25 | from ospd.misc import ResultType 26 | 27 | 28 | class ResultList: 29 | """Class for handling list of resutls.""" 30 | 31 | def __init__(self): 32 | self._result_list = list() 33 | 34 | def __len__(self): 35 | return len(self._result_list) 36 | 37 | def add_scan_host_detail_to_list( 38 | self, 39 | host: str = '', 40 | hostname: str = '', 41 | name: str = '', 42 | value: str = '', 43 | uri: str = '', 44 | ) -> None: 45 | """Adds a host detail result to result list.""" 46 | self.add_result_to_list( 47 | ResultType.HOST_DETAIL, 48 | host, 49 | hostname, 50 | name, 51 | value, 52 | uri, 53 | ) 54 | 55 | def add_scan_error_to_list( 56 | self, 57 | host: str = '', 58 | hostname: str = '', 59 | name: str = '', 60 | value: str = '', 61 | port: str = '', 62 | test_id='', 63 | uri: str = '', 64 | ) -> None: 65 | """Adds an error result to result list.""" 66 | self.add_result_to_list( 67 | ResultType.ERROR, 68 | host, 69 | hostname, 70 | name, 71 | value, 72 | port, 73 | test_id, 74 | uri, 75 | ) 76 | 77 | def add_scan_log_to_list( 78 | self, 79 | host: str = '', 80 | hostname: str = '', 81 | name: str = '', 82 | value: str = '', 83 | port: str = '', 84 | test_id: str = '', 85 | qod: str = '', 86 | uri: str = '', 87 | ) -> None: 88 | """Adds log result to a list of results.""" 89 | self.add_result_to_list( 90 | ResultType.LOG, 91 | host, 92 | hostname, 93 | name, 94 | value, 95 | port, 96 | test_id, 97 | '0.0', 98 | qod, 99 | uri, 100 | ) 101 | 102 | def add_scan_alarm_to_list( 103 | self, 104 | host: str = '', 105 | hostname: str = '', 106 | name: str = '', 107 | value: str = '', 108 | port: str = '', 109 | test_id: str = '', 110 | severity: str = '', 111 | qod: str = '', 112 | uri: str = '', 113 | ) -> None: 114 | """Adds an alarm result to a result list.""" 115 | self.add_result_to_list( 116 | ResultType.ALARM, 117 | host, 118 | hostname, 119 | name, 120 | value, 121 | port, 122 | test_id, 123 | severity, 124 | qod, 125 | uri, 126 | ) 127 | 128 | def add_result_to_list( 129 | self, 130 | result_type: int, 131 | host: str = '', 132 | hostname: str = '', 133 | name: str = '', 134 | value: str = '', 135 | port: str = '', 136 | test_id: str = '', 137 | severity: str = '', 138 | qod: str = '', 139 | uri: str = '', 140 | ) -> None: 141 | 142 | result = OrderedDict() # type: Dict 143 | result['type'] = result_type 144 | result['name'] = name 145 | result['severity'] = severity 146 | result['test_id'] = test_id 147 | result['value'] = value 148 | result['host'] = host 149 | result['hostname'] = hostname 150 | result['port'] = port 151 | result['qod'] = qod 152 | result['uri'] = uri 153 | self._result_list.append(result) 154 | 155 | def __iter__(self): 156 | return iter(self._result_list) 157 | -------------------------------------------------------------------------------- /ospd/server.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ 19 | Module for serving and streaming data 20 | """ 21 | 22 | import logging 23 | import socket 24 | import ssl 25 | import time 26 | import threading 27 | import socketserver 28 | 29 | from abc import ABC, abstractmethod 30 | from pathlib import Path 31 | from typing import Callable, Optional, Tuple, Union 32 | 33 | from ospd.errors import OspdError 34 | 35 | logger = logging.getLogger(__name__) 36 | 37 | DEFAULT_BUFSIZE = 1024 38 | 39 | 40 | class Stream: 41 | def __init__(self, sock: socket.socket, stream_timeout: int): 42 | self.socket = sock 43 | self.socket.settimeout(stream_timeout) 44 | 45 | def close(self): 46 | """Close the stream""" 47 | try: 48 | self.socket.shutdown(socket.SHUT_RDWR) 49 | except OSError as e: 50 | logger.debug( 51 | "Ignoring error while shutting down the connection. %s", e 52 | ) 53 | 54 | self.socket.close() 55 | 56 | def read(self, bufsize: Optional[int] = DEFAULT_BUFSIZE) -> bytes: 57 | """Read at maximum bufsize data from the stream""" 58 | data = self.socket.recv(bufsize) 59 | 60 | if not data: 61 | logger.debug('Client closed the connection') 62 | 63 | return data 64 | 65 | def write(self, data: bytes) -> bool: 66 | """Send data in chunks of DEFAULT_BUFSIZE to the client""" 67 | b_start = 0 68 | b_end = DEFAULT_BUFSIZE 69 | ret_success = True 70 | 71 | while True: 72 | if b_end > len(data): 73 | try: 74 | self.socket.send(data[b_start:]) 75 | except (socket.error, BrokenPipeError) as e: 76 | logger.error("Error sending data to the client. %s", e) 77 | ret_success = False 78 | finally: 79 | return ret_success # pylint: disable=lost-exception 80 | 81 | try: 82 | b_sent = self.socket.send(data[b_start:b_end]) 83 | except (socket.error, BrokenPipeError) as e: 84 | logger.error("Error sending data to the client. %s", e) 85 | return False 86 | 87 | b_start = b_end 88 | b_end += b_sent 89 | 90 | return ret_success 91 | 92 | 93 | StreamCallbackType = Callable[[Stream], None] 94 | 95 | InetAddress = Tuple[str, int] 96 | 97 | 98 | def validate_cacert_file(cacert: str): 99 | """Check if provided file is a valid CA Certificate""" 100 | try: 101 | context = ssl.create_default_context(cafile=cacert) 102 | except AttributeError: 103 | # Python version < 2.7.9 104 | return 105 | except IOError: 106 | raise OspdError('CA Certificate not found') from None 107 | 108 | try: 109 | not_after = context.get_ca_certs()[0]['notAfter'] 110 | not_after = ssl.cert_time_to_seconds(not_after) 111 | not_before = context.get_ca_certs()[0]['notBefore'] 112 | not_before = ssl.cert_time_to_seconds(not_before) 113 | except (KeyError, IndexError): 114 | raise OspdError('CA Certificate is erroneous') from None 115 | 116 | now = int(time.time()) 117 | if not_after < now: 118 | raise OspdError('CA Certificate expired') 119 | 120 | if not_before > now: 121 | raise OspdError('CA Certificate not active yet') 122 | 123 | 124 | class RequestHandler(socketserver.BaseRequestHandler): 125 | """Class to handle the request.""" 126 | 127 | def handle(self): 128 | self.server.handle_request(self.request, self.client_address) 129 | 130 | 131 | class BaseServer(ABC): 132 | def __init__(self, stream_timeout: int): 133 | self.server = None 134 | self.stream_timeout = stream_timeout 135 | 136 | @abstractmethod 137 | def start(self, stream_callback: StreamCallbackType): 138 | """Starts a server with capabilities to handle multiple client 139 | connections simultaneously. 140 | If a new client connects the stream_callback is called with a Stream 141 | 142 | Arguments: 143 | stream_callback (function): Callback function to be called when 144 | a stream is ready 145 | """ 146 | 147 | def close(self): 148 | """Shutdown the server""" 149 | self.server.shutdown() 150 | self.server.server_close() 151 | 152 | @abstractmethod 153 | def handle_request(self, request, client_address): 154 | """Handle an incoming client request""" 155 | 156 | def _start_threading_server(self): 157 | server_thread = threading.Thread(target=self.server.serve_forever) 158 | server_thread.daemon = True 159 | server_thread.start() 160 | 161 | 162 | class SocketServerMixin: 163 | # Use daemon mode to circrumvent a memory leak 164 | # (reported at https://bugs.python.org/issue37193). 165 | # 166 | # Daemonic threads are killed immediately by the python interpreter without 167 | # waiting for until they are finished. 168 | # 169 | # Maybe block_on_close = True could work too. 170 | # In that case the interpreter waits for the threads to finish but doesn't 171 | # track them in the _threads list. 172 | daemon_threads = True 173 | 174 | def __init__(self, server: BaseServer, address: Union[str, InetAddress]): 175 | self.server = server 176 | super().__init__(address, RequestHandler, bind_and_activate=True) 177 | 178 | def handle_request(self, request, client_address): 179 | self.server.handle_request(request, client_address) 180 | 181 | 182 | class ThreadedUnixSocketServer( 183 | SocketServerMixin, 184 | socketserver.ThreadingUnixStreamServer, 185 | ): 186 | pass 187 | 188 | 189 | class ThreadedTlsSocketServer( 190 | SocketServerMixin, 191 | socketserver.ThreadingTCPServer, 192 | ): 193 | pass 194 | 195 | 196 | class UnixSocketServer(BaseServer): 197 | """Server for accepting connections via a Unix domain socket""" 198 | 199 | def __init__(self, socket_path: str, socket_mode: str, stream_timeout: int): 200 | super().__init__(stream_timeout) 201 | self.socket_path = Path(socket_path) 202 | self.socket_mode = int(socket_mode, 8) 203 | 204 | def _cleanup_socket(self): 205 | if self.socket_path.exists(): 206 | self.socket_path.unlink() 207 | 208 | def _create_parent_dirs(self): 209 | # create all parent directories for the socket path 210 | parent = self.socket_path.parent 211 | parent.mkdir(parents=True, exist_ok=True) 212 | 213 | def start(self, stream_callback: StreamCallbackType): 214 | self._cleanup_socket() 215 | self._create_parent_dirs() 216 | 217 | try: 218 | self.stream_callback = stream_callback 219 | self.server = ThreadedUnixSocketServer(self, str(self.socket_path)) 220 | self._start_threading_server() 221 | except OSError as e: 222 | logger.error("Couldn't bind socket on %s", str(self.socket_path)) 223 | raise OspdError( 224 | f"Couldn't bind socket on {str(self.socket_path)}. {e}" 225 | ) from e 226 | 227 | if self.socket_path.exists(): 228 | self.socket_path.chmod(self.socket_mode) 229 | 230 | def close(self): 231 | super().close() 232 | self._cleanup_socket() 233 | 234 | def handle_request(self, request, client_address): 235 | logger.debug("New request from %s", str(self.socket_path)) 236 | 237 | stream = Stream(request, self.stream_timeout) 238 | self.stream_callback(stream) 239 | 240 | 241 | class TlsServer(BaseServer): 242 | """Server for accepting TLS encrypted connections via a TCP socket""" 243 | 244 | def __init__( 245 | self, 246 | address: str, 247 | port: int, 248 | cert_file: str, 249 | key_file: str, 250 | ca_file: str, 251 | stream_timeout: int, 252 | ): 253 | super().__init__(stream_timeout) 254 | self.socket = (address, port) 255 | 256 | if not Path(cert_file).exists(): 257 | raise OspdError(f'cert file {cert_file} not found') 258 | 259 | if not Path(key_file).exists(): 260 | raise OspdError(f'key file {key_file} not found') 261 | 262 | if not Path(ca_file).exists(): 263 | raise OspdError(f'CA file {ca_file} not found') 264 | 265 | validate_cacert_file(ca_file) 266 | 267 | protocol = ssl.PROTOCOL_SSLv23 268 | self.tls_context = ssl.SSLContext(protocol) 269 | self.tls_context.verify_mode = ssl.CERT_REQUIRED 270 | 271 | self.tls_context.load_cert_chain(cert_file, keyfile=key_file) 272 | self.tls_context.load_verify_locations(ca_file) 273 | 274 | def start(self, stream_callback: StreamCallbackType): 275 | try: 276 | self.stream_callback = stream_callback 277 | self.server = ThreadedTlsSocketServer(self, self.socket) 278 | self._start_threading_server() 279 | except OSError as e: 280 | logger.error( 281 | "Couldn't bind socket on %s:%s", self.socket[0], self.socket[1] 282 | ) 283 | raise OspdError( 284 | f"Couldn't bind socket on {self.socket[0]}:{self.socket[1]}. " 285 | f"{e}" 286 | ) from e 287 | 288 | def handle_request(self, request, client_address): 289 | logger.debug("New connection from %s", client_address) 290 | 291 | req_socket = self.tls_context.wrap_socket(request, server_side=True) 292 | 293 | stream = Stream(req_socket, self.stream_timeout) 294 | self.stream_callback(stream) 295 | -------------------------------------------------------------------------------- /ospd/timer.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | import time 19 | import logging 20 | 21 | from ospd.errors import OspdError 22 | 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | class TimerError(OspdError): 28 | """Timer errors""" 29 | 30 | 31 | class Timer: 32 | def __init__( 33 | self, 34 | name: str = None, 35 | text: str = "{}: Elapsed time: {:0.4f} seconds", 36 | logger=logger.debug, # pylint: disable=redefined-outer-name 37 | ): 38 | self._start_time = None 39 | self._name = name 40 | self._text = text 41 | self._logger = logger 42 | 43 | def __enter__(self): 44 | self.start() 45 | return self 46 | 47 | def __exit__(self, exc_type, exc_value, exc_tb): 48 | self.stop() 49 | 50 | @staticmethod 51 | def create(name) -> "Timer": 52 | timer = Timer(name) 53 | timer.start() 54 | return timer 55 | 56 | def start(self): 57 | """Start a new timer""" 58 | self._start_time = time.perf_counter() 59 | 60 | def stop(self): 61 | if not self._start_time: 62 | raise TimerError('Timer is not running.') 63 | 64 | duration = time.perf_counter() - self._start_time 65 | 66 | if self._logger: 67 | self._logger(self._text.format(self._name, duration)) 68 | 69 | return duration 70 | -------------------------------------------------------------------------------- /ospd/vtfilter.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ Vulnerability Test Filter class. 19 | """ 20 | import re 21 | import operator 22 | from typing import Dict, List, Optional 23 | 24 | from ospd.errors import OspdCommandError 25 | 26 | from .vts import Vts 27 | 28 | 29 | class VtsFilter: 30 | """Helper class to filter Vulnerability Tests""" 31 | 32 | def __init__(self) -> None: 33 | """Initialize filter operator and allowed filters.""" 34 | self.filter_operator = { 35 | '<': operator.lt, 36 | '>': operator.gt, 37 | '=': operator.eq, 38 | } 39 | 40 | self.allowed_filter = { 41 | 'creation_time': self.format_vt_creation_time, 42 | 'modification_time': self.format_vt_modification_time, 43 | } 44 | 45 | def parse_filters(self, vt_filter: str) -> List: 46 | """Parse a string containing one or more filters 47 | and return a list of filters 48 | 49 | Arguments: 50 | vt_filter (string): String containing filters separated with 51 | semicolon. 52 | Return: 53 | List with filters. Each filters is a list with 3 elements 54 | e.g. [arg, operator, value] 55 | """ 56 | 57 | filter_list = vt_filter.split(';') 58 | filters = list() 59 | 60 | for single_filter in filter_list: 61 | filter_aux = re.split(r'(\W)', single_filter, 1) 62 | 63 | if len(filter_aux) < 3: 64 | raise OspdCommandError( 65 | "Invalid number of argument in the filter", "get_vts" 66 | ) 67 | 68 | _element, _oper, _val = filter_aux 69 | 70 | if _element not in self.allowed_filter: 71 | raise OspdCommandError("Invalid filter element", "get_vts") 72 | 73 | if _oper not in self.filter_operator: 74 | raise OspdCommandError("Invalid filter operator", "get_vts") 75 | 76 | filters.append(filter_aux) 77 | 78 | return filters 79 | 80 | def format_vt_creation_time(self, value): 81 | """In case the given creationdatetime value must be formatted, 82 | this function must be implemented by the wrapper 83 | """ 84 | return value 85 | 86 | def format_vt_modification_time(self, value): 87 | """In case the given modification datetime value must be formatted, 88 | this function must be implemented by the wrapper 89 | """ 90 | return value 91 | 92 | def format_filter_value(self, element: str, value: Dict): 93 | """Calls the specific function to format value, 94 | depending on the given element. 95 | 96 | Arguments: 97 | element (string): The element of the VT to be formatted. 98 | value (dictionary): The element value. 99 | 100 | Returns: 101 | Returns a formatted value. 102 | 103 | """ 104 | format_func = self.allowed_filter.get(element) 105 | return format_func(value) 106 | 107 | def get_filtered_vts_list( 108 | self, vts: Vts, vt_filter: str 109 | ) -> Optional[List[str]]: 110 | """Gets a collection of vulnerability test from the vts dictionary, 111 | which match the filter. 112 | 113 | Arguments: 114 | vt_filter: Filter to apply to the vts collection. 115 | vts: The complete vts collection. 116 | 117 | Returns: 118 | List with filtered vulnerability tests. The list can be empty. 119 | None in case of filter parse failure. 120 | """ 121 | if not vt_filter: 122 | raise OspdCommandError('vt_filter: A valid filter is required.') 123 | 124 | filters = self.parse_filters(vt_filter) 125 | if not filters: 126 | return None 127 | 128 | vt_oid_list = list(vts) 129 | 130 | for _element, _oper, _filter_val in filters: 131 | for vt_oid in vts: 132 | if vt_oid not in vt_oid_list: 133 | continue 134 | 135 | vt = vts.get(vt_oid) 136 | if vt is None or not vt.get(_element): 137 | vt_oid_list.remove(vt_oid) 138 | continue 139 | 140 | _elem_val = vt.get(_element) 141 | _val = self.format_filter_value(_element, _elem_val) 142 | 143 | if self.filter_operator[_oper](_val, _filter_val): 144 | continue 145 | else: 146 | vt_oid_list.remove(vt_oid) 147 | 148 | return vt_oid_list 149 | -------------------------------------------------------------------------------- /ospd/vts.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ Classes for storing VTs 19 | """ 20 | import logging 21 | import multiprocessing 22 | from hashlib import sha256 23 | import re 24 | 25 | from copy import deepcopy 26 | from typing import ( 27 | Dict, 28 | Any, 29 | Type, 30 | Iterator, 31 | Iterable, 32 | Tuple, 33 | ) 34 | 35 | from ospd.errors import OspdError 36 | 37 | logger = logging.getLogger(__name__) 38 | 39 | DEFAULT_VT_ID_PATTERN = re.compile("[0-9a-zA-Z_\\-:.]{1,80}") 40 | 41 | 42 | class Vts: 43 | def __init__( 44 | self, 45 | storage: Type[Dict] = None, 46 | vt_id_pattern=DEFAULT_VT_ID_PATTERN, 47 | ): 48 | self.storage = storage 49 | 50 | self.vt_id_pattern = vt_id_pattern 51 | self._vts = None 52 | self.sha256_hash = None 53 | 54 | self.is_cache_available = True 55 | 56 | def __contains__(self, key: str) -> bool: 57 | return key in self._vts 58 | 59 | def __iter__(self) -> Iterator[str]: 60 | if hasattr(self.vts, '__iter__'): 61 | return self.vts.__iter__() 62 | 63 | def __getitem__(self, key): 64 | return self.vts[key] 65 | 66 | def items(self) -> Iterator[Tuple[str, Dict]]: 67 | return iter(self.vts.items()) 68 | 69 | def __len__(self) -> int: 70 | return len(self.vts) 71 | 72 | def __init_vts(self): 73 | if self.storage: 74 | self._vts = self.storage() 75 | else: 76 | self._vts = multiprocessing.Manager().dict() 77 | 78 | @property 79 | def vts(self) -> Dict[str, Any]: 80 | if self._vts is None: 81 | self.__init_vts() 82 | 83 | return self._vts 84 | 85 | def add( 86 | self, 87 | vt_id: str, 88 | name: str = None, 89 | vt_params: str = None, 90 | vt_refs: str = None, 91 | custom: str = None, 92 | vt_creation_time: str = None, 93 | vt_modification_time: str = None, 94 | vt_dependencies: str = None, 95 | summary: str = None, 96 | impact: str = None, 97 | affected: str = None, 98 | insight: str = None, 99 | solution: str = None, 100 | solution_t: str = None, 101 | solution_m: str = None, 102 | detection: str = None, 103 | qod_t: str = None, 104 | qod_v: str = None, 105 | severities: str = None, 106 | ) -> None: 107 | """Add a vulnerability test information. 108 | 109 | IMPORTANT: The VT's Data Manager will store the vts collection. 110 | If the collection is considerably big and it will be consultated 111 | intensible during a routine, consider to do a deepcopy(), since 112 | accessing the shared memory in the data manager is very expensive. 113 | At the end of the routine, the temporal copy must be set to None 114 | and deleted. 115 | """ 116 | if not vt_id: 117 | raise OspdError(f'Invalid vt_id {vt_id}') 118 | 119 | if self.vt_id_pattern.fullmatch(vt_id) is None: 120 | raise OspdError(f'Invalid vt_id {vt_id}') 121 | 122 | if vt_id in self.vts: 123 | raise OspdError(f'vt_id {vt_id} already exists') 124 | 125 | if name is None: 126 | name = '' 127 | 128 | vt = {'name': name} 129 | if custom is not None: 130 | vt["custom"] = custom 131 | if vt_params is not None: 132 | vt["vt_params"] = vt_params 133 | if vt_refs is not None: 134 | vt["vt_refs"] = vt_refs 135 | if vt_dependencies is not None: 136 | vt["vt_dependencies"] = vt_dependencies 137 | if vt_creation_time is not None: 138 | vt["creation_time"] = vt_creation_time 139 | if vt_modification_time is not None: 140 | vt["modification_time"] = vt_modification_time 141 | if summary is not None: 142 | vt["summary"] = summary 143 | if impact is not None: 144 | vt["impact"] = impact 145 | if affected is not None: 146 | vt["affected"] = affected 147 | if insight is not None: 148 | vt["insight"] = insight 149 | 150 | if solution is not None: 151 | vt["solution"] = solution 152 | if solution_t is not None: 153 | vt["solution_type"] = solution_t 154 | if solution_m is not None: 155 | vt["solution_method"] = solution_m 156 | 157 | if detection is not None: 158 | vt["detection"] = detection 159 | 160 | if qod_t is not None: 161 | vt["qod_type"] = qod_t 162 | elif qod_v is not None: 163 | vt["qod"] = qod_v 164 | 165 | if severities is not None: 166 | vt["severities"] = severities 167 | 168 | self.vts[vt_id] = vt 169 | 170 | def get(self, vt_id: str) -> Dict[str, Any]: 171 | return self.vts.get(vt_id) 172 | 173 | def keys(self) -> Iterable[str]: 174 | return self.vts.keys() 175 | 176 | def clear(self) -> None: 177 | self._vts.clear() 178 | self._vts = None 179 | 180 | def copy(self) -> "Vts": 181 | copy = Vts(self.storage, vt_id_pattern=self.vt_id_pattern) 182 | copy._vts = deepcopy(self._vts) # pylint: disable=protected-access 183 | return copy 184 | 185 | def calculate_vts_collection_hash(self, include_vt_params: bool = True): 186 | """Calculate the vts collection sha256 hash.""" 187 | if not self._vts: 188 | logger.debug( 189 | "Error calculating VTs collection hash. Cache is empty" 190 | ) 191 | return 192 | 193 | m = sha256() # pylint: disable=invalid-name 194 | 195 | # for a reproducible hash calculation 196 | # the vts must already be sorted in the dictionary. 197 | for vt_id, vt in self.vts.items(): 198 | param_chain = "" 199 | vt_params = vt.get('vt_params') 200 | if include_vt_params and vt_params: 201 | for _, param in sorted(vt_params.items()): 202 | param_chain += ( 203 | param.get('id') 204 | + param.get('name') 205 | + param.get('default') 206 | ) 207 | 208 | m.update( 209 | (vt_id + vt.get('modification_time')).encode('utf-8') 210 | + param_chain.encode('utf-8') 211 | ) 212 | 213 | self.sha256_hash = m.hexdigest() 214 | -------------------------------------------------------------------------------- /ospd/xml.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ OSP XML utils class. 19 | """ 20 | 21 | import re 22 | 23 | from typing import List, Dict, Any, Union 24 | 25 | from xml.sax.saxutils import escape, quoteattr 26 | from xml.etree.ElementTree import tostring, Element 27 | 28 | from ospd.misc import ResultType 29 | 30 | 31 | r = re.compile( # pylint: disable=invalid-name 32 | r'(.*?)(?:([^\x09\x0A\x0D\x20-\x7E\x85\xA0-\xFF' 33 | + r'\u0100-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD])|([\n])|$)' 34 | ) 35 | 36 | 37 | def split_invalid_xml(result_text: str) -> Union[List[Union[str, int]], str]: 38 | """Search for occurrence of non printable chars and replace them 39 | with the integer representation the Unicode code. The original string 40 | is splitted where a non printable char is found. 41 | """ 42 | splitted_string = [] 43 | 44 | def replacer(match): 45 | regex_g1 = match.group(1) 46 | if len(regex_g1) > 0: 47 | splitted_string.append(regex_g1) 48 | regex_g2 = match.group(2) 49 | if regex_g2 is not None: 50 | splitted_string.append(ord(regex_g2)) 51 | regex_g3 = match.group(3) 52 | if regex_g3 is not None: 53 | splitted_string.append(regex_g3) 54 | return "" 55 | 56 | re.sub(r, replacer, result_text) 57 | return splitted_string 58 | 59 | 60 | def escape_ctrl_chars(result_text): 61 | """Replace non printable chars in result_text with an hexa code 62 | in string format. 63 | """ 64 | escaped_str = '' 65 | for fragment in split_invalid_xml(result_text): 66 | if isinstance(fragment, int): 67 | escaped_str += f'\\x{fragment:00004X}' 68 | else: 69 | escaped_str += fragment 70 | 71 | return escaped_str 72 | 73 | 74 | def get_result_xml(result): 75 | """Formats a scan result to XML format. 76 | 77 | Arguments: 78 | result (dict): Dictionary with a scan result. 79 | 80 | Return: 81 | Result as xml element object. 82 | """ 83 | 84 | result_xml = Element('result') 85 | for name, value in [ 86 | ('name', result['name']), 87 | ('type', ResultType.get_str(result['type'])), 88 | ('severity', result['severity']), 89 | ('host', result['host']), 90 | ('hostname', result['hostname']), 91 | ('test_id', result['test_id']), 92 | ('port', result['port']), 93 | ('qod', result['qod']), 94 | ('uri', result['uri']), 95 | ]: 96 | result_xml.set(name, escape(str(value))) 97 | if result['value'] is not None: 98 | result_xml.text = escape_ctrl_chars(result['value']) 99 | 100 | return result_xml 101 | 102 | 103 | def get_progress_xml(progress: Dict[str, int]): 104 | """Formats a scan progress to XML format. 105 | 106 | Arguments: 107 | progress (dict): Dictionary with a scan progress. 108 | 109 | Return: 110 | Progress as xml element object. 111 | """ 112 | 113 | progress_xml = Element('progress') 114 | for progress_item, value in progress.items(): 115 | elem = None 116 | if progress_item == 'current_hosts': 117 | for host, h_progress in value.items(): 118 | elem = Element('host') 119 | elem.set('name', host) 120 | elem.text = str(h_progress) 121 | progress_xml.append(elem) 122 | else: 123 | elem = Element(progress_item) 124 | elem.text = str(value) 125 | progress_xml.append(elem) 126 | return progress_xml 127 | 128 | 129 | def simple_response_str( 130 | command: str, 131 | status: int, 132 | status_text: str, 133 | content: Union[str, Element, List[str], List[Element]] = "", 134 | ) -> bytes: 135 | """Creates an OSP response XML string. 136 | 137 | Arguments: 138 | command (str): OSP Command to respond to. 139 | status (int): Status of the response. 140 | status_text (str): Status text of the response. 141 | content (str): Text part of the response XML element. 142 | 143 | Return: 144 | String of response in xml format. 145 | """ 146 | response = Element(f'{command}_response') 147 | 148 | for name, value in [('status', str(status)), ('status_text', status_text)]: 149 | response.set(name, escape(str(value))) 150 | 151 | if isinstance(content, list): 152 | for elem in content: 153 | if isinstance(elem, Element): 154 | response.append(elem) 155 | elif isinstance(content, Element): 156 | response.append(content) 157 | elif content is not None: 158 | response.text = escape_ctrl_chars(content) 159 | 160 | return tostring(response, encoding='utf-8') 161 | 162 | 163 | def get_elements_from_dict(data: Dict[str, Any]) -> List[Element]: 164 | """Creates a list of etree elements from a dictionary 165 | 166 | Args: 167 | Dictionary of tags and their elements. 168 | 169 | Return: 170 | List of xml elements. 171 | """ 172 | 173 | responses = [] 174 | 175 | for tag, value in data.items(): 176 | elem = Element(tag) 177 | 178 | if isinstance(value, dict): 179 | for val in get_elements_from_dict(value): 180 | elem.append(val) 181 | elif isinstance(value, list): 182 | elem.text = ', '.join(value) 183 | elif value is not None: 184 | elem.text = escape_ctrl_chars(value) 185 | 186 | responses.append(elem) 187 | 188 | return responses 189 | 190 | 191 | def elements_as_text( 192 | elements: Dict[str, Union[str, Dict]], indent: int = 2 193 | ) -> str: 194 | """Returns the elements dictionary as formatted plain text.""" 195 | 196 | text = "" 197 | for elename, eledesc in elements.items(): 198 | if isinstance(eledesc, dict): 199 | desc_txt = elements_as_text(eledesc, indent + 2) 200 | desc_txt = ''.join(['\n', desc_txt]) 201 | elif isinstance(eledesc, str): 202 | desc_txt = ''.join([eledesc, '\n']) 203 | else: 204 | assert False, "Only string or dictionary" 205 | 206 | ele_txt = f"\t{' ' * indent}{elename: <22} {desc_txt}" 207 | 208 | text = ''.join([text, ele_txt]) 209 | 210 | return text 211 | 212 | 213 | class XmlStringHelper: 214 | """Class with methods to help the creation of a xml object in 215 | string format. 216 | """ 217 | 218 | def create_element(self, elem_name: str, end: bool = False) -> bytes: 219 | """Get a name and create the open element of an entity. 220 | 221 | Arguments: 222 | elem_name (str): The name of the tag element. 223 | end (bool): Create a initial tag if False, otherwise the end tag. 224 | 225 | Return: 226 | Encoded string representing a part of an xml element. 227 | """ 228 | if end: 229 | ret = f"" 230 | else: 231 | ret = f"<{elem_name}>" 232 | 233 | return ret.encode('utf-8') 234 | 235 | def create_response(self, command: str, end: bool = False) -> bytes: 236 | """Create or end an xml response. 237 | 238 | Arguments: 239 | command (str): The name of the command for the response element. 240 | end (bool): Create a initial tag if False, otherwise the end tag. 241 | 242 | Return: 243 | Encoded string representing a part of an xml element. 244 | """ 245 | if not command: 246 | return 247 | 248 | if end: 249 | return (f'').encode('utf-8') 250 | 251 | return (f'<{command}_response status="200" status_text="OK">').encode( 252 | 'utf-8' 253 | ) 254 | 255 | def add_element( 256 | self, 257 | content: Union[Element, str, list], 258 | xml_str: bytes = None, 259 | end: bool = False, 260 | ) -> bytes: 261 | """Create the initial or ending tag for a subelement, or add 262 | one or many xml elements 263 | 264 | Arguments: 265 | content (Element, str, list): Content to add. 266 | xml_str (bytes): Initial string where content to be added to. 267 | end (bool): Create a initial tag if False, otherwise the end tag. 268 | It will be added to the xml_str. 269 | 270 | Return: 271 | Encoded string representing a part of an xml element. 272 | """ 273 | 274 | if not xml_str: 275 | xml_str = b'' 276 | 277 | if content: 278 | if isinstance(content, list): 279 | for elem in content: 280 | xml_str = xml_str + tostring(elem, encoding='utf-8') 281 | elif isinstance(content, Element): 282 | xml_str = xml_str + tostring(content, encoding='utf-8') 283 | else: 284 | if end: 285 | xml_str = xml_str + self.create_element(content, False) 286 | else: 287 | xml_str = xml_str + self.create_element(content) 288 | 289 | return xml_str 290 | 291 | def add_attr( 292 | self, tag: bytes, attribute: str, value: Union[str, int] = None 293 | ) -> bytes: 294 | """Add an attribute to the beginning tag of an xml element. 295 | Arguments: 296 | tag (bytes): Tag to add the attribute to. 297 | attribute (str): Attribute name 298 | value (str): Attribute value 299 | Return: 300 | Tag in encoded string format with the given attribute 301 | """ 302 | if not tag: 303 | return None 304 | 305 | if not attribute: 306 | return tag 307 | 308 | if not value: 309 | value = '' 310 | 311 | return tag[:-1] + (f" {attribute}={quoteattr(str(value))}>").encode( 312 | 'utf-8' 313 | ) 314 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | 4 | [tool.poetry] 5 | name = "ospd" 6 | version = "21.10.0.dev1" 7 | description = "OSPD is a base for scanner wrappers which share the same communication protocol: OSP (Open Scanner Protocol)" 8 | authors = ["Greenbone Networks GmbH "] 9 | license = "AGPL-3.0-or-later" 10 | readme = "README.md" 11 | homepage = "https://github.com/greenbone/ospd" 12 | repository = "https://github.com/greenbone/ospd" 13 | classifiers = [ 14 | # Full list: https://pypi.org/pypi?%3Aaction=list_classifiers 15 | "Development Status :: 4 - Beta", 16 | "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", 17 | "Intended Audience :: Developers", 18 | "Intended Audience :: System Administrators", 19 | "Programming Language :: Python :: 3.7", 20 | "Programming Language :: Python :: 3.8", 21 | ] 22 | keywords = [ 23 | "Greenbone Vulnerability Management", 24 | "Vulnerability Scanner", 25 | "Open Scanner Protocol", 26 | "OSP", 27 | ] 28 | packages = [ 29 | { include = "ospd"}, 30 | { include = "tests", format = "sdist" }, 31 | { include = "COPYING", format = "sdist"}, 32 | { include = "CHANGELOG.md", format = "sdist"}, 33 | { include = "poetry.lock", format = "sdist"}, 34 | { include = "poetry.toml", format = "sdist"}, 35 | { include = "setup.py", format = "sdist"}, 36 | ] 37 | 38 | [tool.poetry.dependencies] 39 | python = "^3.7" 40 | psutil = "^5.5.1" 41 | lxml = "^4.5.2" 42 | defusedxml = ">=0.6,<0.8" 43 | paramiko = "^2.7.1" 44 | deprecated = "^1.2.10" 45 | 46 | [tool.poetry.dev-dependencies] 47 | pylint = "^2.11.1" 48 | autohooks-plugin-pylint = "^21.6.0" 49 | 50 | autohooks-plugin-black = {version = "^21.7.1", python = "^3.7"} 51 | black = {version = "21.7b0", python = "^3.7"} 52 | rope = "^0.21.0" 53 | pontos = "^21.10.2" 54 | 55 | [tool.black] 56 | line-length = 80 57 | target-version = ['py37', 'py38'] 58 | skip-string-normalization = true 59 | exclude = ''' 60 | /( 61 | \.git 62 | | \.hg 63 | | \.venv 64 | | \.circleci 65 | | \.github 66 | | \.vscode 67 | | _build 68 | | build 69 | | dist 70 | | docs 71 | )/ 72 | ''' 73 | 74 | [tool.autohooks] 75 | mode = "poetry" 76 | pre-commit = ['autohooks.plugins.black', 'autohooks.plugins.pylint'] 77 | 78 | [tool.pontos.version] 79 | version-module-file = "ospd/__version__.py" 80 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | # pylint: disable=invalid-name 19 | 20 | """ Setup configuration and management for module ospd 21 | Standard Python setup configuration, including support for PyPI. 22 | """ 23 | 24 | from os import path 25 | 26 | from setuptools import ( 27 | setup, 28 | find_packages, 29 | ) # Always prefer setuptools over distutils 30 | 31 | from ospd import __version__ 32 | 33 | here = path.abspath(path.dirname(__file__)) 34 | 35 | # Get the long description from the relevant file 36 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 37 | long_description = f.read() 38 | 39 | setup( 40 | name='ospd', 41 | # Versions should comply with PEP440. For a discussion on single-sourcing 42 | # the version across setup.py and the project code, see 43 | # http://packaging.python.org/en/latest/tutorial.html#version 44 | version=__version__, 45 | description=( 46 | 'OSPD is a base for scanner wrappers which share the ' 47 | 'same communication protocol: OSP (Open Scanner ' 48 | 'Protocol)' 49 | ), 50 | long_description=long_description, 51 | long_description_content_type='text/markdown', 52 | # The project's main homepage. 53 | url='http://www.openvas.org', 54 | # Author 55 | author='Greenbone Networks GmbH', 56 | author_email='info@greenbone.net', 57 | # License 58 | license='GPLv2+', 59 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 60 | classifiers=[ 61 | # How mature is this project? Common values are 62 | # 3 - Alpha 63 | # 4 - Beta 64 | # 5 - Production/Stable 65 | 'Development Status :: 4 - Beta', 66 | # Indicate who your project is intended for 67 | 'Intended Audience :: Developers', 68 | 'Topic :: Software Development :: Build Tools', 69 | # Pick your license as you wish (should match "license" above) 70 | 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', # pylint: disable=line-too-long 71 | # Specify the Python versions you support here. In particular, ensure 72 | # that you indicate whether you support Python 2, Python 3 or both. 73 | 'Programming Language :: Python :: 3.7', 74 | 'Programming Language :: Python :: 3.8', 75 | ], 76 | # What does your project relate to? 77 | keywords=['Greenbone Vulnerability Manager OSP'], 78 | python_requires='>=3.7', 79 | # List run-time dependencies here. These will be installed by pip when your 80 | # project is installed. For an analysis of "install_requires" vs pip's 81 | # requirements files see: 82 | # https://packaging.python.org/en/latest/technical.html#install-requires-vs-requirements-files 83 | install_requires=['paramiko', 'defusedxml', 'lxml', 'deprecated', 'psutil'], 84 | # You can just specify the packages manually here if your project is 85 | # simple. Or you can use find_packages(). 86 | packages=find_packages(exclude=['tests*']), 87 | # If there are data files included in your packages that need to be 88 | # installed, specify them here. 89 | include_package_data=True, 90 | package_data={'': []}, 91 | # Scripts. Define scripts here which should be installed in the 92 | # sys.prefix/bin directory. You can define an alternative place for 93 | # installation by setting the --install-scripts option of setup.py 94 | # scripts = [''], 95 | # To provide executable scripts, use entry points in preference to the 96 | # "scripts" keyword. Entry points provide cross-platform support and allow 97 | # pip to create the appropriate form of executable for the target platform. 98 | # entry_points={ 99 | # 'console_scripts': [ 100 | # 'sample=sample:main', 101 | # ], 102 | # }, 103 | test_suite="tests", 104 | ) 105 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/command/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /tests/command/test_command.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | from unittest import TestCase 19 | 20 | from ospd.command.registry import get_commands, remove_command 21 | from ospd.command.command import BaseCommand 22 | 23 | 24 | class BaseCommandTestCase(TestCase): 25 | def test_auto_register(self): 26 | commands = get_commands() 27 | before = len(commands) 28 | 29 | class Foo(BaseCommand): 30 | name = "foo" 31 | 32 | def handle_xml(self, xml): 33 | pass 34 | 35 | after = len(commands) 36 | 37 | try: 38 | 39 | self.assertEqual(before + 1, after) 40 | 41 | c_dict = {c.name: c for c in commands} 42 | 43 | self.assertIn('foo', c_dict) 44 | self.assertIs(c_dict['foo'], Foo) 45 | finally: 46 | remove_command(Foo) 47 | 48 | def test_basic_properties(self): 49 | class Foo(BaseCommand): 50 | name = "foo" 51 | attributes = {'lorem': 'ipsum'} 52 | elements = {'foo': 'bar'} 53 | description = 'bar' 54 | 55 | def handle_xml(self, xml): 56 | pass 57 | 58 | try: 59 | f = Foo({}) 60 | 61 | self.assertEqual(f.get_name(), 'foo') 62 | self.assertEqual(f.get_description(), 'bar') 63 | self.assertEqual(f.get_attributes(), {'lorem': 'ipsum'}) 64 | self.assertEqual(f.get_elements(), {'foo': 'bar'}) 65 | finally: 66 | remove_command(Foo) 67 | 68 | def test_as_dict(self): 69 | class Foo(BaseCommand): 70 | name = "foo" 71 | attributes = {'lorem': 'ipsum'} 72 | elements = {'foo': 'bar'} 73 | description = 'bar' 74 | 75 | def handle_xml(self, xml): 76 | pass 77 | 78 | try: 79 | f = Foo({}) 80 | 81 | f_dict = f.as_dict() 82 | 83 | self.assertEqual(f_dict['name'], 'foo') 84 | self.assertEqual(f_dict['description'], 'bar') 85 | self.assertEqual(f_dict['attributes'], {'lorem': 'ipsum'}) 86 | self.assertEqual(f_dict['elements'], {'foo': 'bar'}) 87 | finally: 88 | remove_command(Foo) 89 | -------------------------------------------------------------------------------- /tests/command/test_registry.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | from unittest import TestCase 19 | 20 | from ospd.command.registry import get_commands, register_command, remove_command 21 | 22 | 23 | COMMAND_NAMES = [ 24 | "help", 25 | "get_version", 26 | "get_performance", 27 | "get_scanner_details", 28 | "delete_scan", 29 | "get_vts", 30 | "stop_scan", 31 | "get_scans", 32 | "start_scan", 33 | "get_memory_usage", 34 | ] 35 | 36 | 37 | class RegistryTestCase(TestCase): 38 | def test_available_commands(self): 39 | commands = get_commands() 40 | 41 | self.assertEqual(len(COMMAND_NAMES), len(commands)) 42 | 43 | c_list = [c.name for c in commands] 44 | 45 | self.assertListEqual(COMMAND_NAMES, c_list) 46 | 47 | def test_register_command(self): 48 | commands = get_commands() 49 | before = len(commands) 50 | 51 | class Foo: 52 | name = 'foo' 53 | 54 | register_command(Foo) 55 | 56 | commands = get_commands() 57 | after = len(commands) 58 | 59 | try: 60 | self.assertEqual(before + 1, after) 61 | 62 | c_dict = {c.name: c for c in commands} 63 | 64 | self.assertIn('foo', c_dict) 65 | self.assertIs(c_dict['foo'], Foo) 66 | finally: 67 | remove_command(Foo) 68 | 69 | commands = get_commands() 70 | after2 = len(commands) 71 | 72 | self.assertEqual(before, after2) 73 | 74 | c_dict = {c.name: c for c in commands} 75 | self.assertNotIn('foo', c_dict) 76 | -------------------------------------------------------------------------------- /tests/helper.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | import time 19 | 20 | from unittest.mock import Mock 21 | 22 | from xml.etree import ElementTree as et 23 | 24 | from ospd.ospd import OSPDaemon 25 | 26 | 27 | def assert_called(mock: Mock): 28 | if hasattr(mock, 'assert_called'): 29 | return mock.assert_called() 30 | 31 | if not mock.call_count == 1: 32 | # pylint: disable=protected-access 33 | msg = ( 34 | f"Expected {mock._mock_name or 'mock'}' to have been " 35 | f"called once. Called {mock.call_count} times.{mock._calls_repr()}" 36 | ) 37 | raise AssertionError(msg) 38 | 39 | 40 | class FakePsutil: 41 | def __init__(self, available=None): 42 | self.available = available 43 | 44 | 45 | class FakeStream: 46 | def __init__(self, return_value=True): 47 | self.response = b'' 48 | self.return_value = return_value 49 | 50 | def write(self, data): 51 | self.response = self.response + data 52 | return self.return_value 53 | 54 | def get_response(self): 55 | return et.fromstring(self.response) 56 | 57 | 58 | class FakeDataManager: 59 | def __init__(self): 60 | pass 61 | 62 | def dict(self): 63 | return dict() 64 | 65 | 66 | class DummyWrapper(OSPDaemon): 67 | def __init__(self, results, checkresult=True): 68 | super().__init__() 69 | self.checkresult = checkresult 70 | self.results = results 71 | self.initialized = True 72 | self.scan_collection.data_manager = FakeDataManager() 73 | self.scan_collection.file_storage_dir = '/tmp' 74 | 75 | def check(self): 76 | return self.checkresult 77 | 78 | @staticmethod 79 | def get_custom_vt_as_xml_str(vt_id, custom): 80 | return 'static test' 81 | 82 | @staticmethod 83 | def get_params_vt_as_xml_str(vt_id, vt_params): 84 | return ( 85 | '' 86 | 'ABCTest ABC' 87 | 'yes' 88 | '' 89 | 'DEFTest DEF' 90 | 'no' 91 | ) 92 | 93 | @staticmethod 94 | def get_refs_vt_as_xml_str(vt_id, vt_refs): 95 | response = ( 96 | '' 97 | '' 98 | ) 99 | return response 100 | 101 | @staticmethod 102 | def get_dependencies_vt_as_xml_str(vt_id, vt_dependencies): 103 | response = ( 104 | '' 105 | '' 106 | '' 107 | '' 108 | ) 109 | 110 | return response 111 | 112 | @staticmethod 113 | def get_severities_vt_as_xml_str(vt_id, severities): 114 | response = ( 115 | 'AV:N/AC:L/Au:N/C:N/I:N/' 117 | 'A:P' 118 | ) 119 | 120 | return response 121 | 122 | @staticmethod 123 | def get_detection_vt_as_xml_str( 124 | vt_id, detection=None, qod_type=None, qod=None 125 | ): 126 | response = 'some detection' 127 | 128 | return response 129 | 130 | @staticmethod 131 | def get_summary_vt_as_xml_str(vt_id, summary): 132 | response = 'Some summary' 133 | 134 | return response 135 | 136 | @staticmethod 137 | def get_affected_vt_as_xml_str(vt_id, affected): 138 | response = 'Some affected' 139 | 140 | return response 141 | 142 | @staticmethod 143 | def get_impact_vt_as_xml_str(vt_id, impact): 144 | response = 'Some impact' 145 | 146 | return response 147 | 148 | @staticmethod 149 | def get_insight_vt_as_xml_str(vt_id, insight): 150 | response = 'Some insight' 151 | 152 | return response 153 | 154 | @staticmethod 155 | def get_solution_vt_as_xml_str( 156 | vt_id, solution, solution_type=None, solution_method=None 157 | ): 158 | response = 'Some solution' 159 | 160 | return response 161 | 162 | @staticmethod 163 | def get_creation_time_vt_as_xml_str( 164 | vt_id, vt_creation_time 165 | ): # pylint: disable=arguments-differ 166 | response = f'{vt_creation_time}' 167 | 168 | return response 169 | 170 | @staticmethod 171 | def get_modification_time_vt_as_xml_str( 172 | vt_id, vt_modification_time 173 | ): # pylint: disable=arguments-differ 174 | response = ( 175 | f'{vt_modification_time}' 176 | ) 177 | 178 | return response 179 | 180 | def exec_scan(self, scan_id): 181 | time.sleep(0.01) 182 | for res in self.results: 183 | if res.result_type == 'log': 184 | self.add_scan_log( 185 | scan_id, 186 | res.host, 187 | res.hostname, 188 | res.name, 189 | res.value, 190 | res.port, 191 | ) 192 | if res.result_type == 'error': 193 | self.add_scan_error( 194 | scan_id, 195 | res.host, 196 | res.hostname, 197 | res.name, 198 | res.value, 199 | res.port, 200 | ) 201 | elif res.result_type == 'host-detail': 202 | self.add_scan_host_detail( 203 | scan_id, 204 | res.host, 205 | res.hostname, 206 | res.name, 207 | res.value, 208 | ) 209 | elif res.result_type == 'alarm': 210 | self.add_scan_alarm( 211 | scan_id, 212 | res.host, 213 | res.hostname, 214 | res.name, 215 | res.value, 216 | res.port, 217 | res.test_id, 218 | res.severity, 219 | res.qod, 220 | ) 221 | else: 222 | raise ValueError(res.result_type) 223 | -------------------------------------------------------------------------------- /tests/test_argument_parser.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ Test module for command line arguments. 19 | """ 20 | 21 | import unittest 22 | 23 | from unittest.mock import patch 24 | 25 | from io import StringIO 26 | from typing import List 27 | 28 | from ospd.parser import ( 29 | create_parser, 30 | Arguments, 31 | DEFAULT_ADDRESS, 32 | DEFAULT_PORT, 33 | DEFAULT_KEY_FILE, 34 | DEFAULT_NICENESS, 35 | DEFAULT_SCANINFO_STORE_TIME, 36 | DEFAULT_CONFIG_PATH, 37 | DEFAULT_UNIX_SOCKET_PATH, 38 | DEFAULT_PID_PATH, 39 | DEFAULT_LOCKFILE_DIR_PATH, 40 | ) 41 | 42 | 43 | class ArgumentParserTestCase(unittest.TestCase): 44 | def setUp(self): 45 | self.parser = create_parser('Wrapper name') 46 | 47 | def parse_args(self, args: List[str]) -> Arguments: 48 | return self.parser.parse_arguments(args) 49 | 50 | @patch('sys.stderr', new_callable=StringIO) 51 | def test_port_interval(self, _mock_stderr): 52 | with self.assertRaises(SystemExit): 53 | self.parse_args(['--port=65536']) 54 | 55 | with self.assertRaises(SystemExit): 56 | self.parse_args(['--port=0']) 57 | 58 | args = self.parse_args(['--port=3353']) 59 | self.assertEqual(3353, args.port) 60 | 61 | @patch('sys.stderr', new_callable=StringIO) 62 | def test_port_as_string(self, _mock_stderr): 63 | with self.assertRaises(SystemExit): 64 | self.parse_args(['--port=abcd']) 65 | 66 | def test_address_param(self): 67 | args = self.parse_args('-b 1.2.3.4'.split()) 68 | self.assertEqual('1.2.3.4', args.address) 69 | 70 | def test_correct_lower_case_log_level(self): 71 | args = self.parse_args('-L error'.split()) 72 | self.assertEqual('ERROR', args.log_level) 73 | 74 | def test_correct_upper_case_log_level(self): 75 | args = self.parse_args('-L INFO'.split()) 76 | self.assertEqual('INFO', args.log_level) 77 | 78 | @patch('sys.stderr', new_callable=StringIO) 79 | def test_correct_log_level(self, _mock_stderr): 80 | with self.assertRaises(SystemExit): 81 | self.parse_args('-L blah'.split()) 82 | 83 | def test_non_existing_key(self): 84 | args = self.parse_args('-k foo'.split()) 85 | self.assertEqual('foo', args.key_file) 86 | 87 | def test_existing_key(self): 88 | args = self.parse_args('-k /etc/passwd'.split()) 89 | self.assertEqual('/etc/passwd', args.key_file) 90 | 91 | def test_defaults(self): 92 | args = self.parse_args([]) 93 | 94 | self.assertEqual(args.key_file, DEFAULT_KEY_FILE) 95 | self.assertEqual(args.niceness, DEFAULT_NICENESS) 96 | self.assertEqual(args.log_level, 'INFO') 97 | self.assertEqual(args.address, DEFAULT_ADDRESS) 98 | self.assertEqual(args.port, DEFAULT_PORT) 99 | self.assertEqual(args.scaninfo_store_time, DEFAULT_SCANINFO_STORE_TIME) 100 | self.assertEqual(args.config, DEFAULT_CONFIG_PATH) 101 | self.assertEqual(args.unix_socket, DEFAULT_UNIX_SOCKET_PATH) 102 | self.assertEqual(args.pid_file, DEFAULT_PID_PATH) 103 | self.assertEqual(args.lock_file_dir, DEFAULT_LOCKFILE_DIR_PATH) 104 | -------------------------------------------------------------------------------- /tests/test_cvss.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ Test module for cvss scoring calculation 19 | """ 20 | 21 | import unittest 22 | 23 | from ospd.cvss import CVSS 24 | 25 | 26 | class CvssTestCase(unittest.TestCase): 27 | def test_cvssv2(self): 28 | vector = 'AV:A/AC:L/Au:S/C:P/I:P/A:P' 29 | cvss_base = CVSS.cvss_base_v2_value(vector) 30 | 31 | self.assertEqual(cvss_base, 5.2) 32 | 33 | def test_cvssv3(self): 34 | vector = 'CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N' 35 | cvss_base = CVSS.cvss_base_v3_value(vector) 36 | 37 | self.assertEqual(cvss_base, 3.8) 38 | -------------------------------------------------------------------------------- /tests/test_datapickler.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | import pickle 19 | 20 | from pathlib import Path 21 | from hashlib import sha256 22 | from unittest import TestCase 23 | from unittest.mock import patch 24 | 25 | from ospd.errors import OspdCommandError 26 | from ospd.datapickler import DataPickler 27 | 28 | from .helper import assert_called 29 | 30 | 31 | class DataPecklerTestCase(TestCase): 32 | def test_store_data(self): 33 | data = {'foo', 'bar'} 34 | filename = 'scan_info_1' 35 | pickled_data = pickle.dumps(data) 36 | tmp_hash = sha256() 37 | tmp_hash.update(pickled_data) 38 | 39 | data_pickler = DataPickler('/tmp') 40 | ret = data_pickler.store_data(filename, data) 41 | 42 | self.assertEqual(ret, tmp_hash.hexdigest()) 43 | 44 | data_pickler.remove_file(filename) 45 | 46 | def test_store_data_failed(self): 47 | data = {'foo', 'bar'} 48 | filename = 'scan_info_1' 49 | 50 | data_pickler = DataPickler('/root') 51 | 52 | self.assertRaises( 53 | OspdCommandError, data_pickler.store_data, filename, data 54 | ) 55 | 56 | def test_store_data_check_permission(self): 57 | OWNER_ONLY_RW_PERMISSION = '0o100600' # pylint: disable=invalid-name 58 | data = {'foo', 'bar'} 59 | filename = 'scan_info_1' 60 | 61 | data_pickler = DataPickler('/tmp') 62 | data_pickler.store_data(filename, data) 63 | 64 | file_path = ( 65 | Path(data_pickler._storage_path) # pylint: disable=protected-access 66 | / filename 67 | ) 68 | self.assertEqual( 69 | oct(file_path.stat().st_mode), OWNER_ONLY_RW_PERMISSION 70 | ) 71 | 72 | data_pickler.remove_file(filename) 73 | 74 | def test_load_data(self): 75 | 76 | data_pickler = DataPickler('/tmp') 77 | 78 | data = {'foo', 'bar'} 79 | filename = 'scan_info_1' 80 | pickled_data = pickle.dumps(data) 81 | 82 | tmp_hash = sha256() 83 | tmp_hash.update(pickled_data) 84 | pickled_data_hash = tmp_hash.hexdigest() 85 | 86 | ret = data_pickler.store_data(filename, data) 87 | self.assertEqual(ret, pickled_data_hash) 88 | 89 | original_data = data_pickler.load_data(filename, pickled_data_hash) 90 | self.assertIsNotNone(original_data) 91 | 92 | self.assertIn('foo', original_data) 93 | 94 | @patch("ospd.datapickler.logger") 95 | def test_remove_file_failed(self, mock_logger): 96 | filename = 'inenxistent_file' 97 | data_pickler = DataPickler('/root') 98 | data_pickler.remove_file(filename) 99 | 100 | assert_called(mock_logger.error) 101 | 102 | @patch("ospd.datapickler.logger") 103 | def test_load_data_no_file(self, mock_logger): 104 | filename = 'scan_info_1' 105 | data_pickler = DataPickler('/tmp') 106 | 107 | data_loaded = data_pickler.load_data(filename, "1234") 108 | assert_called(mock_logger.error) 109 | self.assertIsNone(data_loaded) 110 | 111 | data_pickler.remove_file(filename) 112 | 113 | def test_load_data_corrupted(self): 114 | 115 | data_pickler = DataPickler('/tmp') 116 | 117 | data = {'foo', 'bar'} 118 | filename = 'scan_info_1' 119 | pickled_data = pickle.dumps(data) 120 | 121 | tmp_hash = sha256() 122 | tmp_hash.update(pickled_data) 123 | pickled_data_hash = tmp_hash.hexdigest() 124 | 125 | ret = data_pickler.store_data(filename, data) 126 | self.assertEqual(ret, pickled_data_hash) 127 | 128 | # courrupt data 129 | file_to_corrupt = ( 130 | Path(data_pickler._storage_path) # pylint: disable=protected-access 131 | / filename 132 | ) 133 | with file_to_corrupt.open('ab') as f: 134 | f.write(b'bar2') 135 | 136 | original_data = data_pickler.load_data(filename, pickled_data_hash) 137 | self.assertIsNone(original_data) 138 | 139 | data_pickler.remove_file(filename) 140 | -------------------------------------------------------------------------------- /tests/test_errors.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ Test module for OspdCommandError class 19 | """ 20 | 21 | import unittest 22 | 23 | from ospd.errors import OspdError, OspdCommandError, RequiredArgument 24 | 25 | 26 | class OspdCommandErrorTestCase(unittest.TestCase): 27 | def test_is_ospd_error(self): 28 | e = OspdCommandError('message') 29 | self.assertIsInstance(e, OspdError) 30 | 31 | def test_default_params(self): 32 | e = OspdCommandError('message') 33 | 34 | self.assertEqual('message', e.message) 35 | self.assertEqual(400, e.status) 36 | self.assertEqual('osp', e.command) 37 | 38 | def test_constructor(self): 39 | e = OspdCommandError('message', 'command', 304) 40 | 41 | self.assertEqual('message', e.message) 42 | self.assertEqual('command', e.command) 43 | self.assertEqual(304, e.status) 44 | 45 | def test_string_conversion(self): 46 | e = OspdCommandError('message foo bar', 'command', 304) 47 | 48 | self.assertEqual('message foo bar', str(e)) 49 | 50 | def test_as_xml(self): 51 | e = OspdCommandError('message') 52 | 53 | self.assertEqual( 54 | b'', e.as_xml() 55 | ) 56 | 57 | 58 | class RequiredArgumentTestCase(unittest.TestCase): 59 | def test_raise_exception(self): 60 | with self.assertRaises(RequiredArgument) as cm: 61 | raise RequiredArgument('foo', 'bar') 62 | 63 | ex = cm.exception 64 | self.assertEqual(ex.function, 'foo') 65 | self.assertEqual(ex.argument, 'bar') 66 | 67 | def test_string_conversion(self): 68 | ex = RequiredArgument('foo', 'bar') 69 | self.assertEqual(str(ex), 'foo: Argument bar is required') 70 | 71 | def test_is_ospd_error(self): 72 | e = RequiredArgument('foo', 'bar') 73 | self.assertIsInstance(e, OspdError) 74 | -------------------------------------------------------------------------------- /tests/test_protocol.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | import unittest 19 | 20 | from ospd.protocol import RequestParser 21 | 22 | 23 | class RequestParserTestCase(unittest.TestCase): 24 | def test_parse(self): 25 | parser = RequestParser() 26 | self.assertFalse(parser.has_ended(b'')) 27 | self.assertFalse(parser.has_ended(b'')) 28 | self.assertTrue(parser.has_ended(b'')) 29 | -------------------------------------------------------------------------------- /tests/test_ssh_daemon.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ Test module for ospd ssh support. 19 | """ 20 | 21 | import unittest 22 | 23 | from ospd import ospd_ssh 24 | from ospd.ospd_ssh import OSPDaemonSimpleSSH 25 | from .helper import FakeDataManager 26 | 27 | 28 | class FakeFile(object): 29 | def __init__(self, content): 30 | self.content = content 31 | 32 | def readlines(self): 33 | return self.content.split('\n') 34 | 35 | 36 | commands = None # pylint: disable=invalid-name 37 | 38 | 39 | class FakeSSHClient(object): 40 | def __init__(self): 41 | global commands # pylint: disable=global-statement,invalid-name 42 | commands = [] 43 | 44 | def set_missing_host_key_policy(self, policy): 45 | pass 46 | 47 | def connect(self, **kwargs): 48 | pass 49 | 50 | def exec_command(self, cmd): 51 | commands.append(cmd) 52 | return None, FakeFile(''), None 53 | 54 | def close(self): 55 | pass 56 | 57 | 58 | class FakeExceptions(object): 59 | AuthenticationException = None # pylint: disable=invalid-name 60 | 61 | 62 | class fakeparamiko(object): # pylint: disable=invalid-name 63 | @staticmethod 64 | def SSHClient(*args): # pylint: disable=invalid-name 65 | return FakeSSHClient(*args) 66 | 67 | @staticmethod 68 | def AutoAddPolicy(): # pylint: disable=invalid-name 69 | pass 70 | 71 | ssh_exception = FakeExceptions 72 | 73 | 74 | class DummyWrapper(OSPDaemonSimpleSSH): 75 | def __init__(self, niceness=10): 76 | super().__init__(niceness=niceness) 77 | self.scan_collection.data_manager = FakeDataManager() 78 | self.scan_collection.file_storage_dir = '/tmp' 79 | self.initialized = True 80 | 81 | def check(self): 82 | return True 83 | 84 | def exec_scan(self, scan_id: str): 85 | return 86 | 87 | 88 | class SSHDaemonTestCase(unittest.TestCase): 89 | def test_no_paramiko(self): 90 | ospd_ssh.paramiko = None 91 | 92 | with self.assertRaises(ImportError): 93 | OSPDaemonSimpleSSH() 94 | 95 | def test_run_command(self): 96 | ospd_ssh.paramiko = fakeparamiko 97 | 98 | daemon = DummyWrapper(niceness=10) 99 | scanid = daemon.create_scan( 100 | None, 101 | { 102 | 'target': 'host.example.com', 103 | 'ports': '80, 443', 104 | 'credentials': {}, 105 | 'exclude_hosts': '', 106 | 'finished_hosts': '', 107 | 'options': {}, 108 | }, 109 | dict(port=5, ssh_timeout=15, username_password='dummy:pw'), 110 | '', 111 | ) 112 | daemon.start_queued_scans() 113 | res = daemon.run_command(scanid, 'host.example.com', 'cat /etc/passwd') 114 | 115 | self.assertIsInstance(res, list) 116 | self.assertEqual(commands, ['nice -n 10 cat /etc/passwd']) 117 | 118 | def test_run_command_legacy_credential(self): 119 | ospd_ssh.paramiko = fakeparamiko 120 | 121 | daemon = DummyWrapper(niceness=10) 122 | scanid = daemon.create_scan( 123 | None, 124 | { 125 | 'target': 'host.example.com', 126 | 'ports': '80, 443', 127 | 'credentials': {}, 128 | 'exclude_hosts': '', 129 | 'finished_hosts': '', 130 | 'options': {}, 131 | }, 132 | dict(port=5, ssh_timeout=15, username='dummy', password='pw'), 133 | '', 134 | ) 135 | daemon.start_queued_scans() 136 | res = daemon.run_command(scanid, 'host.example.com', 'cat /etc/passwd') 137 | 138 | self.assertIsInstance(res, list) 139 | self.assertEqual(commands, ['nice -n 10 cat /etc/passwd']) 140 | 141 | def test_run_command_new_credential(self): 142 | ospd_ssh.paramiko = fakeparamiko 143 | 144 | daemon = DummyWrapper(niceness=10) 145 | 146 | cred_dict = { 147 | 'ssh': { 148 | 'type': 'up', 149 | 'password': 'mypass', 150 | 'port': '22', 151 | 'username': 'scanuser', 152 | }, 153 | 'smb': {'type': 'up', 'password': 'mypass', 'username': 'smbuser'}, 154 | } 155 | 156 | scanid = daemon.create_scan( 157 | None, 158 | { 159 | 'target': 'host.example.com', 160 | 'ports': '80, 443', 161 | 'credentials': cred_dict, 162 | 'exclude_hosts': '', 163 | 'finished_hosts': '', 164 | 'options': {}, 165 | }, 166 | dict(port=5, ssh_timeout=15), 167 | '', 168 | ) 169 | daemon.start_queued_scans() 170 | res = daemon.run_command(scanid, 'host.example.com', 'cat /etc/passwd') 171 | 172 | self.assertIsInstance(res, list) 173 | self.assertEqual(commands, ['nice -n 10 cat /etc/passwd']) 174 | 175 | def test_run_command_no_credential(self): 176 | ospd_ssh.paramiko = fakeparamiko 177 | 178 | daemon = DummyWrapper(niceness=10) 179 | scanid = daemon.create_scan( 180 | None, 181 | { 182 | 'target': 'host.example.com', 183 | 'ports': '80, 443', 184 | 'credentials': {}, 185 | 'exclude_hosts': '', 186 | 'finished_hosts': '', 187 | 'options': {}, 188 | }, 189 | dict(port=5, ssh_timeout=15), 190 | '', 191 | ) 192 | daemon.start_queued_scans() 193 | 194 | with self.assertRaises(ValueError): 195 | daemon.run_command(scanid, 'host.example.com', 'cat /etc/passwd') 196 | -------------------------------------------------------------------------------- /tests/test_target_convert.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | """ Test suites for Target manipulations. 19 | """ 20 | 21 | import unittest 22 | from unittest.mock import patch 23 | 24 | from ospd.network import ( 25 | target_str_to_list, 26 | get_hostname_by_address, 27 | is_valid_address, 28 | target_to_ipv4, 29 | socket, 30 | ) 31 | 32 | 33 | class ConvertTargetListsTestCase(unittest.TestCase): 34 | def test_24_net(self): 35 | addresses = target_str_to_list('195.70.81.0/24') 36 | 37 | self.assertIsNotNone(addresses) 38 | self.assertEqual(len(addresses), 254) 39 | 40 | for i in range(1, 255): 41 | self.assertIn(f'195.70.81.{str(i)}', addresses) 42 | 43 | def test_bad_ipv4_cidr(self): 44 | addresses = target_str_to_list('195.70.81.0/32') 45 | self.assertIsNotNone(addresses) 46 | self.assertEqual(len(addresses), 0) 47 | 48 | addresses = target_str_to_list('195.70.81.0/31') 49 | self.assertIsNotNone(addresses) 50 | self.assertEqual(len(addresses), 0) 51 | 52 | def test_good_ipv4_cidr(self): 53 | addresses = target_str_to_list('195.70.81.0/30') 54 | self.assertIsNotNone(addresses) 55 | self.assertEqual(len(addresses), 2) 56 | 57 | def test_range(self): 58 | addresses = target_str_to_list('195.70.81.0-10') 59 | 60 | self.assertIsNotNone(addresses) 61 | self.assertEqual(len(addresses), 11) 62 | 63 | for i in range(0, 10): 64 | self.assertIn(f'195.70.81.{str(i)}', addresses) 65 | 66 | def test_target_str_with_trailing_comma(self): 67 | addresses = target_str_to_list(',195.70.81.1,195.70.81.2,') 68 | 69 | self.assertIsNotNone(addresses) 70 | self.assertEqual(len(addresses), 2) 71 | 72 | for i in range(1, 2): 73 | self.assertIn(f'195.70.81.{str(i)}', addresses) 74 | 75 | def test_get_hostname_by_address(self): 76 | with patch.object(socket, "getfqdn", return_value="localhost"): 77 | hostname = get_hostname_by_address('127.0.0.1') 78 | self.assertEqual(hostname, 'localhost') 79 | 80 | hostname = get_hostname_by_address('') 81 | self.assertEqual(hostname, '') 82 | 83 | hostname = get_hostname_by_address('127.0.0.1111') 84 | self.assertEqual(hostname, '') 85 | 86 | def test_is_valid_address(self): 87 | self.assertFalse(is_valid_address(None)) 88 | self.assertFalse(is_valid_address('')) 89 | self.assertFalse(is_valid_address('foo')) 90 | self.assertFalse(is_valid_address('127.0.0.1111')) 91 | self.assertFalse(is_valid_address('127.0.0,1')) 92 | 93 | self.assertTrue(is_valid_address('127.0.0.1')) 94 | self.assertTrue(is_valid_address('192.168.0.1')) 95 | self.assertTrue(is_valid_address('::1')) 96 | self.assertTrue(is_valid_address('fc00::')) 97 | self.assertTrue(is_valid_address('fec0::')) 98 | self.assertTrue( 99 | is_valid_address('2001:0db8:85a3:08d3:1319:8a2e:0370:7344') 100 | ) 101 | 102 | def test_target_to_ipv4(self): 103 | self.assertIsNone(target_to_ipv4('foo')) 104 | self.assertIsNone(target_to_ipv4('')) 105 | self.assertIsNone(target_to_ipv4('127,0,0,1')) 106 | self.assertIsNone(target_to_ipv4('127.0.0')) 107 | self.assertIsNone(target_to_ipv4('127.0.0.11111')) 108 | 109 | self.assertEqual(target_to_ipv4('127.0.0.1'), ['127.0.0.1']) 110 | self.assertEqual(target_to_ipv4('192.168.1.1'), ['192.168.1.1']) 111 | -------------------------------------------------------------------------------- /tests/test_vts.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | import logging 19 | 20 | from hashlib import sha256 21 | from unittest import TestCase 22 | from unittest.mock import Mock 23 | 24 | from collections import OrderedDict 25 | from ospd.errors import OspdError 26 | from ospd.vts import Vts 27 | 28 | 29 | class VtsTestCase(TestCase): 30 | def test_add_vt(self): 31 | vts = Vts() 32 | 33 | vts.add('id_1', name='foo') 34 | 35 | self.assertEqual(len(vts.vts), 1) 36 | 37 | def test_add_duplicate_vt(self): 38 | vts = Vts() 39 | 40 | vts.add('id_1', name='foo') 41 | 42 | with self.assertRaises(OspdError): 43 | vts.add('id_1', name='bar') 44 | 45 | self.assertEqual(len(vts.vts), 1) 46 | 47 | def test_add_vt_with_empty_id(self): 48 | vts = Vts() 49 | 50 | with self.assertRaises(OspdError): 51 | vts.add(None, name='foo') 52 | 53 | with self.assertRaises(OspdError): 54 | vts.add('', name='foo') 55 | 56 | def test_add_vt_with_invalid_id(self): 57 | vts = Vts() 58 | 59 | with self.assertRaises(OspdError): 60 | vts.add('$$$_1', name='foo') 61 | 62 | self.assertEqual(len(vts.vts), 0) 63 | 64 | def test_contains(self): 65 | vts = Vts() 66 | 67 | vts.add('id_1', name='foo') 68 | 69 | self.assertIn('id_1', vts) 70 | 71 | def test_get(self): 72 | vts = Vts() 73 | 74 | vts.add('id_1', name='foo') 75 | vt = vts.get('id_1') 76 | 77 | self.assertIsNotNone(vt) 78 | self.assertEqual(vt['name'], 'foo') 79 | 80 | self.assertIsNone(vt.get('bar')) 81 | 82 | def test_iterator(self): 83 | vts = Vts() 84 | 85 | vts.add('id_1', name='foo') 86 | vts.add('id_2', name='bar') 87 | 88 | it = iter(vts) 89 | 90 | vt_id = next(it) 91 | self.assertIn(vt_id, ['id_1', 'id_2']) 92 | 93 | vt_id = next(it) 94 | self.assertIn(vt_id, ['id_1', 'id_2']) 95 | 96 | with self.assertRaises(StopIteration): 97 | next(it) 98 | 99 | def test_keys(self): 100 | vts = Vts() 101 | 102 | vts.add('id_1', name='foo') 103 | vts.add('id_2', name='bar') 104 | 105 | self.assertEqual(vts.keys(), ['id_1', 'id_2']) 106 | 107 | def test_getitem(self): 108 | vts = Vts() 109 | 110 | vts.add('id_1', name='foo') 111 | 112 | vt = vts['id_1'] 113 | 114 | self.assertEqual(vt['name'], 'foo') 115 | 116 | with self.assertRaises(KeyError): 117 | vt = vts['foo'] 118 | 119 | def test_copy(self): 120 | vts = Vts() 121 | 122 | vts.add('id_1', name='foo') 123 | vts.add('id_2', name='bar') 124 | 125 | vts2 = vts.copy() 126 | 127 | self.assertIsNot(vts, vts2) 128 | self.assertIsNot(vts.vts, vts2.vts) 129 | 130 | vta = vts.get('id_1') 131 | vtb = vts2.get('id_1') 132 | self.assertEqual(vta['name'], vtb['name']) 133 | self.assertIsNot(vta, vtb) 134 | 135 | vta = vts.get('id_2') 136 | vtb = vts2.get('id_2') 137 | self.assertEqual(vta['name'], vtb['name']) 138 | self.assertIsNot(vta, vtb) 139 | 140 | def test_calculate_vts_collection_hash(self): 141 | vts = Vts(storage=OrderedDict) 142 | 143 | vts.add( 144 | 'id_1', 145 | name='foo', 146 | vt_modification_time='01234', 147 | vt_params={ 148 | '0': { 149 | 'id': '0', 150 | 'name': 'timeout', 151 | 'default': '20', 152 | }, 153 | '1': { 154 | 'id': '1', 155 | 'name': 'foo_pref:', 156 | 'default': 'bar_value', 157 | }, 158 | }, 159 | ) 160 | vts.add('id_2', name='bar', vt_modification_time='56789') 161 | 162 | vts.calculate_vts_collection_hash() 163 | 164 | vt_hash = sha256() 165 | vt_hash.update( 166 | "id_1012340timeout201foo_pref:bar_valueid_256789".encode('utf-8') 167 | ) 168 | hash_test = vt_hash.hexdigest() 169 | 170 | self.assertEqual(hash_test, vts.sha256_hash) 171 | 172 | def test_calculate_vts_collection_hash_no_params(self): 173 | vts = Vts(storage=OrderedDict) 174 | 175 | vts.add( 176 | 'id_1', 177 | name='foo', 178 | vt_modification_time='01234', 179 | vt_params={ 180 | '0': { 181 | 'id': '0', 182 | 'name': 'timeout', 183 | 'default': '20', 184 | }, 185 | '1': { 186 | 'id': '1', 187 | 'name': 'foo_pref:', 188 | 'default': 'bar_value', 189 | }, 190 | }, 191 | ) 192 | vts.add('id_2', name='bar', vt_modification_time='56789') 193 | 194 | vts.calculate_vts_collection_hash(include_vt_params=False) 195 | 196 | vt_hash = sha256() 197 | vt_hash.update("id_101234id_256789".encode('utf-8')) 198 | hash_test = vt_hash.hexdigest() 199 | 200 | self.assertEqual(hash_test, vts.sha256_hash) 201 | 202 | def test_calculate_vts_collection_hash_empty(self): 203 | vts = Vts() 204 | logging.Logger.debug = Mock() 205 | 206 | vts.calculate_vts_collection_hash() 207 | 208 | self.assertEqual(vts.sha256_hash, None) 209 | logging.Logger.debug.assert_called_with( # pylint: disable=no-member 210 | "Error calculating VTs collection hash. Cache is empty" 211 | ) 212 | -------------------------------------------------------------------------------- /tests/test_xml.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | from collections import OrderedDict 19 | 20 | from unittest import TestCase 21 | 22 | from xml.etree.ElementTree import Element, tostring, fromstring 23 | 24 | from ospd.xml import elements_as_text, escape_ctrl_chars 25 | 26 | 27 | class ElementsAsText(TestCase): 28 | def test_simple_element(self): 29 | elements = {'foo': 'bar'} 30 | text = elements_as_text(elements) 31 | 32 | self.assertEqual(text, '\t foo bar\n') 33 | 34 | def test_simple_elements(self): 35 | elements = OrderedDict([('foo', 'bar'), ('lorem', 'ipsum')]) 36 | text = elements_as_text(elements) 37 | 38 | self.assertEqual( 39 | text, 40 | '\t foo bar\n' 41 | '\t lorem ipsum\n', 42 | ) 43 | 44 | def test_elements(self): 45 | elements = OrderedDict( 46 | [ 47 | ('foo', 'bar'), 48 | ( 49 | 'lorem', 50 | OrderedDict( 51 | [ 52 | ('dolor', 'sit amet'), 53 | ('consectetur', 'adipiscing elit'), 54 | ] 55 | ), 56 | ), 57 | ] 58 | ) 59 | text = elements_as_text(elements) 60 | 61 | self.assertEqual( 62 | text, 63 | '\t foo bar\n' 64 | '\t lorem \n' 65 | '\t dolor sit amet\n' 66 | '\t consectetur adipiscing elit\n', 67 | ) 68 | 69 | 70 | class EscapeText(TestCase): 71 | def test_escape_xml_valid_text(self): 72 | text = 'this is a valid xml' 73 | res = escape_ctrl_chars(text) 74 | 75 | self.assertEqual(text, res) 76 | 77 | def test_escape_xml_invalid_char(self): 78 | text = 'End of transmission is not printable \x04.' 79 | res = escape_ctrl_chars(text) 80 | self.assertEqual(res, 'End of transmission is not printable \\x0004.') 81 | 82 | # Create element 83 | elem = Element('text') 84 | elem.text = res 85 | self.assertEqual( 86 | tostring(elem), 87 | b'End of transmission is not printable \\x0004.', 88 | ) 89 | 90 | # The string format of the element does not break the xml. 91 | elem_as_str = tostring(elem, encoding='utf-8') 92 | new_elem = fromstring(elem_as_str) 93 | self.assertEqual( 94 | b'' + new_elem.text.encode('utf-8') + b'', elem_as_str 95 | ) 96 | 97 | def test_escape_xml_printable_char(self): 98 | text = 'Latin Capital Letter A With Circumflex \xc2 is printable.' 99 | res = escape_ctrl_chars(text) 100 | self.assertEqual( 101 | res, 'Latin Capital Letter A With Circumflex  is printable.' 102 | ) 103 | 104 | # Create the element 105 | elem = Element('text') 106 | elem.text = res 107 | self.assertEqual( 108 | tostring(elem), 109 | b'Latin Capital Letter A With Circumflex  is ' 110 | b'printable.', 111 | ) 112 | 113 | # The string format of the element does not break the xml 114 | elem_as_str = tostring(elem, encoding='utf-8') 115 | new_elem = fromstring(elem_as_str) 116 | self.assertEqual( 117 | b'' + new_elem.text.encode('utf-8') + b'', elem_as_str 118 | ) 119 | --------------------------------------------------------------------------------