├── .fmf └── version ├── .github ├── dependabot.yml └── workflows │ ├── coverity.yml │ ├── differential-pylint.yml │ └── differential-shellcheck.yml ├── .gitignore ├── .packit.yaml ├── .pylintrc ├── CMakeLists.txt ├── COPYING ├── Makefile ├── README ├── csmock ├── CMakeLists.txt ├── __init__.py ├── common │ ├── .gitignore │ ├── __init__.py │ ├── cflags.py │ ├── results.py │ ├── snyk.py │ └── util.py ├── csbuild ├── csmock └── plugins │ ├── __init__.py │ ├── bandit.py │ ├── cbmc.py │ ├── clang.py │ ├── clippy.py │ ├── cppcheck.py │ ├── divine.py │ ├── gcc.py │ ├── gitleaks.py │ ├── infer.py │ ├── pylint.py │ ├── semgrep.py │ ├── shellcheck.py │ ├── smatch.py │ ├── snyk.py │ ├── strace.py │ ├── symbiotic.py │ ├── unicontrol.py │ └── valgrind.py ├── cwe-map.csv ├── doc ├── CMakeLists.txt ├── csbuild.h2m └── csmock.h2m ├── make-srpm.sh ├── plans └── ci.fmf ├── scripts ├── CMakeLists.txt ├── chroot-fixups │ ├── 00-pre-usr-move-shells.sh │ ├── gdk-pixbuf2-triggers.sh │ ├── glib2-triggers.sh │ ├── kpathsea-texhash.sh │ ├── openssl-public-header-files.sh │ ├── qt5-core-abi.sh │ ├── rpm-build-scripts.sh │ ├── rpm-macros.sh │ ├── rpm-python-extras.sh │ ├── shared-mime-info-triggers.sh │ └── symbiotic-timeout.sh ├── convert-clippy.py ├── enable-keep-going.sh ├── filter-infer.py ├── find-unicode-control.py ├── inject-clippy.sh ├── install-infer.sh ├── patch-rawbuild.sh ├── run-bandit.sh ├── run-pylint.sh ├── run-scan.sh └── run-shellcheck.sh ├── tests └── simple_build │ ├── clang.fmf │ ├── gcc.fmf │ ├── shellcheck.fmf │ └── test.sh └── upload-release.sh /.fmf/version: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: github-actions 6 | directory: / 7 | schedule: 8 | interval: weekly 9 | reviewers: 10 | - lzaoral 11 | -------------------------------------------------------------------------------- /.github/workflows/coverity.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Coverity 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | coverity: 10 | if: github.repository == 'csutils/csmock' 11 | name: Coverity Scan 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Coverity scan 19 | uses: vapier/coverity-scan-action@v1 20 | with: 21 | build_language: other # other is for Python, etc... 22 | email: ${{ secrets.COVERITY_SCAN_EMAIL }} 23 | token: ${{ secrets.COVERITY_SCAN_TOKEN }} 24 | project: csutils/csmock 25 | command: --no-command --fs-capture-search ./csmock ./scripts 26 | -------------------------------------------------------------------------------- /.github/workflows/differential-pylint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Differential PyLint 4 | 5 | on: 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | security-events: write 18 | 19 | steps: 20 | - name: Repository checkout 21 | uses: actions/checkout@v4 22 | 23 | - id: PyLint 24 | name: Differential PyLint 25 | uses: fedora-copr/vcs-diff-lint-action@v1 26 | 27 | - if: ${{ always() }} 28 | name: Upload artifact with detected PyLint defects in SARIF format 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: Differential PyLint SARIF 32 | path: ${{ steps.PyLint.outputs.sarif }} 33 | 34 | - if: ${{ failure() }} 35 | name: Upload SARIF to GitHub using github/codeql-action/upload-sarif 36 | uses: github/codeql-action/upload-sarif@v3 37 | with: 38 | sarif_file: ${{ steps.PyLint.outputs.sarif }} 39 | 40 | ... 41 | -------------------------------------------------------------------------------- /.github/workflows/differential-shellcheck.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Differential ShellCheck 4 | on: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | 17 | permissions: 18 | security-events: write 19 | pull-requests: write 20 | 21 | steps: 22 | - name: Repository checkout 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | 27 | - id: ShellCheck 28 | name: Differential ShellCheck 29 | uses: redhat-plumbers-in-action/differential-shellcheck@v5 30 | with: 31 | display-engine: sarif-fmt 32 | token: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - if: ${{ runner.debug == '1' && !cancelled() }} 35 | name: Upload artifact with ShellCheck defects in SARIF format 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: Differential ShellCheck SARIF 39 | path: ${{ steps.ShellCheck.outputs.sarif }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /csmock_build/ 2 | /csmock-*.src.rpm 3 | -------------------------------------------------------------------------------- /.packit.yaml: -------------------------------------------------------------------------------- 1 | # See the documentation for more information: 2 | # https://packit.dev/docs/configuration/ 3 | 4 | specfile_path: csmock.spec 5 | 6 | # add or remove files that should be synced 7 | files_to_sync: 8 | - csmock.spec 9 | - .packit.yaml 10 | 11 | # name in upstream package repository or registry (e.g. in PyPI) 12 | upstream_package_name: csmock 13 | # downstream (Fedora) RPM package name 14 | downstream_package_name: csmock 15 | 16 | srpm_build_deps: [rpm-build] 17 | 18 | update_release: false 19 | actions: 20 | post-upstream-clone: ./make-srpm.sh --generate-spec 21 | get-current-version: "sed -n 's|^Version: *||p' csmock.spec" 22 | 23 | jobs: 24 | - &copr 25 | job: copr_build 26 | trigger: pull_request 27 | targets: 28 | - epel-all-aarch64 29 | - epel-all-ppc64le 30 | - epel-all-s390x 31 | - epel-all-x86_64 32 | - fedora-all-aarch64 33 | - fedora-all-ppc64le 34 | - fedora-all-s390x 35 | - fedora-all-x86_64 36 | 37 | # EPEL tests 38 | - job: tests 39 | trigger: pull_request 40 | targets: 41 | - epel-8-aarch64 42 | - epel-8-x86_64 43 | identifier: epel8 44 | tf_extra_params: 45 | environments: 46 | - artifacts: 47 | - type: repository-file 48 | id: https://copr.fedorainfracloud.org/coprs/g/codescan/csutils/repo/epel-8/group_codescan-csutils-epel-8 49 | - job: tests 50 | trigger: pull_request 51 | targets: 52 | - epel-9-aarch64 53 | - epel-9-x86_64 54 | identifier: epel9 55 | tf_extra_params: 56 | environments: 57 | - artifacts: 58 | - type: repository-file 59 | id: https://copr.fedorainfracloud.org/coprs/g/codescan/csutils/repo/epel-9/group_codescan-csutils-epel-9 60 | 61 | # Fedora tests 62 | - job: tests 63 | trigger: pull_request 64 | targets: 65 | - fedora-all-aarch64 66 | - fedora-all-x86_64 67 | identifier: fedora 68 | tf_extra_params: 69 | environments: 70 | - artifacts: 71 | - type: repository-file 72 | id: https://copr.fedorainfracloud.org/coprs/g/codescan/csutils/repo/fedora/group_codescan-csutils-fedora 73 | 74 | - <<: *copr 75 | trigger: commit 76 | owner: "@codescan" 77 | project: "csutils" 78 | branch: main 79 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [FORMAT] 2 | 3 | # increase threshold to eliminate C0301 warnings about lines exceeding 100 chars 4 | max-line-length=130 5 | 6 | 7 | [MESSAGES CONTROL] 8 | 9 | # suppress pointless warnings about duplicated code across csmock plug-ins 10 | disable=duplicate-code 11 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2022 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | cmake_minimum_required(VERSION 3.12) 19 | project(csmock NONE) 20 | 21 | set(PERM_EXECUTABLE 22 | OWNER_READ OWNER_WRITE OWNER_EXECUTE 23 | GROUP_READ GROUP_EXECUTE 24 | WORLD_READ WORLD_EXECUTE) 25 | 26 | if(NOT DEFINED SHARE_INSTALL_PREFIX) 27 | set(SHARE_INSTALL_PREFIX /usr/share) 28 | endif() 29 | 30 | option(ENABLE_CSBUILD "if enabled, install the csbuild tool" ON) 31 | option(ENABLE_CSMOCK "if enabled, install the csmock tool" ON) 32 | 33 | if(ENABLE_CSMOCK) 34 | install(FILES 35 | ${CMAKE_CURRENT_SOURCE_DIR}/cwe-map.csv 36 | DESTINATION ${SHARE_INSTALL_PREFIX}/csmock) 37 | endif() 38 | 39 | add_subdirectory(csmock) 40 | add_subdirectory(doc) 41 | add_subdirectory(scripts) 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | CMAKE ?= cmake 19 | CTEST ?= ctest 20 | 21 | .PHONY: all check clean distclean distcheck install 22 | 23 | all: 24 | mkdir -p csmock_build 25 | cd csmock_build && $(CMAKE) .. 26 | $(MAKE) -C csmock_build 27 | 28 | check: all 29 | cd csmock_build && $(CTEST) --output-on-failure 30 | 31 | clean: 32 | if test -e csmock_build/Makefile; then $(MAKE) clean -C csmock_build; fi 33 | 34 | distclean: 35 | rm -rf csmock_build 36 | 37 | distcheck: distclean 38 | $(MAKE) check 39 | 40 | install: all 41 | $(MAKE) -C csmock_build install 42 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | csmock 2 | ====== 3 | csmock is a tool for scanning SRPMs by Static Analysis tools in a fully 4 | automated way. You can find the up2date sources in the following repository: 5 | 6 | https://github.com/csutils/csmock 7 | 8 | csmock is licensed under GPLv3+, see COPYING for details. Please report bugs 9 | and feature requests on GitHub using the above URL. 10 | 11 | 12 | Dependences 13 | ----------- 14 | * csdiff (non-interactive tools for processing scan results in plain-text) 15 | 16 | * cswrap (generic compiler wrapper that captures diagnostic messages) 17 | 18 | * mock (chroot-based tool for building RPMs) 19 | 20 | 21 | RPM-based Installation 22 | ---------------------- 23 | ./make-srpm.sh 24 | 25 | rpmbuild --rebuild ./csmock-*.src.rpm 26 | 27 | sudo dnf install ... 28 | 29 | 30 | Documentation 31 | ------------- 32 | See the 'csmock(1)' man page for the usage of csmock itself. The output 33 | of 'csmock --help' additionally includes the command-line options handled 34 | by csmock plug-ins. A higher-level description can be found in the Flock 35 | 2014 presentation about csmock: 36 | 37 | https://kdudka.fedorapeople.org/static-analysis-flock2014.pdf 38 | -------------------------------------------------------------------------------- /csmock/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2022 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | find_package(Python3 REQUIRED) 19 | 20 | set(PLUGIN_DIR "${Python3_SITELIB}/csmock/plugins") 21 | message(STATUS "PLUGIN_DIR: ${PLUGIN_DIR}") 22 | 23 | # install common python modules to the csmock/common subdirectory 24 | set(src_dir "${CMAKE_CURRENT_SOURCE_DIR}") 25 | set(dst_dir "${Python3_SITELIB}/csmock") 26 | install(FILES ${src_dir}/__init__.py DESTINATION ${dst_dir}) 27 | install(FILES ${src_dir}/common/__init__.py DESTINATION ${dst_dir}/common) 28 | install(FILES ${src_dir}/common/cflags.py DESTINATION ${dst_dir}/common) 29 | install(FILES ${src_dir}/common/results.py DESTINATION ${dst_dir}/common) 30 | install(FILES ${src_dir}/common/snyk.py DESTINATION ${dst_dir}/common) 31 | install(FILES ${src_dir}/common/util.py DESTINATION ${dst_dir}/common) 32 | 33 | macro(install_executable FILE_NAME) 34 | configure_file( 35 | ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME} 36 | ${CMAKE_CURRENT_BINARY_DIR}/${FILE_NAME} 37 | @ONLY) 38 | 39 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${FILE_NAME} 40 | DESTINATION bin 41 | PERMISSIONS ${PERM_EXECUTABLE}) 42 | endmacro() 43 | 44 | if(ENABLE_CSBUILD) 45 | install_executable(csbuild) 46 | endif() 47 | 48 | if(ENABLE_CSMOCK) 49 | install_executable(csmock) 50 | install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/plugins/__init__.py 51 | DESTINATION ${PLUGIN_DIR}) 52 | 53 | macro(install_plugin PLUGIN_NAME) 54 | install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/plugins/${PLUGIN_NAME}.py 55 | DESTINATION ${PLUGIN_DIR}) 56 | endmacro() 57 | 58 | install_plugin(bandit) 59 | install_plugin(cbmc) 60 | install_plugin(clang) 61 | install_plugin(clippy) 62 | install_plugin(cppcheck) 63 | install_plugin(divine) 64 | install_plugin(gcc) 65 | install_plugin(gitleaks) 66 | install_plugin(infer) 67 | install_plugin(pylint) 68 | install_plugin(semgrep) 69 | install_plugin(shellcheck) 70 | install_plugin(smatch) 71 | install_plugin(snyk) 72 | install_plugin(strace) 73 | install_plugin(symbiotic) 74 | install_plugin(valgrind) 75 | install_plugin(unicontrol) 76 | endif() 77 | -------------------------------------------------------------------------------- /csmock/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csutils/csmock/a728c5bfbf9e68a89f2f7ebba78b7e5dc0abdeaa/csmock/__init__.py -------------------------------------------------------------------------------- /csmock/common/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /csmock/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csutils/csmock/a728c5bfbf9e68a89f2f7ebba78b7e5dc0abdeaa/csmock/common/__init__.py -------------------------------------------------------------------------------- /csmock/common/cflags.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | DEL_FLAGS_BY_LEVEL_COMMON = { 19 | 0: ["-Werror*", "-fdiagnostics-color*", "-no-canonical-prefixes", 20 | "-Wno-error=deprecated-register"]} 21 | 22 | ADD_FLAGS_BY_LEVEL_COMMON = { 23 | 1: ["-Wall", "-Wextra"], 24 | 2: ["-Wunreachable-code", "-Wundef", "-Wcast-align", 25 | "-Wpointer-arith", "-Wfloat-equal", "-Wshadow", 26 | "-Wwrite-strings", "-Wformat=2"]} 27 | 28 | ADD_FLAGS_BY_LEVEL_C_ONLY = { 29 | 0: ["-Wno-unknown-pragmas"], 30 | 2: ["-Wstrict-prototypes"]} 31 | 32 | ADD_FLAGS_BY_LEVEL_CXX_ONLY = { 33 | 2: ["-Wctor-dtor-privacy", "-Woverloaded-virtual"]} 34 | 35 | 36 | def add_custom_flag_opts(parser): 37 | parser.add_argument( 38 | "--gcc-add-flag", action="append", default=[], 39 | help="append the given compiler flag when invoking gcc \ 40 | (can be used multiple times)") 41 | 42 | parser.add_argument( 43 | "--gcc-add-c-only-flag", action="append", default=[], 44 | help="append the given compiler flag when invoking gcc for C \ 45 | (can be used multiple times)") 46 | 47 | parser.add_argument( 48 | "--gcc-add-cxx-only-flag", action="append", default=[], 49 | help="append the given compiler flag when invoking gcc for C++ \ 50 | (can be used multiple times)") 51 | 52 | parser.add_argument( 53 | "--gcc-del-flag", action="append", default=[], 54 | help="drop the given compiler flag when invoking gcc \ 55 | (can be used multiple times)") 56 | 57 | 58 | def encode_custom_flag_opts(args): 59 | cmd = "" 60 | for flag in args.gcc_add_flag: 61 | cmd += " --gcc-add-flag='%s'" % flag 62 | for flag in args.gcc_add_c_only_flag: 63 | cmd += " --gcc-add-c-only-flag='%s'" % flag 64 | for flag in args.gcc_add_cxx_only_flag: 65 | cmd += " --gcc-add-cxx-only-flag='%s'" % flag 66 | for flag in args.gcc_del_flag: 67 | cmd += " --gcc-del-flag='%s'" % flag 68 | return cmd 69 | 70 | 71 | def serialize_flags(flags, separator=":"): 72 | out = "" 73 | for f in flags: 74 | if out: 75 | out += separator 76 | out += f 77 | return out 78 | 79 | 80 | class FlagsMatrix: 81 | def __init__(self): 82 | self.add_cflags = [] 83 | self.del_cflags = [] 84 | self.add_cxxflags = [] 85 | self.del_cxxflags = [] 86 | 87 | def append_flags(self, flags): 88 | self.add_cflags += flags 89 | self.add_cxxflags += flags 90 | 91 | def remove_flags(self, flags): 92 | self.del_cflags += flags 93 | self.del_cxxflags += flags 94 | 95 | def append_custom_flags(self, args): 96 | self.add_cflags += args.gcc_add_flag 97 | self.add_cflags += args.gcc_add_c_only_flag 98 | 99 | self.add_cxxflags += args.gcc_add_flag 100 | self.add_cxxflags += args.gcc_add_cxx_only_flag 101 | 102 | self.del_cflags += args.gcc_del_flag 103 | self.del_cxxflags += args.gcc_del_flag 104 | 105 | return (0 < len(args.gcc_add_flag)) or \ 106 | (0 < len(args.gcc_add_c_only_flag)) or \ 107 | (0 < len(args.gcc_add_cxx_only_flag)) or \ 108 | (0 < len(args.gcc_del_flag)) 109 | 110 | def write_to_env(self, env): 111 | def append_to_env(key, arr): 112 | env[key] = (env[key] + ":" if key in env else "") + serialize_flags(arr) 113 | 114 | append_to_env("CSWRAP_ADD_CFLAGS", self.add_cflags) 115 | append_to_env("CSWRAP_DEL_CFLAGS", self.del_cflags) 116 | append_to_env("CSWRAP_ADD_CXXFLAGS", self.add_cxxflags) 117 | append_to_env("CSWRAP_DEL_CXXFLAGS", self.del_cxxflags) 118 | 119 | 120 | def flags_by_warning_level(level): 121 | flags = FlagsMatrix() 122 | for l in range(0, level + 1): 123 | if l in DEL_FLAGS_BY_LEVEL_COMMON: 124 | flags.del_cflags += DEL_FLAGS_BY_LEVEL_COMMON[l] 125 | flags.del_cxxflags += DEL_FLAGS_BY_LEVEL_COMMON[l] 126 | 127 | if l in ADD_FLAGS_BY_LEVEL_COMMON: 128 | flags.add_cflags += ADD_FLAGS_BY_LEVEL_COMMON[l] 129 | flags.add_cxxflags += ADD_FLAGS_BY_LEVEL_COMMON[l] 130 | 131 | if l in ADD_FLAGS_BY_LEVEL_C_ONLY: 132 | flags.add_cflags += ADD_FLAGS_BY_LEVEL_C_ONLY[l] 133 | 134 | if l in ADD_FLAGS_BY_LEVEL_CXX_ONLY: 135 | flags.add_cxxflags += ADD_FLAGS_BY_LEVEL_CXX_ONLY[l] 136 | return flags 137 | -------------------------------------------------------------------------------- /csmock/common/results.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | # standard imports 19 | import codecs 20 | import datetime 21 | import errno 22 | import os 23 | import re 24 | import shlex 25 | import shutil 26 | import signal 27 | import socket 28 | import stat 29 | import subprocess 30 | import sys 31 | import tempfile 32 | 33 | # local imports 34 | from csmock.common.util import strlist_to_shell_cmd 35 | 36 | CSGREP_FINAL_FILTER_ARGS = "--invert-match --event \"internal warning\" \ 37 | --prune-events=1" 38 | 39 | def current_iso_date(): 40 | now = datetime.datetime.now() 41 | return "%04u-%02u-%02u %02u:%02u:%02u" % \ 42 | (now.year, now.month, now.day, now.hour, now.minute, now.second) 43 | 44 | 45 | class FatalError(Exception): 46 | def __init__(self, ec): 47 | self.ec = ec 48 | 49 | 50 | class ScanResults: 51 | def __init__(self, output, tool, tool_version, keep_going=False, create_dbgdir=True, 52 | no_clean=False): 53 | self.output = output 54 | self.tool = tool 55 | self.tool_version = tool_version 56 | self.keep_going = keep_going 57 | self.create_dbgdir = create_dbgdir 58 | self.no_clean = no_clean 59 | self.use_xz = False 60 | self.use_tar = False 61 | self.dirname = os.path.basename(output) 62 | self.codec = codecs.lookup('utf8') 63 | self.ec = 0 64 | self.dying = False 65 | 66 | # just to silence pylint, will be initialized in __enter__() 67 | self.tmpdir = None 68 | self.resdir = None 69 | self.dbgdir = None 70 | self.dbgdir_raw = None 71 | self.dbgdir_uni = None 72 | self.log_pid = None 73 | self.log_fd = None 74 | self.ini_writer = None 75 | self.subproc = None 76 | 77 | m = re.match("^(.*)\\.xz$", self.dirname) 78 | if m is not None: 79 | self.use_xz = True 80 | self.dirname = m.group(1) 81 | 82 | m = re.match("^(.*)\\.tar$", self.dirname) 83 | if m is not None: 84 | self.use_tar = True 85 | self.dirname = m.group(1) 86 | 87 | def utf8_wrap(self, fd): 88 | # the following hack is needed to support both Python 2 and 3 89 | return codecs.StreamReaderWriter( 90 | fd, self.codec.streamreader, self.codec.streamwriter) 91 | 92 | def __enter__(self): 93 | self.tmpdir = tempfile.mkdtemp(prefix=self.tool) 94 | if self.use_tar: 95 | self.resdir = "%s/%s" % (self.tmpdir, self.dirname) 96 | else: 97 | if os.path.exists(self.output): 98 | shutil.rmtree(self.output) 99 | self.resdir = self.output 100 | 101 | try: 102 | os.mkdir(self.resdir) 103 | except OSError as e: 104 | sys.stderr.write( 105 | "error: failed to create output directory: %s\n" % e) 106 | raise FatalError(1) 107 | 108 | if self.create_dbgdir: 109 | self.dbgdir = "%s/debug" % self.resdir 110 | self.dbgdir_raw = "%s/raw-results" % self.dbgdir 111 | self.dbgdir_uni = "%s/uni-results" % self.dbgdir 112 | os.mkdir(self.dbgdir) 113 | os.mkdir(self.dbgdir_raw) 114 | os.mkdir(self.dbgdir_uni) 115 | os.mknod(os.path.join(self.dbgdir_uni, "empty.err"), stat.S_IFREG|0o444) 116 | 117 | tee = ["tee", "%s/scan.log" % self.resdir] 118 | self.log_pid = subprocess.Popen( 119 | tee, stdin=subprocess.PIPE, preexec_fn=os.setsid) 120 | self.log_fd = self.utf8_wrap(self.log_pid.stdin) 121 | 122 | def signal_handler(signum, frame): 123 | # avoid throwing FatalError out of a signal handler 124 | self.dying = True 125 | self.error("caught signal %d" % signum, 128 + signum) 126 | if self.subproc is not None: 127 | # forward the signal to the child process being executed 128 | try: 129 | os.kill(self.subproc.pid, signum) 130 | except Exception as e: 131 | self.error("failed to kill child process: %s" % e) 132 | # this will make the foreground process throw FatalError synchronously 133 | self.dying = False 134 | 135 | for i in [signal.SIGINT, signal.SIGPIPE, signal.SIGQUIT, signal.SIGTERM]: 136 | signal.signal(i, signal_handler) 137 | 138 | self.ini_writer = IniWriter(self) 139 | return self 140 | 141 | def __exit__(self, exc_type, exc_val, exc_tb): 142 | self.ini_writer.close() 143 | if self.no_clean: 144 | self.print_with_ts(f"temporary directory preserved: {self.tmpdir}") 145 | 146 | self.print_with_ts("%s exit code: %d\n" % (self.tool, self.ec), prefix="<<< ") 147 | self.log_fd.close() 148 | self.log_fd = sys.stderr 149 | self.log_pid.wait() 150 | if self.use_tar: 151 | tar_opts = "-c --remove-files" 152 | if self.use_xz: 153 | tar_opts += " -J" 154 | tar_cmd = "tar %s -f '%s' -C '%s' '%s'" % ( 155 | tar_opts, self.output, self.tmpdir, self.dirname) 156 | # do not treat 'tar: file changed as we read it' as fatal error 157 | if os.system(tar_cmd) > 1: 158 | self.fatal_error( 159 | "failed to write '%s', not removing '%s'..." % ( 160 | self.output, self.tmpdir)) 161 | 162 | sys.stderr.write("Wrote: %s\n\n" % self.output) 163 | if self.no_clean: 164 | return 165 | 166 | try: 167 | shutil.rmtree(self.tmpdir) 168 | except Exception: 169 | sys.stderr.write("%s: warning: failed to remove tmp dir: %s\n" \ 170 | % (self.tool, self.tmpdir)) 171 | 172 | 173 | def print_with_ts(self, msg, prefix=">>> "): 174 | self.log_fd.write("%s%s\t%s\n" % (prefix, current_iso_date(), msg)) 175 | self.log_fd.flush() 176 | # eventually handle terminating signals 177 | self.handle_ec() 178 | 179 | def update_ec(self, ec): 180 | if self.ec < ec: 181 | self.ec = ec 182 | 183 | def error(self, msg, ec=1, err_prefix="", fatal=False): 184 | if fatal: 185 | assert not err_prefix 186 | 187 | # cannot recurse, should never return 188 | self.fatal_error(msg, ec) 189 | assert False 190 | 191 | level = "warning" if ec == 0 else "error" 192 | self.print_with_ts(f"{err_prefix}{level}: {msg}\n", prefix="!!! ") 193 | self.update_ec(ec) 194 | if not self.dying and not self.keep_going and (self.ec != 0): 195 | raise FatalError(ec) 196 | 197 | def fatal_error(self, msg, ec=1): 198 | # avoid recursive handling of errors, handle synchronous shutdown 199 | self.dying = True 200 | self.error(msg, err_prefix="fatal ", ec=ec) 201 | raise FatalError(ec) 202 | 203 | def handle_ec(self): 204 | if not self.dying and (128 < self.ec < (128 + 64)): 205 | # caught terminating signal, handle synchronous shutdown 206 | self.fatal_error("caught signal %d" % (self.ec - 128), self.ec) 207 | 208 | def handle_rv(self, rv): 209 | if 128 < rv: 210 | # command terminated by signal, handle synchronous shutdown 211 | self.update_ec(rv) 212 | self.handle_ec() 213 | 214 | def exec_cmd(self, cmd, shell=False, echo=True): 215 | self.handle_ec() 216 | if echo: 217 | if shell: 218 | self.print_with_ts(cmd) 219 | else: 220 | self.print_with_ts(strlist_to_shell_cmd(cmd, escape_special=True)) 221 | try: 222 | self.subproc = subprocess.Popen( 223 | cmd, stdout=self.log_fd, stderr=self.log_fd, shell=shell) 224 | rv = self.subproc.wait() 225 | self.subproc = None 226 | self.log_fd.write("\n") 227 | except OSError as e: 228 | self.log_fd.write("%s\n" % str(e)) 229 | if e.errno == errno.ENOENT: 230 | # command not found 231 | return 0x7F 232 | else: 233 | # command not executable 234 | return 0x7E 235 | self.handle_rv(rv) 236 | return rv 237 | 238 | def get_cmd_output(self, cmd, shell=True): 239 | self.handle_ec() 240 | self.subproc = subprocess.Popen( 241 | cmd, stdout=subprocess.PIPE, stderr=self.log_fd, shell=shell) 242 | (out, _) = self.subproc.communicate() 243 | rv = self.subproc.returncode 244 | self.subproc = None 245 | self.handle_rv(rv) 246 | out = out.decode("utf8") 247 | return (rv, out) 248 | 249 | def open_res_file(self, rel_path): 250 | abs_path = "%s/%s" % (self.resdir, rel_path) 251 | return open(abs_path, "w") 252 | 253 | 254 | class IniWriter: 255 | def __init__(self, results): 256 | self.results = results 257 | self.ini = self.results.open_res_file("scan.ini") 258 | self.write("[scan]\n") 259 | self.append("tool", self.results.tool) 260 | self.append("tool-version", self.results.tool_version) 261 | self.append("tool-args", strlist_to_shell_cmd(sys.argv)) 262 | self.append("host", socket.gethostname()) 263 | self.append("store-results-to", self.results.output) 264 | self.append("time-created", current_iso_date()) 265 | 266 | def close(self): 267 | if self.ini is None: 268 | return 269 | self.append("time-finished", current_iso_date()) 270 | self.append("exit-code", self.results.ec) 271 | self.ini.close() 272 | self.ini = None 273 | 274 | def write(self, text): 275 | self.ini.write(text) 276 | self.results.log_fd.write("scan.ini: " + text) 277 | 278 | def append(self, key, value): 279 | val_str = str(value).strip() 280 | self.write("%s = %s\n" % (key, val_str)) 281 | 282 | 283 | def re_from_checker_set(checker_set): 284 | """return operand for the --checker option of csgrep based on checker_set""" 285 | chk_re = "^(" 286 | first = True 287 | for chk in sorted(checker_set): 288 | if first: 289 | first = False 290 | else: 291 | chk_re += "|" 292 | chk_re += chk 293 | chk_re += ")$" 294 | return chk_re 295 | 296 | 297 | def transform_results(js_file, results): 298 | err_file = re.sub("\\.js", ".err", js_file) 299 | html_file = re.sub("\\.js", ".html", js_file) 300 | stat_file = re.sub("\\.js", "-summary.txt", js_file) 301 | results.exec_cmd("csgrep --mode=grep %s '%s' > '%s'" % 302 | (CSGREP_FINAL_FILTER_ARGS, js_file, err_file), shell=True) 303 | results.exec_cmd("csgrep --mode=json %s '%s' | cshtml - > '%s'" % 304 | (CSGREP_FINAL_FILTER_ARGS, js_file, html_file), shell=True) 305 | results.exec_cmd("csgrep --mode=evtstat %s '%s' | tee '%s'" % \ 306 | (CSGREP_FINAL_FILTER_ARGS, js_file, stat_file), shell=True) 307 | return err_file, html_file 308 | 309 | 310 | def finalize_results(js_file, results, props): 311 | """transform scan-results.js to scan-results.{err,html} and write stats""" 312 | if props.imp_checker_set: 313 | # filter out "important" defects, first based on checkers only 314 | cmd = "csgrep '%s' --mode=json --checker '%s'" % \ 315 | (js_file, re_from_checker_set(props.imp_checker_set)) 316 | 317 | # then apply custom per-checker filters 318 | for (chk, csgrep_args) in props.imp_csgrep_filters: 319 | chk_re = re_from_checker_set(props.imp_checker_set - set([chk])) 320 | cmd += " | csdiff <(csgrep '%s' --mode=json --drop-scan-props --invert-regex --checker '%s' %s) -" \ 321 | % (js_file, chk_re, csgrep_args) 322 | 323 | # finally take all defects that were tagged important by the scanner already 324 | cmd += " | csgrep --mode=json --set-imp-level=1 --remove-duplicates" 325 | cmd += f" <(csgrep --mode=json --imp-level=1 '{js_file}') -" 326 | 327 | # write the result into *-imp.js 328 | imp_js_file = re.sub("\\.js", "-imp.js", js_file) 329 | cmd += " > '%s'" % imp_js_file 330 | 331 | # bash is needed to process <(...) 332 | cmd = strlist_to_shell_cmd(["bash", "-c", cmd], escape_special=True) 333 | results.exec_cmd(cmd, shell=True) 334 | 335 | # initialize the "imp" flag in the resulting `-all.js` output file 336 | # and replace the original .js file by `-imp.js` 337 | all_js_file = re.sub("\\.js", "-all.js", js_file) 338 | cmd = "cslinker --implist '%s' '%s' > '%s' && mv -v '%s' '%s'" \ 339 | % (imp_js_file, js_file, all_js_file, imp_js_file, js_file) 340 | if 0 != results.exec_cmd(cmd, shell=True): 341 | results.error("failed to tag important findings in the full results", ec=0) 342 | 343 | # generate *-all{.err,.html,-summary.txt} 344 | transform_results(all_js_file, results) 345 | 346 | (err_file, _) = transform_results(js_file, results) 347 | 348 | if props.print_defects: 349 | os.system("csgrep '%s'" % err_file) 350 | 351 | 352 | def apply_result_filters(props, results, supp_filters=[]): 353 | """apply filters, sort the list and record suppressed results""" 354 | js_file = os.path.join(results.resdir, "scan-results.js") 355 | all_file = os.path.join(results.dbgdir, "scan-results-all.js") 356 | 357 | # apply filters, sort the list and store the result as scan-results.js 358 | cmd = f"cat '{all_file}'" 359 | for filt in props.result_filters: 360 | cmd += f" | {filt}" 361 | cmd += f" | cssort --key=path > '{js_file}'" 362 | results.exec_cmd(cmd, shell=True) 363 | 364 | # record suppressed results 365 | js_supp = os.path.join(results.dbgdir, "suppressed-results.js") 366 | cmd = f"cat '{all_file}'" 367 | for filt in supp_filters: 368 | cmd += f" | {filt}" 369 | cmd += f" | csdiff --show-internal '{js_file}' -" 370 | cmd += f" | cssort > '{js_supp}'" 371 | results.exec_cmd(cmd, shell=True) 372 | finalize_results(js_supp, results, props) 373 | finalize_results(js_file, results, props) 374 | 375 | # create `-imp` symlinks for compatibility (if important defects were filtered) 376 | if props.imp_checker_set: 377 | for suffix in [".err", ".html", ".js", "-summary.txt"]: 378 | src = f"scan-results{suffix}" 379 | dst = os.path.join(results.resdir, f"scan-results-imp{suffix}") 380 | results.exec_cmd(["ln", "-s", src, dst]) 381 | 382 | 383 | def handle_kfp_git_url(props): 384 | """Update props.result_filters based on props.kfp_git_url""" 385 | if not props.kfp_git_url: 386 | return 387 | 388 | # construct the command to invoke csfilter-kfp 389 | # FIXME: csfilter-kfp will update scan metadata in the JSON files but not in `scan.ini` # pylint: disable=fixme 390 | filter_cmd = f"csfilter-kfp --json-output --kfp-git-url={shlex.quote(props.kfp_git_url)} --verbose" 391 | if props.nvr is not None: 392 | filter_cmd += f" --project-nvr={shlex.quote(props.nvr)}" 393 | 394 | # append the command as a results filter 395 | props.result_filters += [filter_cmd] 396 | 397 | 398 | def handle_known_fp_list(props, results): 399 | """Update props.result_filters based on props.known_false_positives""" 400 | if not props.known_false_positives: 401 | return 402 | 403 | # update scan metadata 404 | results.ini_writer.append("known-false-positives", props.known_false_positives) 405 | cmd = ["rpm", "-qf", props.known_false_positives] 406 | (ec, out) = results.get_cmd_output(cmd, shell=False) 407 | if 0 == ec: 408 | # record the RPM package that provided the known-false-positives file 409 | results.ini_writer.append("known-false-positives-rpm", out.strip()) 410 | 411 | # install global filter of known false positives 412 | filter_cmd = f'csdiff --json-output --show-internal "{props.known_false_positives}" -' 413 | props.result_filters += [filter_cmd] 414 | 415 | if props.pkg is None: 416 | # no package name available 417 | return 418 | 419 | kfp_dir = re.sub("\\.js", ".d", props.known_false_positives) 420 | if not os.path.isdir(kfp_dir): 421 | # no per-pkg known false positives available 422 | return 423 | 424 | ep_file = os.path.join(kfp_dir, props.pkg, "exclude-paths.txt") 425 | if not os.path.exists(ep_file): 426 | # no list of path regexes to exclude for this pkg 427 | return 428 | 429 | # install path exclusion filters for this pkg 430 | with open(ep_file) as file_handle: 431 | lines = file_handle.readlines() 432 | for line in lines: 433 | path_re = line.strip() 434 | if len(path_re) == 0 or path_re.startswith("#"): 435 | # skip comments and empty lines 436 | continue 437 | filter_cmd = f'csgrep --mode=json --invert-match --path={shlex.quote(path_re)}' 438 | props.result_filters += [filter_cmd] 439 | -------------------------------------------------------------------------------- /csmock/common/snyk.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | import json 19 | 20 | 21 | def snyk_write_analysis_meta(results, raw_results_file): 22 | """write snyk stats on metadata file. At the time, we write the total number of files, 23 | the number of supported files and the coverage ratio.""" 24 | 25 | try: 26 | with open(raw_results_file) as snyk_results_file: 27 | data = json.load(snyk_results_file) 28 | coverage_stats = data["runs"][0]["properties"]["coverage"] 29 | total_files = 0 30 | supported_files = 0 31 | for lang in coverage_stats: 32 | total_files += lang["files"] 33 | if lang["type"] == "SUPPORTED": 34 | supported_files += lang["files"] 35 | 36 | coverage_ratio = 0 37 | if total_files > 0: 38 | coverage_ratio = int(supported_files * 100 / total_files) 39 | 40 | results.ini_writer.append("snyk-scanned-files-coverage", coverage_ratio) 41 | results.ini_writer.append("snyk-scanned-files-success", supported_files) 42 | results.ini_writer.append("snyk-scanned-files-total", total_files) 43 | 44 | return 0 45 | 46 | except OSError as e: 47 | results.error(f"snyk-scan: failed to read {raw_results_file}: {e}") 48 | return 1 49 | 50 | except KeyError as e: 51 | results.error(f"snyk-scan: error parsing results from snyk-results.sarif file: {e}") 52 | return 1 53 | -------------------------------------------------------------------------------- /csmock/common/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | import os 19 | import re 20 | import shlex 21 | 22 | 23 | def shell_quote(str_in): 24 | str_out = "" 25 | for i in range(0, len(str_in)): 26 | c = str_in[i] 27 | if c == "\\": 28 | str_out += "\\\\" 29 | elif c == "\"": 30 | str_out += "\\\"" 31 | elif c == "$": 32 | str_out += "\\$" 33 | else: 34 | str_out += c 35 | return "\"" + str_out + "\"" 36 | 37 | 38 | def arg_value_by_name(parser, args, arg_name): 39 | """return value of an argument parsed by argparse.ArgumentParser""" 40 | for action in parser._actions: 41 | if arg_name in action.option_strings: 42 | return getattr(args, action.dest) 43 | 44 | 45 | def sanitize_opts_arg(parser, args, arg_name): 46 | """sanitize command-line options passed to an option of argparse.ArgumentParser""" 47 | opts_str = arg_value_by_name(parser, args, arg_name) 48 | if opts_str is None: 49 | return None 50 | 51 | # split, quote, and rejoin the options to avoid shell injection 52 | try: 53 | split_opts = shlex.split(opts_str) 54 | 55 | # starting with Python 3.8, one can use shlex.join(split_opts) 56 | return ' '.join(shlex.quote(arg) for arg in split_opts) 57 | 58 | except ValueError as e: 59 | parser.error(f"failed to parse value given to {arg_name}: {str(e)}") 60 | 61 | 62 | def strlist_to_shell_cmd(cmd_in, escape_special=False): 63 | def translate_one(i): 64 | if escape_special: 65 | return shell_quote(i) 66 | return "'%s'" % i 67 | 68 | if type(cmd_in) is str: 69 | return "sh -c %s" % translate_one(cmd_in) 70 | cmd_out = "" 71 | for i in cmd_in: 72 | cmd_out += " " + translate_one(i) 73 | return cmd_out.lstrip() 74 | 75 | 76 | def write_toolver(ini_writer, tool_key, ver): 77 | ini_writer.append("analyzer-version-%s" % tool_key, ver) 78 | 79 | 80 | def write_toolver_from_rpmlist(results, mock, tool, tool_key): 81 | cmd = "grep '^%s-[0-9]' %s/rpm-list-mock.txt" % (tool, results.dbgdir) 82 | (rc, nvr) = results.get_cmd_output(cmd) 83 | if rc != 0: 84 | results.error("tool \"%s\" does not seem to be installed in build root" \ 85 | % tool, ec=0) 86 | return rc 87 | 88 | ver = re.sub("-[0-9].*$", "", re.sub("^%s-" % tool, "", nvr.strip())) 89 | write_toolver(results.ini_writer, tool_key, ver) 90 | return 0 91 | 92 | 93 | def install_default_toolver_hook(props, tool): 94 | tool_key = tool.lower() 95 | 96 | def toolver_by_rpmlist_hook(results, mock): 97 | return write_toolver_from_rpmlist(results, mock, tool, tool_key) 98 | 99 | props.post_depinst_hooks += [toolver_by_rpmlist_hook] 100 | 101 | 102 | def add_paired_flag(parser, name, help): 103 | help_no = "disables --" + name 104 | arg = parser.add_argument( 105 | "--" + name, action="store_const", const=True, help=help) 106 | parser.add_argument( 107 | "--no-" + name, action="store_const", const=False, help=help_no, 108 | dest=arg.dest) 109 | 110 | 111 | def install_script_scan_opts(parser, tool): 112 | # render help text 113 | help_tpl = "make %s scan files in the %s directory (%s by default) " 114 | help_build = help_tpl % (tool, "build", "disabled") 115 | help_install = help_tpl % (tool, "install", "enabled") 116 | 117 | # add 2x2 options 118 | add_paired_flag(parser, tool + "-scan-build", help=help_build) 119 | add_paired_flag(parser, tool + "-scan-install", help=help_install) 120 | 121 | 122 | def dirs_to_scan_by_args(parser, args, props, tool): 123 | scan_build = getattr(args, tool + "_scan_build") 124 | if scan_build is None: 125 | scan_build = (props.shell_cmd_to_build is not None) 126 | 127 | scan_install = getattr(args, tool + "_scan_install") 128 | if scan_install is None: 129 | scan_install = (props.shell_cmd_to_build is None) 130 | 131 | if not scan_build and not scan_install: 132 | parser.error(f"either --{tool}-scan-build or --{tool}-scan-install must be enabled") 133 | 134 | if scan_install and (props.shell_cmd_to_build is not None): 135 | parser.error(f"--shell-cmd and --{tool}-scan-install cannot be used together") 136 | 137 | dirs_to_scan = [] 138 | if scan_build: 139 | dirs_to_scan += ["/builddir/build/BUILD"] 140 | 141 | if scan_install: 142 | # the second variant was introduced by a backward-incompatible change in `rpm` 143 | # https://github.com/rpm-software-management/rpm/commit/9d35c8df497534e1fbd806a4dc78802bcf35d7cb 144 | dirs_to_scan += ["/builddir/build/BUILDROOT", "/builddir/build/BUILD/*/BUILDROOT"] 145 | props.need_rpm_bi = True 146 | 147 | return ' '.join(dirs_to_scan) 148 | 149 | 150 | def require_file(parser, name): 151 | """Print an error and exit unsuccessfully if 'name' is not a file""" 152 | if not os.path.isfile(name): 153 | parser.error(f"'{name}' is not a file") 154 | -------------------------------------------------------------------------------- /csmock/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csutils/csmock/a728c5bfbf9e68a89f2f7ebba78b7e5dc0abdeaa/csmock/plugins/__init__.py -------------------------------------------------------------------------------- /csmock/plugins/bandit.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | import csmock.common.util 19 | 20 | 21 | RUN_BANDIT_SH = "/usr/share/csmock/scripts/run-bandit.sh" 22 | 23 | BANDIT_CAPTURE = "/builddir/bandit-capture.err" 24 | 25 | FILTER_CMD = "csgrep --quiet '%s' | csgrep --event '%s' > '%s'" 26 | 27 | 28 | class PluginProps: 29 | def __init__(self): 30 | self.description = "A tool designed to find common security issues in Python code." 31 | 32 | 33 | class Plugin: 34 | def __init__(self): 35 | self.enabled = False 36 | self._severity_levels = ['LOW', 'MEDIUM', 'HIGH'] 37 | 38 | def get_props(self): 39 | return PluginProps() 40 | 41 | def enable(self): 42 | self.enabled = True 43 | 44 | def init_parser(self, parser): 45 | csmock.common.util.install_script_scan_opts(parser, "bandit") 46 | parser.add_argument("--bandit-evt-filter", default="^B[0-9]+", 47 | help="report only Bandit defects " 48 | "whose key event matches the given regex " 49 | "(defaults to '^B[0-9]+')") 50 | parser.add_argument("--bandit-severity-filter", default="LOW", 51 | help="suppress Bandit defects whose severity level is below given level " 52 | "(default 'LOW')", choices=self._severity_levels) 53 | 54 | def handle_args(self, parser, args, props): 55 | if not self.enabled: 56 | return 57 | 58 | # which directories are we going to scan (build and/or install) 59 | dirs_to_scan = csmock.common.util.dirs_to_scan_by_args( 60 | parser, args, props, "bandit") 61 | 62 | # Note: bandit is running on python3, pbr needs git to assert correct version 63 | props.install_pkgs += ["bandit"] 64 | 65 | severity_filter = dict(zip(self._severity_levels, ['-l', '-ll', '-lll']))[args.bandit_severity_filter.upper()] 66 | run_cmd = f"shopt -s nullglob && {RUN_BANDIT_SH} {severity_filter} {dirs_to_scan} > {BANDIT_CAPTURE}" 67 | props.post_build_chroot_cmds += [run_cmd] 68 | props.copy_out_files += [BANDIT_CAPTURE] 69 | 70 | csmock.common.util.install_default_toolver_hook(props, "bandit") 71 | 72 | def filter_hook(results): 73 | src = results.dbgdir_raw + BANDIT_CAPTURE 74 | dst = "%s/bandit-capture.err" % results.dbgdir_uni 75 | cmd = FILTER_CMD % (src, args.bandit_evt_filter, dst) 76 | return results.exec_cmd(cmd, shell=True) 77 | 78 | props.post_process_hooks += [filter_hook] 79 | -------------------------------------------------------------------------------- /csmock/plugins/cbmc.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | import csmock.common.util 19 | from csmock.common.cflags import flags_by_warning_level 20 | 21 | 22 | CBMC_CAPTURE_DIR = "/builddir/cbmc-capture" 23 | 24 | DEFAULT_CBMC_TIMEOUT = 30 25 | 26 | 27 | class PluginProps: 28 | def __init__(self): 29 | self.description = "Bounded Model Checker for C and C++ programs" 30 | 31 | # hook this plug-in before "gcc" to make ScanProps:enable_csexec() work 32 | self.pass_before = ["gcc"] 33 | 34 | 35 | class Plugin: 36 | def __init__(self): 37 | self.enabled = False 38 | self.flags = flags_by_warning_level(0) 39 | 40 | def get_props(self): 41 | return PluginProps() 42 | 43 | def enable(self): 44 | self.enabled = True 45 | 46 | def init_parser(self, parser): 47 | parser.add_argument( 48 | "--cbmc-add-flag", action="append", default=[], 49 | help="append the given flag when invoking cbmc \ 50 | (can be used multiple times)") 51 | 52 | parser.add_argument( 53 | "--cbmc-timeout", type=int, default=DEFAULT_CBMC_TIMEOUT, 54 | help="maximal amount of time taken by analysis of a single process [s]") 55 | 56 | def handle_args(self, parser, args, props): 57 | if not self.enabled: 58 | return 59 | 60 | # make sure cbmc and its helper scripts are installed in chroot 61 | props.install_pkgs += ["cbmc", "cbmc-utils"] 62 | 63 | # record version of the installed "cbmc" tool 64 | csmock.common.util.install_default_toolver_hook(props, "cbmc") 65 | 66 | # enable cswrap 67 | props.enable_cswrap() 68 | props.cswrap_filters += ["csgrep --mode=json --invert-match --checker GCC_WARNING --event error"] 69 | 70 | # set dioscc as the default compiler 71 | # FIXME: this is not 100% reliable 72 | # FIXME: goto-gcc does not seem to be able to compile C++ 73 | props.env["CC"] = "goto-gcc" 74 | props.env["CXX"] = "goto-gcc" 75 | props.rpm_opts += ["--define", "__cc goto-gcc", "--define", "__cxx goto-gcc", "--define", "__cpp goto-gcc -E"] 76 | 77 | # nuke default options 78 | props.rpm_opts += ["--define", "optflags -O0", "--define", "build_ldflags -O0"] 79 | 80 | # hook csexec into the binaries produced in %build 81 | props.enable_csexec() 82 | 83 | # create directory for cbmc's results 84 | def create_cap_dir_hook(results, mock): 85 | cmd = "mkdir -pv '%s'" % CBMC_CAPTURE_DIR 86 | return mock.exec_mockbuild_cmd(cmd) 87 | props.post_depinst_hooks += [create_cap_dir_hook] 88 | 89 | # default cbmc cmd-line 90 | wrap_cmd_list = [ 91 | "--skip-ld-linux", 92 | "/usr/bin/csexec-cbmc", 93 | "-t", "%d" % args.cbmc_timeout, 94 | "-l", CBMC_CAPTURE_DIR, 95 | "-c","--unwind 1 --json-ui --verbosity 4 --pointer-overflow-check --memory-leak-check"] 96 | 97 | # append custom args if specified 98 | # FIXME: what about single arguments with whitespaces? 99 | wrap_cmd_list[-1] += " " + " ".join(args.cbmc_add_flag) 100 | 101 | # FIXME: multiple runs of %check for multiple dynamic analyzers not yet supported 102 | assert "CSEXEC_WRAP_CMD" not in props.env 103 | 104 | # configure csexec to use cbmc as the execution wrapper 105 | wrap_cmd = csmock.common.cflags.serialize_flags(wrap_cmd_list, separator="\\a") 106 | props.env["CSEXEC_WRAP_CMD"] = wrap_cmd 107 | 108 | # write all compiler flags to the environment 109 | self.flags.append_flags(['-Wno-unknown-warning-option', '-O0', '-g', '-Wl,--dynamic-linker,/usr/bin/csexec-loader']) 110 | self.flags.remove_flags(['-O1', '-O2', '-O3', '-Os', '-Ofast', '-Og']) 111 | self.flags.write_to_env(props.env) 112 | 113 | # run %check (disabled by default for static analyzers) 114 | props.run_check = True 115 | 116 | # pick the captured files when %check is complete 117 | props.copy_out_files += [CBMC_CAPTURE_DIR] 118 | 119 | # transform XML files produced by cbmc into csdiff format 120 | def filter_hook(results): 121 | src_dir = results.dbgdir_raw + CBMC_CAPTURE_DIR 122 | 123 | # ensure we have permission to read all capture files 124 | results.exec_cmd(['chmod', '-R', '+r', src_dir]) 125 | 126 | # `cd` first to avoid `csgrep: Argument list too long` error on glob expansion 127 | dst = f"{results.dbgdir_uni}/cbmc-capture.js" 128 | 129 | cmd = f"cd '{src_dir}' && touch empty.conv && csgrep --mode=json --remove-duplicates *.conv > '{dst}'" 130 | 131 | return results.exec_cmd(cmd, shell=True) 132 | props.post_process_hooks += [filter_hook] 133 | -------------------------------------------------------------------------------- /csmock/plugins/clang.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | # standard imports 19 | import os 20 | import subprocess 21 | 22 | # local imports 23 | import csmock.common.cflags 24 | import csmock.common.util 25 | 26 | 27 | class PluginProps: 28 | def __init__(self): 29 | self.description = "Source code analysis tool that finds bugs in C, C++, and Objective-C programs." 30 | 31 | # include this plug-in in `csmock --all-tools` 32 | self.stable = True 33 | 34 | 35 | class Plugin: 36 | def __init__(self): 37 | self.enabled = False 38 | 39 | def get_props(self): 40 | return PluginProps() 41 | 42 | def enable(self): 43 | self.enabled = True 44 | 45 | def init_parser(self, parser): 46 | parser.add_argument( 47 | "--clang-add-flag", action="append", default=[], 48 | help="append the given flag when invoking clang static analyzer \ 49 | (can be used multiple times)") 50 | 51 | def handle_args(self, parser, args, props): 52 | if not self.enabled: 53 | return 54 | 55 | props.enable_cswrap() 56 | props.env["CSWRAP_TIMEOUT_FOR"] += ":clang:clang++" 57 | if args.clang_add_flag: 58 | # propagate custom clang flags 59 | props.env["CSCLNG_ADD_OPTS"] = csmock.common.cflags.serialize_flags(args.clang_add_flag) 60 | 61 | props.cswrap_filters += \ 62 | ["csgrep --mode=json --invert-match --checker CLANG_WARNING --event error"] 63 | 64 | props.install_pkgs += ["clang"] 65 | 66 | # resolve csclng_path by querying csclng binary 67 | cmd = ["csclng", "--print-path-to-wrap"] 68 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 69 | (out, err) = p.communicate() 70 | csclng_path = out.decode("utf8").strip() 71 | 72 | props.path = [csclng_path] + props.path 73 | props.copy_in_files += \ 74 | ["/usr/bin/csclng", csclng_path] 75 | if os.path.exists("/usr/bin/csclng++"): 76 | props.copy_in_files += ["/usr/bin/csclng++"] 77 | 78 | csmock.common.util.install_default_toolver_hook(props, "clang") 79 | -------------------------------------------------------------------------------- /csmock/plugins/clippy.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from csmock.common.util import write_toolver_from_rpmlist 4 | 5 | RUN_CLIPPY_CONVERT = "/usr/share/csmock/scripts/convert-clippy.py" 6 | CLIPPY_OUTPUT = "/builddir/clippy-output.txt" 7 | CLIPPY_INJECT_SCRIPT = "/usr/share/csmock/scripts/inject-clippy.sh" 8 | 9 | class PluginProps: 10 | def __init__(self): 11 | self.description = "Rust source code analyzer which looks for programming errors." 12 | 13 | 14 | class Plugin: 15 | def __init__(self): 16 | self.enabled = False 17 | 18 | def get_props(self): 19 | return PluginProps() 20 | 21 | def enable(self): 22 | self.enabled = True 23 | 24 | def init_parser(self, parser): 25 | return 26 | 27 | def handle_args(self, parser, args, props): 28 | if not self.enabled: 29 | return 30 | 31 | # install `clippy` only if the package is available in the build repos 32 | props.install_opt_pkgs += ["clippy"] 33 | 34 | def inject_clippy_hook(results, mock): 35 | ec = write_toolver_from_rpmlist(results, mock, "clippy", "clippy") 36 | if 0 != ec: 37 | # a warning has already been emitted 38 | return 0 39 | 40 | # clippy was found in the buildroot -> instrument the build 41 | props.copy_out_files += [CLIPPY_OUTPUT] 42 | return mock.exec_chroot_cmd(CLIPPY_INJECT_SCRIPT) 43 | 44 | props.post_depinst_hooks += [inject_clippy_hook] 45 | 46 | def convert_hook(results): 47 | src = f"{results.dbgdir_raw}{CLIPPY_OUTPUT}" 48 | if not os.path.exists(src): 49 | # if `cargo build` was not executed during the scan, there are no results to process 50 | return 0 51 | 52 | dst = f"{results.dbgdir_uni}/clippy-capture.err" 53 | cmd = f'set -o pipefail; {RUN_CLIPPY_CONVERT} < {src} | csgrep --remove-duplicates > {dst}' 54 | return results.exec_cmd(cmd, shell=True) 55 | 56 | props.post_process_hooks += [convert_hook] 57 | -------------------------------------------------------------------------------- /csmock/plugins/cppcheck.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | import os 19 | import re 20 | import subprocess 21 | 22 | # local imports 23 | import csmock.common.cflags 24 | 25 | 26 | class PluginProps: 27 | def __init__(self): 28 | self.description = "Static analysis tool for C/C++ code." 29 | 30 | # include this plug-in in `csmock --all-tools` 31 | self.stable = True 32 | 33 | 34 | class Plugin: 35 | def __init__(self): 36 | self.enabled = False 37 | self.use_host_cppcheck = False 38 | 39 | def get_props(self): 40 | return PluginProps() 41 | 42 | def enable(self): 43 | self.enabled = True 44 | 45 | def init_parser(self, parser): 46 | parser.add_argument( 47 | "--use-host-cppcheck", action="store_true", 48 | help="use statically linked cppcheck installed on the host \ 49 | (automatically enables the Cppcheck plug-in)") 50 | 51 | parser.add_argument( 52 | "--cppcheck-add-flag", action="append", default=[], 53 | help="append the given flag when invoking cppcheck \ 54 | (can be used multiple times)") 55 | 56 | def handle_args(self, parser, args, props): 57 | self.use_host_cppcheck = args.use_host_cppcheck 58 | if self.use_host_cppcheck: 59 | self.enable() 60 | 61 | if not self.enabled: 62 | return 63 | 64 | props.enable_cswrap() 65 | props.env["CSWRAP_TIMEOUT_FOR"] += ":cppcheck" 66 | props.cswrap_filters += ["csgrep --mode=json --invert-match \ 67 | --checker CPPCHECK_WARNING \ 68 | --event 'cppcheckError|internalAstError|normalCheckLevelMaxBranches|preprocessorErrorDirective|syntaxError|unknownMacro'"] 69 | 70 | if args.cppcheck_add_flag: 71 | # propagate custom cppcheck flags 72 | props.env["CSCPPC_ADD_OPTS"] = csmock.common.cflags.serialize_flags(args.cppcheck_add_flag) 73 | 74 | # resolve cscppc_path by querying cscppc binary 75 | cmd = ["cscppc", "--print-path-to-wrap"] 76 | subproc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 77 | (out, err) = subproc.communicate() 78 | cscppc_path = out.decode("utf8").strip() 79 | 80 | props.path = [cscppc_path] + props.path 81 | props.copy_in_files += \ 82 | ["/usr/bin/cscppc", cscppc_path, "/usr/share/cscppc"] 83 | 84 | if self.use_host_cppcheck: 85 | # install only tinyxml2 (if acutally required by cppcheck) 86 | cmd = "rpm -q cppcheck --requires | grep tinyxml2 > /dev/null" 87 | if os.system(cmd) == 0: 88 | props.install_pkgs += ["tinyxml2"] 89 | 90 | # copy cppcheck's executable into the chroot 91 | props.copy_in_files += ["/usr/bin/cppcheck"] 92 | 93 | # copy cppcheck's data files into the chroot 94 | if os.path.isdir("/usr/share/Cppcheck"): 95 | props.copy_in_files += ["/usr/share/Cppcheck"] 96 | if os.path.isdir("/usr/share/cppcheck"): 97 | props.copy_in_files += ["/usr/share/cppcheck"] 98 | else: 99 | # install cppcheck into the chroot 100 | props.install_pkgs += ["cppcheck"] 101 | 102 | def store_cppcheck_version_hook(results, mock): 103 | cmd = mock.get_mock_cmd(["--chroot", "cppcheck --version"]) 104 | (ec, verstr) = results.get_cmd_output(cmd, shell=False) 105 | if ec != 0: 106 | if self.use_host_cppcheck: 107 | results.error("--use-host-cppcheck expects statically linked cppcheck installed on the host", ec=0) 108 | results.error("failed to query cppcheck version", ec=ec) 109 | return ec 110 | 111 | ver = re.sub("^Cppcheck ", "", verstr.strip()) 112 | results.ini_writer.append("analyzer-version-cppcheck", ver) 113 | return 0 114 | 115 | props.post_depinst_hooks += [store_cppcheck_version_hook] 116 | -------------------------------------------------------------------------------- /csmock/plugins/divine.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | import csmock.common.util 19 | 20 | from csmock.common.cflags import flags_by_warning_level 21 | 22 | 23 | DIVINE_CAPTURE_DIR = "/builddir/divine-capture" 24 | 25 | DEFAULT_DIVINE_TIMEOUT = 30 26 | 27 | 28 | class PluginProps: 29 | def __init__(self): 30 | self.description = "A formal verification tool based on explicit-state model checking." 31 | 32 | # hook this plug-in before "gcc" to make ScanProps:enable_csexec() work 33 | self.pass_before = ["gcc"] 34 | 35 | 36 | class Plugin: 37 | def __init__(self): 38 | self.enabled = False 39 | self.flags = flags_by_warning_level(0) 40 | 41 | def get_props(self): 42 | return PluginProps() 43 | 44 | def enable(self): 45 | self.enabled = True 46 | 47 | def init_parser(self, parser): 48 | parser.add_argument( 49 | "--divine-add-flag", action="append", default=[], 50 | help="append the given flag when invoking divine (can be used multiple times)") 51 | 52 | parser.add_argument( 53 | "--divine-timeout", type=int, default=DEFAULT_DIVINE_TIMEOUT, 54 | help="maximal amount of time taken by analysis of a single process [s]") 55 | 56 | def handle_args(self, parser, args, props): 57 | if not self.enabled: 58 | return 59 | 60 | # make sure divine and gllvm are installed in chroot 61 | props.add_repos += ["https://download.copr.fedorainfracloud.org/results/@aufover/divine/fedora-$releasever-$basearch/"] 62 | props.install_pkgs += ["divine"] 63 | 64 | # dioscc seems to require /usr/include/gnu/stubs-32.h 65 | props.install_opt_pkgs += ["glibc-devel(x86-32)"] 66 | 67 | # enable cswrap 68 | props.enable_cswrap() 69 | props.cswrap_filters += ["csgrep --mode=json --invert-match --checker CLANG_WARNING --event error"] 70 | 71 | # set dioscc as the default compiler 72 | # FIXME: this is not 100% reliable 73 | props.env["CC"] = "dioscc" 74 | props.env["CXX"] = "diosc++" 75 | props.rpm_opts += ["--define", "__cc dioscc", "--define", "__cxx diosc++", "--define", "__cpp dioscc -E"] 76 | 77 | # nuke default options 78 | props.rpm_opts += ["--define", "toolchain clang", "--define", "optflags -O0", "--define", "build_ldflags -O0"] 79 | 80 | # record version of the installed "divine" tool 81 | csmock.common.util.install_default_toolver_hook(props, "divine") 82 | 83 | # hook csexec into the binaries produced in %build 84 | props.enable_csexec() 85 | 86 | # create directory for divine's results 87 | def create_cap_dir_hook(results, mock): 88 | cmd = "mkdir -pv '%s'" % DIVINE_CAPTURE_DIR 89 | return mock.exec_mockbuild_cmd(cmd) 90 | props.post_depinst_hooks += [create_cap_dir_hook] 91 | 92 | # default divine cmd-line 93 | wrap_cmd_list = [ 94 | "--skip-ld-linux", 95 | "/usr/bin/csexec-divine", 96 | "-l", DIVINE_CAPTURE_DIR, 97 | # --lart stubs replaces undefined functions with stubs 98 | # -o ignore:notimplemented ignores calls to undefined functions 99 | # -o ignore:exit ignores non-zero exit codes 100 | "-d", "check --lart stubs -o ignore:notimplemented " 101 | f"-o ignore:exit --max-time {args.divine_timeout}"] 102 | 103 | # append custom args if specified 104 | # FIXME: what about single arguments with whitespaces? 105 | wrap_cmd_list[-1] += " " + " ".join(args.divine_add_flag) 106 | 107 | # FIXME: multiple runs of %check for multiple dynamic analyzers not yet supported 108 | assert "CSEXEC_WRAP_CMD" not in props.env 109 | 110 | # configure csexec to use divine as the execution wrapper 111 | wrap_cmd = csmock.common.cflags.serialize_flags(wrap_cmd_list, separator="\\a") 112 | props.env["CSEXEC_WRAP_CMD"] = wrap_cmd 113 | 114 | # write all compiler flags to the environment 115 | self.flags.append_flags(['-Wno-unknown-warning-option', '-O0', '-g', '-Wl,--dynamic-linker,/usr/bin/csexec-loader']) 116 | self.flags.remove_flags(['-O1', '-O2', '-O3', '-Os', '-Ofast', '-Og']) 117 | self.flags.write_to_env(props.env) 118 | 119 | # run %check (disabled by default for static analyzers) 120 | props.run_check = True 121 | 122 | # pick the captured files when %check is complete 123 | props.copy_out_files += [DIVINE_CAPTURE_DIR] 124 | 125 | # transform log files produced by divine into csdiff format 126 | def filter_hook(results): 127 | src_dir = results.dbgdir_raw + DIVINE_CAPTURE_DIR 128 | 129 | # ensure we have permission to read all capture files 130 | results.exec_cmd(['chmod', '-R', '+r', src_dir]) 131 | 132 | # `cd` first to avoid `csgrep: Argument list too long` error on glob expansion 133 | dst = f"{results.dbgdir_uni}/divine-capture.js" 134 | cmd = f"cd '{src_dir}' && touch empty.conv && csgrep --mode=json --remove-duplicates *.conv > '{dst}'" 135 | return results.exec_cmd(cmd, shell=True) 136 | props.post_process_hooks += [filter_hook] 137 | -------------------------------------------------------------------------------- /csmock/plugins/gcc.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014-2023 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | # standard imports 19 | import os 20 | import subprocess 21 | 22 | # local imports 23 | import csmock.common.util 24 | 25 | from csmock.common.cflags import add_custom_flag_opts, flags_by_warning_level 26 | 27 | CSGCCA_BIN = "/usr/bin/csgcca" 28 | 29 | # directory for GCC results (currently used only with `--gcc-analyzer-bin`) 30 | GCC_RESULTS_DIR = "/builddir/gcc-results" 31 | 32 | CSMOCK_GCC_WRAPPER_NAME = 'csmock-gcc-wrapper' 33 | CSMOCK_GCC_WRAPPER_PATH = '/usr/bin/%s' % CSMOCK_GCC_WRAPPER_NAME 34 | 35 | # script to run gcc analyzer and dump its output to a seaprate SARIF file in GCC_RESULTS_DIR 36 | CSMOCK_GCC_WRAPPER_TEMPLATE = f"""#!/bin/bash 37 | fn=$(flock {GCC_RESULTS_DIR} mktemp "{GCC_RESULTS_DIR}/$$-XXXX.sarif") 38 | exec %s "$@" -fdiagnostics-set-output="sarif:file=$fn" 39 | """ 40 | 41 | # command to read and join all captured SARIF files 42 | FILTER_CMD = "csgrep --mode=json --remove-duplicates" 43 | 44 | SANITIZER_CAPTURE_DIR = "/builddir/gcc-sanitizer-capture" 45 | 46 | class PluginProps: 47 | def __init__(self): 48 | self.description = "Plugin capturing GCC warnings, optionally with customized compiler flags." 49 | 50 | # include this plug-in in `csmock --all-tools` 51 | self.stable = True 52 | 53 | 54 | class Plugin: 55 | def __init__(self): 56 | self.enabled = False 57 | self.sanitize = False 58 | self.flags = flags_by_warning_level(0) 59 | self.csgcca_path = None 60 | 61 | def get_props(self): 62 | return PluginProps() 63 | 64 | def enable(self): 65 | self.enabled = True 66 | 67 | def enable_sanitize(self, props, pkgs, flags): 68 | self.enabled = True 69 | self.sanitize = True 70 | props.install_pkgs += pkgs 71 | self.flags.append_flags(flags) 72 | 73 | # FIXME: too hacky, strip lib prefix from the name 74 | name = pkgs[0][3:] 75 | 76 | # save logs to specified files 77 | props.env[f"{name.upper()}_OPTIONS"] = \ 78 | f"log_path={SANITIZER_CAPTURE_DIR}/{name}" 79 | 80 | # GCC sanitizers usually do not work well with valgrind 81 | props.install_pkgs_blacklist += ["valgrind"] 82 | 83 | def init_parser(self, parser): 84 | parser.add_argument( 85 | "-w", "--gcc-warning-level", type=int, 86 | help="Adjust GCC warning level. -w0 means default flags, \ 87 | -w1 appends -Wall and -Wextra, and -w2 enables some other useful warnings. \ 88 | (automatically enables the GCC plug-in)") 89 | 90 | parser.add_argument( 91 | "--gcc-analyze", action="store_true", 92 | help="run `gcc -fanalyzer` in a separate process") 93 | 94 | parser.add_argument( 95 | "--gcc-analyzer-bin", action="store", 96 | help="Use custom build of gcc to perform scan. \ 97 | Absolute path to the binary must be provided.") 98 | 99 | parser.add_argument( 100 | "--gcc-analyze-add-flag", action="append", default=[], 101 | help="append the given flag when invoking `gcc -fanalyzer` \ 102 | (can be used multiple times)") 103 | 104 | parser.add_argument( 105 | "--gcc-set-env", action="store_true", 106 | help="set $CC and $CXX to gcc and g++, respectively, for build") 107 | 108 | # -fsanitize={address,leak} cannot be combined with -fsanitize=thread 109 | # generally, more sanitizer libraries cannot be combined because 110 | # the collection results would be unreliable: 111 | # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94328 112 | group = parser.add_mutually_exclusive_group() 113 | group.add_argument( 114 | "--gcc-sanitize-address", action="store_true", 115 | help="enable %%check and compile with -fsanitize=address") 116 | 117 | group.add_argument( 118 | "--gcc-sanitize-leak", action="store_true", 119 | help="enable %%check and compile with -fsanitize=leak") 120 | 121 | group.add_argument( 122 | "--gcc-sanitize-thread", action="store_true", 123 | help="enable %%check and compile with -fsanitize=thread") 124 | 125 | group.add_argument( 126 | "--gcc-sanitize-undefined", action="store_true", 127 | help="enable %%check and compile with -fsanitize=undefined") 128 | 129 | add_custom_flag_opts(parser) 130 | 131 | def handle_args(self, parser, args, props): 132 | if args.gcc_warning_level is not None: 133 | self.enable() 134 | self.flags = flags_by_warning_level(args.gcc_warning_level) 135 | 136 | if args.gcc_analyze or \ 137 | args.gcc_analyzer_bin or \ 138 | getattr(args, "all_tools", False): 139 | self.enable() 140 | 141 | if args.gcc_analyzer_bin and not args.gcc_analyzer_bin.startswith("/"): 142 | parser.error("--gcc-analyzer-bin should be an absolute path. " 143 | "Found value: '%s'." % args.gcc_analyzer_bin) 144 | 145 | # resolve csgcca_path by querying csclng binary 146 | cmd = [CSGCCA_BIN, "--print-path-to-wrap"] 147 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 148 | (out, _) = p.communicate() 149 | if 0 != p.returncode: 150 | parser.error("--gcc-analyze requires %s to be available" % CSGCCA_BIN) 151 | self.csgcca_path = out.decode("utf8").strip() 152 | props.copy_in_files += [CSGCCA_BIN, self.csgcca_path] 153 | 154 | if args.gcc_set_env: 155 | self.enable() 156 | props.env["CC"] = "gcc" 157 | props.env["CXX"] = "g++" 158 | 159 | if args.gcc_sanitize_address: 160 | self.enable_sanitize(props, ["libasan"], ["-fsanitize=address"]) 161 | 162 | # leak checker is currently too picky even for standard libs 163 | props.env["ASAN_OPTIONS"] += ",detect_leaks=0" 164 | 165 | # detect usage of already destroyed stack variables 166 | props.env["ASAN_OPTIONS"] += ",detect_stack_use_after_return=1" 167 | 168 | # -fsanitize=address does not seem to be supported with -static 169 | self.flags.remove_flags(["-static"]) 170 | 171 | # preload ASAN for %check 172 | def run_asan_hook(results, mock, props): 173 | cmd = "echo /usr/lib64/libasan.so.*.* > /etc/ld.so.preload" 174 | rv = mock.exec_chroot_cmd(cmd) 175 | if 0 != rv: 176 | results.error(f"ASAN plug-in: ASAN preloading failed with exit code {rv}") 177 | return rv 178 | 179 | # FIXME: hack 180 | extra_env = {"ASAN_OPTIONS": props.env["ASAN_OPTIONS"] + ",verify_asan_link_order=0"} 181 | ec = mock.exec_rpmbuild_bi(props, extra_env=extra_env) 182 | if 0 != ec: 183 | results.error(f"ASAN plug-in: %install or %check failed with exit code {rv}") 184 | return ec 185 | 186 | props.post_install_hooks += [run_asan_hook] 187 | 188 | if args.gcc_sanitize_leak: 189 | self.enable_sanitize(props, ["liblsan"], ["-fsanitize=leak"]) 190 | 191 | # execute %check section 192 | props.run_check = True 193 | 194 | # -fsanitize=leak does not seem to work well with -static 195 | self.flags.remove_flags(["-static"]) 196 | 197 | if args.gcc_sanitize_thread: 198 | self.enable_sanitize(props, ["libtsan"], ["-fsanitize=thread"]) 199 | 200 | # execute %check section 201 | props.run_check = True 202 | 203 | # -fsanitize=thread does not seem to be supported with -static 204 | self.flags.remove_flags(["-static"]) 205 | 206 | if args.gcc_sanitize_undefined: 207 | self.enable_sanitize(props, ["libubsan", "libubsan-static"], ["-fsanitize=undefined"]) 208 | 209 | # execute %check section 210 | props.run_check = True 211 | 212 | # print full stack traces 213 | props.env["UBSAN_OPTIONS"] += ",print_stacktrace=1" 214 | 215 | # serialize custom compiler flags 216 | if self.flags.append_custom_flags(args): 217 | self.enable() 218 | 219 | if not self.enabled: 220 | # drop COMPILER_WARNING defects mistakenly enabled by other plug-ins 221 | props.cswrap_filters += \ 222 | ["csgrep --mode=json --invert-match --checker COMPILER_WARNING"] 223 | return 224 | 225 | if self.sanitize: 226 | if "valgrind" in props.install_pkgs: 227 | parser.error("GCC sanitizers are not compatible with valgrind") 228 | 229 | # %check should not be enabled for ASAN 230 | if args.gcc_sanitize_address: 231 | assert not props.run_check 232 | 233 | self.flags.append_flags(['-g', '-fno-omit-frame-pointer', 234 | '-fsanitize-recover=all']) 235 | 236 | # sanitizers are not compatible with _FORTIFY_SOURCE 237 | # https://github.com/google/sanitizers/issues/247#issuecomment-1283500316 238 | self.flags.remove_flags(["-D_FORTIFY_SOURCE=*"]) 239 | 240 | # silence annobin's complaints about missing _FORTIFY_SOURCE defines 241 | props.rpm_opts += ["--undefine", "_annotated_build"] 242 | 243 | # create directory for sanitizer's results 244 | def create_cap_dir_hook(results, mock): 245 | cmd = f"mkdir -pv '{SANITIZER_CAPTURE_DIR}'" 246 | return mock.exec_mockbuild_cmd(cmd) 247 | props.post_depinst_hooks += [create_cap_dir_hook] 248 | 249 | # pick the captured files when %check is complete 250 | props.copy_out_files += [SANITIZER_CAPTURE_DIR] 251 | 252 | # make sure gcc is installed in the chroot 253 | props.install_pkgs += ["gcc"] 254 | 255 | props.enable_cswrap() 256 | props.cswrap_filters += \ 257 | ["csgrep --mode=json --invert-match --checker COMPILER_WARNING --event error"] 258 | 259 | # write all compiler flags to the environment 260 | self.flags.write_to_env(props.env) 261 | 262 | csmock.common.util.install_default_toolver_hook(props, "gcc") 263 | 264 | if self.csgcca_path is not None: 265 | def csgcca_hook(results, mock): 266 | analyzer_bin = args.gcc_analyzer_bin if args.gcc_analyzer_bin else "gcc" 267 | cmd = "echo 'int main() {}'" 268 | cmd += " | %s -xc - -c -o /dev/null" % analyzer_bin 269 | cmd += " -fanalyzer -fdiagnostics-path-format=separate-events" 270 | if 0 != mock.exec_mockbuild_cmd(cmd): 271 | results.error("`%s -fanalyzer` does not seem to work, " 272 | "disabling the tool" % analyzer_bin, ec=0) 273 | return 0 274 | 275 | # use -fdiagnostics-text-art-charset=none if supported by gcc in the chroot 276 | flag = "-fdiagnostics-text-art-charset=none" 277 | if 0 == mock.exec_mockbuild_cmd(f"{cmd} {flag}"): 278 | props.env["CSGCCA_ADD_OPTS"] = flag 279 | 280 | if args.gcc_analyzer_bin: 281 | # create an executable shell script to wrap the custom gcc binary 282 | wrapper_script = CSMOCK_GCC_WRAPPER_TEMPLATE % analyzer_bin 283 | cmd = "echo '%s' > %s && chmod 755 %s" % \ 284 | (wrapper_script, 285 | CSMOCK_GCC_WRAPPER_PATH, 286 | CSMOCK_GCC_WRAPPER_PATH) 287 | rv = mock.exec_chroot_cmd(cmd) 288 | if 0 != rv: 289 | results.error("failed to create csmock gcc wrapper script") 290 | return rv 291 | 292 | # wrap the shell script by cswrap to capture output of `gcc -fanalyzer` 293 | cmd = "ln -sf ../../bin/cswrap %s/%s" % \ 294 | (props.cswrap_path, CSMOCK_GCC_WRAPPER_NAME) 295 | rv = mock.exec_chroot_cmd(cmd) 296 | if 0 != rv: 297 | results.error("failed to create csmock gcc wrapper symlink") 298 | return rv 299 | 300 | # tell csgcca to use the wrapped script rather than system gcc analyzer 301 | props.env["CSGCCA_ANALYZER_BIN"] = CSMOCK_GCC_WRAPPER_NAME 302 | 303 | # create directory for gcc results 304 | def create_gcc_results_dir_hook(_, mock): 305 | cmd = f"mkdir -pv '{GCC_RESULTS_DIR}' && touch '{GCC_RESULTS_DIR}/empty.sarif'" 306 | return mock.exec_mockbuild_cmd(cmd) 307 | props.post_depinst_hooks += [create_gcc_results_dir_hook] 308 | 309 | # copy gcc results out of the chroot 310 | props.copy_out_files += [GCC_RESULTS_DIR] 311 | 312 | # process all captured SARIF files 313 | def filter_hook(results): 314 | src = os.path.join(results.dbgdir_raw, GCC_RESULTS_DIR[1:]) 315 | dst = os.path.join(results.dbgdir_uni, "gcc-results.json") 316 | cmd = f'{FILTER_CMD} --file-glob "{src}/*.sarif" > "{dst}"' 317 | return results.exec_cmd(cmd, shell=True) 318 | props.post_process_hooks += [filter_hook] 319 | 320 | # XXX: changing props this way is extremely fragile 321 | # insert csgcca right before cswrap to avoid chaining 322 | # csclng/cscppc while invoking `gcc -fanalyzer` 323 | idx_cswrap = 0 324 | for idx in range(0, len(props.path)): 325 | if props.path[idx].endswith("/cswrap"): 326 | idx_cswrap = idx 327 | break 328 | props.path.insert(idx_cswrap, self.csgcca_path) 329 | 330 | props.env["CSWRAP_TIMEOUT_FOR"] += ":%s" % (CSMOCK_GCC_WRAPPER_NAME if args.gcc_analyzer_bin else "gcc") 331 | if args.gcc_analyze_add_flag: 332 | # propagate custom GCC analyzer flags 333 | props.env["CSGCCA_ADD_OPTS"] = csmock.common.cflags.serialize_flags(args.gcc_analyze_add_flag) 334 | 335 | # record that `gcc -fanalyzer` was used for this scan 336 | cmd = mock.get_mock_cmd(["--chroot", "%s --version" % analyzer_bin]) 337 | (rc, ver) = results.get_cmd_output(cmd, shell=False) 338 | if rc != 0: 339 | return rc 340 | ver = ver.partition('\n')[0].strip() 341 | ver = ver.split(' ')[2] 342 | csmock.common.util.write_toolver(results.ini_writer, "gcc-analyzer", ver) 343 | 344 | return 0 345 | 346 | props.post_depinst_hooks += [csgcca_hook] 347 | 348 | # transform log files produced by UBSAN into csdiff format 349 | if args.gcc_sanitize_undefined: 350 | def ubsan_filter_hook(results): 351 | src_dir = results.dbgdir_raw + SANITIZER_CAPTURE_DIR 352 | 353 | # ensure we have permission to read all capture files 354 | results.exec_cmd(['chmod', '-R', '+r', src_dir]) 355 | 356 | # `cd` first to avoid `csgrep: Argument list too long` error on glob expansion 357 | dst = f"{results.dbgdir_uni}/ubsan-capture.js" 358 | cmd = f"cd '{src_dir}' && touch ubsan.empty && csgrep --mode=json --remove-duplicates ubsan.* > '{dst}'" 359 | return results.exec_cmd(cmd, shell=True) 360 | 361 | props.post_process_hooks += [ubsan_filter_hook] 362 | -------------------------------------------------------------------------------- /csmock/plugins/gitleaks.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | import os 19 | import re 20 | import shutil 21 | 22 | import csmock.common.util 23 | 24 | 25 | # default URL to download gitleaks binary executable (in a .tar.gz) from 26 | GITLEAKS_BIN_URL = "https://github.com/zricethezav/gitleaks/releases/download/v8.15.1/gitleaks_8.15.1_linux_x64.tar.gz" 27 | 28 | # default directory where downloaded Gitleaks tarballs are cached across runs 29 | GITLEAKS_CACHE_DIR = "/var/tmp/csmock/gitleaks" 30 | 31 | GITLEAKS_SCAN_DIR = "/builddir/build/BUILD" 32 | 33 | GITLEAKS_OUTPUT = "/builddir/gitleaks-capture.sarif" 34 | 35 | GITLEAKS_LOG = "/builddir/gitleaks-capture.log" 36 | 37 | FILTER_CMD = "csgrep '%s' --mode=json --warning-rate-limit=%i --limit-msg-len=%i > '%s'" 38 | 39 | 40 | class PluginProps: 41 | def __init__(self): 42 | self.description = "Tool for finding secrets in source code." 43 | 44 | # include this plug-in in `csmock --all-tools` 45 | self.stable = True 46 | 47 | 48 | class Plugin: 49 | def __init__(self): 50 | self.enabled = False 51 | 52 | def get_props(self): 53 | return PluginProps() 54 | 55 | def enable(self): 56 | self.enabled = True 57 | 58 | def init_parser(self, parser): 59 | parser.add_argument( 60 | "--gitleaks-bin-url", default=GITLEAKS_BIN_URL, 61 | help="URL to download gitleaks binary executable (in a .tar.gz) from") 62 | 63 | parser.add_argument( 64 | "--gitleaks-cache-dir", default=GITLEAKS_CACHE_DIR, 65 | help="directory where downloaded Gitleaks tarballs are cached across runs") 66 | 67 | parser.add_argument( 68 | "--gitleaks-config", 69 | help="local configuration file to be used for gitleaks") 70 | 71 | parser.add_argument( 72 | "--gitleaks-rate-limit", type=int, default=1024, 73 | help="drop warnings if their count exceeds the specified limit") 74 | 75 | parser.add_argument( 76 | "--gitleaks-limit-msg-len", type=int, default=512, 77 | help="trim message if it exceeds max message length") 78 | 79 | parser.add_argument( 80 | "--gitleaks-refresh", action="store_true", 81 | help="force download of gitleaks binary executable (in a .tar.gz) from") 82 | 83 | def handle_args(self, parser, args, props): 84 | if args.gitleaks_config is not None: 85 | self.enable() 86 | 87 | if not self.enabled: 88 | return 89 | 90 | # fetch gitleaks using the given URL 91 | def fetch_gitleaks_hook(results, props): 92 | cache_dir = args.gitleaks_cache_dir 93 | try: 94 | # make sure the cache directory exists 95 | os.makedirs(cache_dir, mode=0o755, exist_ok=True) 96 | except OSError: 97 | results.error("failed to create gitleaks cache directory: %s" % cache_dir) 98 | return 1 99 | 100 | url = args.gitleaks_bin_url 101 | gitleaks_tgz_name = url.split("/")[-1] 102 | gitleaks_tgz = os.path.join(cache_dir, gitleaks_tgz_name) 103 | 104 | if not args.gitleaks_refresh and os.path.exists(gitleaks_tgz): 105 | results.print_with_ts("reusing previously downloaded gitleaks tarball: %s" % gitleaks_tgz) 106 | else: 107 | # fetch .tar.gz 108 | ec = results.exec_cmd(['curl', '-Lfso', gitleaks_tgz, url]) 109 | if 0 != ec: 110 | results.error("failed to download gitleaks binary executable: %s" % url) 111 | return ec 112 | 113 | # extract the binary executable 114 | ec = results.exec_cmd(['tar', '-C', results.tmpdir, '-xvzf', gitleaks_tgz, 'gitleaks']) 115 | if 0 != ec: 116 | results.error("failed to extract gitleaks binary executable from .tar.gz: %s" % url) 117 | return ec 118 | 119 | # check whether we have eXecute access 120 | gitleaks_bin = os.path.join(results.tmpdir, "gitleaks") 121 | if not os.access(gitleaks_bin, os.X_OK): 122 | results.error("gitleaks binary is not executable: %s" % gitleaks_bin) 123 | return 2 124 | 125 | # query version of gitleaks 126 | (ec, out) = results.get_cmd_output([gitleaks_bin, 'version'], shell=False) 127 | if 0 != ec: 128 | results.error("failed to query gitleaks version", ec=ec) 129 | return ec 130 | 131 | ver = re.sub("^v", "", out) 132 | results.ini_writer.append("analyzer-version-gitleaks", ver) 133 | 134 | props.copy_in_files += [gitleaks_bin] 135 | cmd = f"{gitleaks_bin} detect --no-git cmd --source={GITLEAKS_SCAN_DIR}" \ 136 | f" --report-path={GITLEAKS_OUTPUT} --report-format=sarif" 137 | props.copy_out_files += [GITLEAKS_OUTPUT, GITLEAKS_LOG] 138 | 139 | if args.gitleaks_config is not None: 140 | gitleaks_config = "%s/gitleaks-config.js" % results.tmpdir 141 | shutil.copyfile(args.gitleaks_config, gitleaks_config) 142 | props.copy_in_files += [gitleaks_config] 143 | cmd += " --config-path=%s" % gitleaks_config 144 | 145 | cmd += " 2>%s" % GITLEAKS_LOG 146 | props.post_build_chroot_cmds += [cmd] 147 | return 0 148 | 149 | props.pre_mock_hooks += [fetch_gitleaks_hook] 150 | 151 | def filter_hook(results): 152 | src = results.dbgdir_raw + GITLEAKS_OUTPUT 153 | dst = "%s/gitleaks-capture.js" % results.dbgdir_uni 154 | cmd = FILTER_CMD % (src, args.gitleaks_rate_limit, args.gitleaks_limit_msg_len, dst) 155 | return results.exec_cmd(cmd, shell=True) 156 | 157 | props.post_process_hooks += [filter_hook] 158 | -------------------------------------------------------------------------------- /csmock/plugins/infer.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | 4 | import csmock.common.util 5 | 6 | 7 | INFER_RESULTS_FILTER_SCRIPT = "/usr/share/csmock/scripts/filter-infer.py" 8 | INFER_INSTALL_SCRIPT = "/usr/share/csmock/scripts/install-infer.sh" 9 | INFER_RESULTS = "/builddir/infer-results.txt" 10 | INFER_OUT_DIR = "/builddir/infer-out" 11 | DEFAULT_INFER_TIMEOUT = 300 12 | 13 | class PluginProps: 14 | def __init__(self): 15 | self.description = "Static analysis tool for Java/C/C++ code." 16 | 17 | 18 | class Plugin: 19 | def __init__(self): 20 | self.enabled = False 21 | 22 | def get_props(self): 23 | return PluginProps() 24 | 25 | def enable(self): 26 | self.enabled = True 27 | 28 | def init_parser(self, parser): 29 | parser.add_argument( 30 | "--infer-analyze-add-flag", action="append", default=[], 31 | help="appends the given flag (except '-o') when invoking 'infer analyze' \ 32 | (can be used multiple times)(default flags '--bufferoverrun', '--pulse')") 33 | 34 | parser.add_argument( 35 | "--infer-archive-path", default="", 36 | help="use the given archive to install Infer (default is /opt/infer-linux*.tar.xz)") 37 | 38 | csmock.common.util.add_paired_flag( 39 | parser, "infer-filter", 40 | help="apply false positive filter (enabled by default)") 41 | 42 | csmock.common.util.add_paired_flag( 43 | parser, "infer-biabduction-filter", 44 | help="apply false positive bi-abduction filter (enabled by default)") 45 | 46 | csmock.common.util.add_paired_flag( 47 | parser, "infer-inferbo-filter", 48 | help="apply false positive inferbo filter (enabled by default)") 49 | 50 | csmock.common.util.add_paired_flag( 51 | parser, "infer-uninit-filter", 52 | help="apply false positive uninit filter (enabled by default)") 53 | 54 | csmock.common.util.add_paired_flag( 55 | parser, "infer-dead-store-severity", 56 | help="lower dead store severity (enabled by default)") 57 | 58 | parser.add_argument( 59 | "--infer-timeout", type=int, default=DEFAULT_INFER_TIMEOUT, 60 | help="maximal amount of time taken by Infer's analysis phase [s] (default 300)") 61 | 62 | def handle_args(self, parser, args, props): 63 | if not self.enabled: 64 | return 65 | 66 | # python3 -- for the Infer's reporting module 67 | # ncurses-compat-libs -- libtinfo.so.5 68 | # ncurses-libs -- libtinfo.so.6 69 | props.install_pkgs += ["python3", "ncurses-compat-libs", "ncurses-libs"] 70 | 71 | infer_archive_path = "" 72 | 73 | if args.infer_archive_path: 74 | # use the archive specified with --infer-archive-path 75 | if os.path.isfile(args.infer_archive_path): 76 | infer_archive_path = os.path.abspath(args.infer_archive_path) 77 | else: 78 | parser.error(f"--infer-archive-path: given path \"{args.infer_archive_path}\" doesn't exist") 79 | else: 80 | # search for the archive in /opt 81 | for file in os.listdir("/opt"): 82 | if re.search(r"infer-linux.*\.tar\.xz", file): 83 | infer_archive_path = "/opt/" + file 84 | if not infer_archive_path: 85 | parser.error('The Infer plugin requires an archive with the binary release of Infer. ' 86 | 'The archive can be downloaded from https://github.com/facebook/infer/releases. ' 87 | 'By default, the plugin looks for the archive in /opt/infer-linux*.tar.xz. ' 88 | 'Alternatively, the "--infer-archive-path INFER_ARCHIVE_PATH" option can be ' 89 | 'used to specify the path to the archive.') 90 | 91 | props.copy_in_files += [infer_archive_path] 92 | 93 | # install Infer and wrappers for a Infer's capture phase 94 | install_cmd = f"{INFER_INSTALL_SCRIPT} {infer_archive_path} {INFER_OUT_DIR}" 95 | def install_infer_hook(results, mock): 96 | return mock.exec_chroot_cmd(install_cmd) 97 | props.post_depinst_hooks += [install_infer_hook] 98 | 99 | # run an Infer's analysis phase 100 | infer_analyze_flags = "--pulse --bufferoverrun" 101 | if args.infer_analyze_add_flag: 102 | infer_analyze_flags = csmock.common.cflags.serialize_flags(args.infer_analyze_add_flag, separator=" ") 103 | run_cmd = f"/usr/bin/timeout {args.infer_timeout} infer analyze --keep-going {infer_analyze_flags} -o {INFER_OUT_DIR}" 104 | 105 | filter_args = [] 106 | 107 | if getattr(args, "infer_filter") == False: 108 | filter_args += ["--only-transform"] 109 | 110 | if getattr(args, "infer_biabduction_filter") == False: 111 | filter_args += ["--no-biabduction"] 112 | 113 | if getattr(args, "infer_inferbo_filter") == False: 114 | filter_args += ["--no-inferbo"] 115 | 116 | if getattr(args, "infer_uninit_filter") == False: 117 | filter_args += ["--no-uninit"] 118 | 119 | if getattr(args, "infer_dead_store_severity") == False: 120 | filter_args += ["--no-dead-store"] 121 | 122 | 123 | filter_args_serialized = csmock.common.cflags.serialize_flags(filter_args, separator=" ") 124 | 125 | # the filter script tries to filter out false positives and transforms results into csdiff compatible format 126 | filter_cmd = f"python3 {INFER_RESULTS_FILTER_SCRIPT} {filter_args_serialized}" \ 127 | f" < {INFER_OUT_DIR}/report.json > {INFER_RESULTS}" 128 | 129 | props.copy_out_files += [INFER_RESULTS] 130 | 131 | def run_analysis_hook(results, mock, props): 132 | rc = mock.exec_chroot_cmd(run_cmd, quiet=False) 133 | if rc == 124: 134 | results.error("The timeout for the Infer's analysis phase has expired. " 135 | 'Try increasing the timeout with "--infer-timeout INFER_TIMEOUT". ' 136 | "Alternatively, try disabling the Infer's InferBO plugin.") 137 | return rc 138 | 139 | return mock.exec_chroot_cmd(filter_cmd, quiet=False) 140 | 141 | props.post_install_hooks += [run_analysis_hook] 142 | 143 | def filter_hook(results): 144 | src = results.dbgdir_raw + INFER_RESULTS 145 | dst = f"{results.dbgdir_uni}/infer-results.err" 146 | cmd = f"csgrep --quiet {src} > {dst}" 147 | return results.exec_cmd(cmd, shell=True, echo=True) 148 | 149 | props.post_process_hooks += [filter_hook] 150 | -------------------------------------------------------------------------------- /csmock/plugins/pylint.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | import csmock.common.util 19 | 20 | 21 | RUN_PYLINT_SH = "/usr/share/csmock/scripts/run-pylint.sh" 22 | 23 | PYLINT_CAPTURE = "/builddir/pylint-capture.err" 24 | 25 | FILTER_CMD = "csgrep --quiet '%s' " \ 26 | "| csgrep --event '%s' " \ 27 | "> '%s'" 28 | 29 | 30 | class PluginProps: 31 | def __init__(self): 32 | self.description = "Python source code analyzer which looks for programming errors." 33 | 34 | 35 | class Plugin: 36 | def __init__(self): 37 | self.enabled = False 38 | 39 | def get_props(self): 40 | return PluginProps() 41 | 42 | def enable(self): 43 | self.enabled = True 44 | 45 | def init_parser(self, parser): 46 | csmock.common.util.install_script_scan_opts(parser, "pylint") 47 | parser.add_argument( 48 | "--pylint-evt-filter", default="^W[0-9]+", 49 | help="filter out Pylint defects whose key event matches the given regex \ 50 | (defaults to '^W[0-9]+', use '.*' to get all defects detected by Pylint)") 51 | 52 | def handle_args(self, parser, args, props): 53 | if not self.enabled: 54 | return 55 | 56 | # which directories are we going to scan (build and/or install) 57 | dirs_to_scan = csmock.common.util.dirs_to_scan_by_args( 58 | parser, args, props, "pylint") 59 | 60 | props.install_pkgs += ["pylint"] 61 | cmd = f"shopt -s nullglob && {RUN_PYLINT_SH} {dirs_to_scan} > {PYLINT_CAPTURE}" 62 | props.post_build_chroot_cmds += [cmd] 63 | props.copy_out_files += [PYLINT_CAPTURE] 64 | 65 | csmock.common.util.install_default_toolver_hook(props, "pylint") 66 | 67 | def filter_hook(results): 68 | src = results.dbgdir_raw + PYLINT_CAPTURE 69 | dst = "%s/pylint-capture.err" % results.dbgdir_uni 70 | cmd = FILTER_CMD % (src, args.pylint_evt_filter, dst) 71 | return results.exec_cmd(cmd, shell=True) 72 | 73 | props.post_process_hooks += [filter_hook] 74 | -------------------------------------------------------------------------------- /csmock/plugins/semgrep.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | """ 19 | Semgrep client plugin 20 | """ 21 | import os 22 | 23 | from csmock.common.util import sanitize_opts_arg # pylint: disable=import-error 24 | 25 | 26 | # disable metrics to be sent to semgrep cloud 27 | DEFAULT_SEMGREP_SEND_METRICS = "off" 28 | 29 | # This plug-in is pinned to a specific version of Semgrep because newer Semgrep 30 | # versions break compatibility with externally maintained Semgrep rules. See 31 | # https://github.com/semgrep/semgrep/releases for available releases. 32 | SEMGREP_CLI_VERSION = "1.56.0" 33 | 34 | SEMGREP_SCAN_DIR = "/builddir/build/BUILD" 35 | 36 | SEMGREP_SCAN_OUTPUT = "/builddir/semgrep-scan-results.sarif" 37 | 38 | SEMGREP_SCAN_LOG = "/builddir/semgrep-scan.log" 39 | 40 | 41 | class PluginProps: # pylint: disable=too-few-public-methods 42 | """ 43 | Props of the plugin 44 | """ 45 | def __init__(self): 46 | self.description = ( 47 | "A fast, open-source, static analysis engine for finding bugs, " 48 | "detecting dependency vulnerabilities, and enforcing code standards." 49 | ) 50 | 51 | self.stable = False 52 | 53 | 54 | class Plugin: 55 | """ 56 | Semgrep static analysis engine plugin 57 | """ 58 | def __init__(self): 59 | self.enabled = False 60 | self.semgrep_scan_opts = None 61 | self.mock_root = None 62 | 63 | def get_props(self): # pylint: disable=missing-function-docstring 64 | return PluginProps() 65 | 66 | def enable(self): # pylint: disable=missing-function-docstring 67 | self.enabled = True 68 | 69 | def init_parser(self, parser): 70 | """ 71 | Initialize the argument parser for the Semgrep plugin. 72 | """ 73 | parser.add_argument( 74 | "--semgrep-metrics", 75 | default=DEFAULT_SEMGREP_SEND_METRICS, 76 | help=f"configure whether usage metrics are sent to the Semgrep server (defaults to {DEFAULT_SEMGREP_SEND_METRICS})", 77 | ) 78 | 79 | parser.add_argument( 80 | "--semgrep-rules-repo", 81 | help="semgrep rules repo, assuming rules are located under the 'rules' sub-directory", 82 | ) 83 | 84 | parser.add_argument( 85 | "--semgrep-verbose", 86 | action="store_true", 87 | help="show more details about what rules are running, which files failed to parse, etc.", 88 | ) 89 | 90 | parser.add_argument( 91 | "--semgrep-scan-opts", 92 | help="space-separated list of additional options passed to the 'semgrep scan' command", 93 | ) 94 | 95 | def handle_args(self, parser, args, props): # pylint: disable=too-many-statements,missing-function-docstring 96 | if not self.enabled: 97 | return 98 | 99 | if not args.semgrep_rules_repo: 100 | parser.error("'--semgrep-rules-repo' is required to run semgrep scan") 101 | 102 | # sanitize options passed to --semgrep-scan-opts to avoid shell injection 103 | self.semgrep_scan_opts = sanitize_opts_arg(parser, args, "--semgrep-scan-opts") 104 | 105 | # install semgrep cli and download semgrep rules 106 | def prepare_semgrep_runtime_hook(results, props): 107 | # target dir where semgrep cli and its dependencies are installed 108 | semgrep_lib_dir = os.path.join(results.tmpdir, "semgrep_lib") 109 | try: 110 | # make sure the lib directory exists 111 | os.makedirs(semgrep_lib_dir, mode=0o755, exist_ok=True) 112 | except OSError: 113 | results.error("failed to create semgrep lib directory") 114 | return 1 115 | 116 | # install semgrep cli using pip 117 | cmd = f"python3 -m pip install --target={semgrep_lib_dir} semgrep=={SEMGREP_CLI_VERSION}" 118 | ec = results.exec_cmd(cmd, shell=True) 119 | if 0 != ec: 120 | results.error("failed to install semgrep cli using pip") 121 | return 1 122 | 123 | semgrep_prefix = f"env PATH={semgrep_lib_dir}/bin:$PATH, PYTHONPATH={semgrep_lib_dir}" 124 | 125 | semgrep_rules_repo_dir = os.path.join(results.tmpdir, "semgrep_rules") 126 | repo_clone_cmd = [ 127 | "git", "clone", "--depth", "1", 128 | args.semgrep_rules_repo, 129 | semgrep_rules_repo_dir 130 | ] 131 | ec = results.exec_cmd(repo_clone_cmd) 132 | if 0 != ec: 133 | results.error("failed to download semgrep rules") 134 | return ec 135 | # query version of semgrep 136 | cmd = semgrep_prefix + " semgrep --version" 137 | ec, output = results.get_cmd_output(cmd) 138 | if 0 != ec: 139 | results.error("failed to query semgrep cli version", ec=ec) 140 | return ec 141 | 142 | # parse and record the version of semgrep cli 143 | version = output.rstrip("\n") 144 | results.ini_writer.append("analyzer-version-semgrep-cli", version) 145 | 146 | # get the results out of the chroot 147 | props.copy_out_files += [ 148 | SEMGREP_SCAN_OUTPUT, 149 | SEMGREP_SCAN_LOG, 150 | ] 151 | return 0 152 | 153 | props.pre_mock_hooks += [prepare_semgrep_runtime_hook] 154 | 155 | def scan_hook(results, mock, props): # pylint: disable=unused-argument 156 | semgrep_lib_dir = os.path.join(results.tmpdir, "semgrep_lib") 157 | semgrep_prefix = f"env PATH={semgrep_lib_dir}/bin:$PATH PYTHONPATH={semgrep_lib_dir}" 158 | # assuming semgrep rules are located under the 'rules' directory 159 | semgrep_rules_dir = os.path.join(results.tmpdir, "semgrep_rules/rules") 160 | 161 | # record the mock_root path (without the trailing /) because we need it in filter_hook 162 | self.mock_root = mock.mock_root.rstrip("/") 163 | 164 | # command to run semgrep scan 165 | semgrep_scan_cmd = semgrep_prefix + ( 166 | f" semgrep scan --metrics={args.semgrep_metrics} --sarif" 167 | f" --config={semgrep_rules_dir}" 168 | ) 169 | if args.semgrep_verbose: 170 | semgrep_scan_cmd += " --verbose" 171 | 172 | # append additional options passed to the 'semgrep scan' command 173 | if self.semgrep_scan_opts: 174 | semgrep_scan_cmd += f" {self.semgrep_scan_opts}" 175 | 176 | # eventually append the target directory to be scanned 177 | semgrep_scan_cmd += ( 178 | f" --output={mock.mock_root}{SEMGREP_SCAN_OUTPUT} {self.mock_root}{SEMGREP_SCAN_DIR}" 179 | f" 2>{mock.mock_root}{SEMGREP_SCAN_LOG}" 180 | ) 181 | # run semgrep scan 182 | ec = results.exec_cmd(semgrep_scan_cmd, shell=True) 183 | 184 | # according to semgrep cli scan doc, below are the known error codes 185 | error_messages = { 186 | 123: "Indiscriminate errors reported on standard error.", 187 | 124: "Command line parsing errors.", 188 | 125: "Unexpected internal errors (bugs)." 189 | } 190 | 191 | if ec in error_messages: 192 | results.error(f"semgrep: {error_messages[ec]} Command: {semgrep_scan_cmd}") 193 | elif ec != 0: 194 | results.error(f"semgrep: Scan failed with exit code {ec}. Command: {semgrep_scan_cmd}") 195 | 196 | return 0 197 | 198 | # run semgrep scan after successful build 199 | props.post_install_hooks += [scan_hook] 200 | 201 | # convert the results into the csdiff's JSON format 202 | def filter_hook(results): 203 | src = results.dbgdir_raw + SEMGREP_SCAN_OUTPUT 204 | if not os.path.exists(src): 205 | return 0 206 | dst = f"{results.dbgdir_uni}/semgrep-scan-results.json" 207 | 208 | tmp_dir_basename = results.tmpdir.split("/")[-1] 209 | semgrep_rules_path_prefix = f"{tmp_dir_basename}/semgrep_rules/" 210 | # semgrep report has dot-separated rules path 211 | tmp_path = semgrep_rules_path_prefix.lstrip("/").replace("/", r"\.") 212 | # strip suspicious path prefix from the semgrep rules directory 213 | # depending on where the semgrep scan process is run, the raw report may or may not contain "/tmp" 214 | # in its rules path. The following sed command strips suspicious path prefixes by removing 215 | # any sequence of non left-square-bracket characters preceding '{tmp_path}' 216 | cmd = ( 217 | fr"csgrep {src} --mode=json --strip-path-prefix {self.mock_root} " 218 | fr"| sed 's|[^\[]*{tmp_path}||' > {dst}" # pylint: disable=W1401 219 | ) 220 | 221 | return results.exec_cmd(cmd, shell=True) 222 | 223 | props.post_process_hooks += [filter_hook] 224 | -------------------------------------------------------------------------------- /csmock/plugins/shellcheck.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | import os 19 | 20 | import csmock.common.util 21 | 22 | 23 | RUN_SHELLCHECK_SH = "/usr/share/csmock/scripts/run-shellcheck.sh" 24 | 25 | SHELLCHECK_CAP_DIR = "/builddir/shellcheck-results" 26 | 27 | FILTER_CMD = "csgrep --mode=json --remove-duplicates --quiet " \ 28 | "--invert-match --event '^info|style|warning\\[SC1090\\]'" 29 | 30 | # default maximum number of scripts scanned by a single shellcheck process 31 | DEFAULT_SC_BATCH = 1 32 | 33 | # default maximum amount of wall-clock time taken by a single shellcheck process [s] 34 | DEFAULT_SC_TIMEOUT = 30 35 | 36 | 37 | class PluginProps: 38 | def __init__(self): 39 | self.description = "A static analysis tool that gives warnings and suggestions for bash/sh shell scripts." 40 | 41 | # include this plug-in in `csmock --all-tools` 42 | self.stable = True 43 | 44 | 45 | class Plugin: 46 | def __init__(self): 47 | self.enabled = False 48 | 49 | def get_props(self): 50 | return PluginProps() 51 | 52 | def enable(self): 53 | self.enabled = True 54 | 55 | def init_parser(self, parser): 56 | csmock.common.util.install_script_scan_opts(parser, "shellcheck") 57 | parser.add_argument( 58 | "--shellcheck-batch", type=int, default=DEFAULT_SC_BATCH, 59 | help="maximum number of scripts scanned by a single shellcheck process" \ 60 | f" (defaults to {DEFAULT_SC_BATCH})") 61 | parser.add_argument( 62 | "--shellcheck-timeout", type=int, default=DEFAULT_SC_TIMEOUT, 63 | help="maximum amount of wall-clock time taken by a single shellcheck process [s]" \ 64 | f" (defaults to {DEFAULT_SC_TIMEOUT})") 65 | 66 | def handle_args(self, parser, args, props): 67 | if not self.enabled: 68 | return 69 | 70 | # which directories are we going to scan (build and/or install) 71 | dirs_to_scan = csmock.common.util.dirs_to_scan_by_args( 72 | parser, args, props, "shellcheck") 73 | 74 | # append "/*" to each directory in dirs_to_scan (to scan pkg-specific dirs) 75 | dirs_to_scan = " ".join([dir + "/*" for dir in dirs_to_scan.split()]) 76 | 77 | props.install_pkgs += ["ShellCheck"] 78 | cmd = "shopt -s nullglob && " 79 | cmd += f"SC_RESULTS_DIR={SHELLCHECK_CAP_DIR} " 80 | cmd += f"SC_BATCH={args.shellcheck_batch} " 81 | cmd += f"SC_TIMEOUT={args.shellcheck_timeout} " 82 | cmd += f"{RUN_SHELLCHECK_SH} {dirs_to_scan}" 83 | props.post_build_chroot_cmds += [cmd] 84 | props.copy_out_files += [SHELLCHECK_CAP_DIR] 85 | 86 | csmock.common.util.install_default_toolver_hook(props, "ShellCheck") 87 | 88 | def filter_hook(results): 89 | src = os.path.join(results.dbgdir_raw, SHELLCHECK_CAP_DIR[1:]) 90 | dst = os.path.join(results.dbgdir_uni, "shellcheck-capture.json") 91 | cmd = f"cd {src} && {FILTER_CMD} *.json > {dst}" 92 | return results.exec_cmd(cmd, shell=True) 93 | 94 | props.post_process_hooks += [filter_hook] 95 | -------------------------------------------------------------------------------- /csmock/plugins/smatch.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 - 2018 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | # standard imports 19 | import os 20 | import subprocess 21 | 22 | # local imports 23 | import csmock.common.util 24 | 25 | 26 | class PluginProps: 27 | def __init__(self): 28 | self.description = "Source code analysis for C, based on sparse." 29 | 30 | 31 | class Plugin: 32 | def __init__(self): 33 | self.enabled = False 34 | 35 | def get_props(self): 36 | return PluginProps() 37 | 38 | def enable(self): 39 | self.enabled = True 40 | 41 | def init_parser(self, parser): 42 | # TODO: introduce options to enable/disable checkers 43 | pass 44 | 45 | def handle_args(self, parser, args, props): 46 | if not self.enabled: 47 | return 48 | 49 | props.enable_cswrap() 50 | props.env["CSWRAP_TIMEOUT_FOR"] += ":smatch" 51 | props.cswrap_filters += \ 52 | ["csgrep --mode=json --invert-match --checker SMATCH_WARNING --event error"] 53 | 54 | props.install_pkgs += ["smatch"] 55 | 56 | # resolve csmatch_path by querying csmatch binary 57 | cmd = ["csmatch", "--print-path-to-wrap"] 58 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 59 | (out, err) = p.communicate() 60 | csmatch_path = out.decode("utf8").strip() 61 | 62 | props.path = [csmatch_path] + props.path 63 | props.copy_in_files += ["/usr/bin/csmatch", csmatch_path] 64 | 65 | csmock.common.util.install_default_toolver_hook(props, "smatch") 66 | -------------------------------------------------------------------------------- /csmock/plugins/snyk.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | import os 19 | 20 | from csmock.common.snyk import snyk_write_analysis_meta 21 | from csmock.common.util import sanitize_opts_arg 22 | 23 | 24 | # default URL to download snyk binary executable 25 | SNYK_BIN_URL = "https://static.snyk.io/cli/latest/snyk-linux" 26 | 27 | # default directory where downloaded snyk executable is cached across runs 28 | SNYK_CACHE_DIR = "/var/tmp/csmock/snyk" 29 | 30 | SNYK_SCAN_DIR = "/builddir/build/BUILD" 31 | 32 | SNYK_OUTPUT = "/builddir/snyk-results.sarif" 33 | 34 | SNYK_LOG = "/builddir/snyk-scan.log" 35 | 36 | FILTER_CMD = f"csgrep '%s' --mode=json --prepend-path-prefix={SNYK_SCAN_DIR}/ --remove-duplicates > '%s'" 37 | 38 | # default value for the maximum amount of time taken by invocation of Snyk (5 hours) 39 | DEFAULT_SNYK_TIMEOUT = 18000 40 | 41 | 42 | class PluginProps: 43 | def __init__(self): 44 | self.description = "Tool to find vulnerabilities in source code." 45 | 46 | 47 | class Plugin: 48 | def __init__(self): 49 | self.enabled = False 50 | self.auth_token_src = None 51 | self.snyk_bin = None 52 | 53 | def get_props(self): 54 | return PluginProps() 55 | 56 | def enable(self): 57 | self.enabled = True 58 | 59 | def init_parser(self, parser): 60 | parser.add_argument( 61 | "--snyk-bin-url", default=SNYK_BIN_URL, 62 | help="URL to download snyk binary executable") 63 | 64 | parser.add_argument( 65 | "--snyk-auth", default="~/.config/configstore/snyk.json", 66 | help="file containing snyk authentication token") 67 | 68 | parser.add_argument( 69 | "--snyk-cache-dir", default=SNYK_CACHE_DIR, 70 | help="directory where downloaded snyk tarballs are cached across runs") 71 | 72 | parser.add_argument( 73 | "--snyk-refresh", action="store_true", 74 | help="force download of snyk binary executable") 75 | 76 | parser.add_argument( 77 | "--snyk-timeout", type=int, default=DEFAULT_SNYK_TIMEOUT, 78 | help="maximum amount of time taken by invocation of Snyk [s]") 79 | 80 | parser.add_argument( 81 | "--snyk-code-test-opts", 82 | help="space-separated list of additional options passed to the 'snyk code test' command") 83 | 84 | def handle_args(self, parser, args, props): 85 | if not self.enabled: 86 | return 87 | 88 | # sanitize options passed to --snyk-code-test-opts to avoid shell injection 89 | self.snyk_code_test_opts = sanitize_opts_arg(parser, args, "--snyk-code-test-opts") 90 | 91 | # check whether we have access to snyk authentication token 92 | self.auth_token_src = os.path.expanduser(args.snyk_auth) 93 | if not os.access(self.auth_token_src, os.R_OK): 94 | parser.error("unable to read snyk authentication token: %s" % self.auth_token_src) 95 | 96 | # define all Snyk's findings with level `error` as important 97 | props.imp_checker_set.add("SNYK_CODE_WARNING") 98 | props.imp_csgrep_filters.append(("SNYK_CODE_WARNING", "--event=^error")) 99 | 100 | # fetch snyk using the given URL 101 | def fetch_snyk_hook(results, props): 102 | cache_dir = args.snyk_cache_dir 103 | try: 104 | # make sure the cache directory exists 105 | os.makedirs(cache_dir, mode=0o755, exist_ok=True) 106 | except OSError: 107 | results.error("failed to create snyk cache directory: %s" % cache_dir) 108 | return 1 109 | 110 | url = args.snyk_bin_url 111 | snyk_bin_name = url.split("/")[-1] 112 | self.snyk_bin = os.path.join(cache_dir, snyk_bin_name) 113 | 114 | if not args.snyk_refresh and os.path.exists(self.snyk_bin): 115 | results.print_with_ts("reusing previously downloaded snyk executable: " + self.snyk_bin) 116 | else: 117 | # fetch the binary executable 118 | ec = results.exec_cmd(['curl', '-Lfso', self.snyk_bin, url]) 119 | if 0 != ec: 120 | results.error("failed to download snyk binary executable: %s" % url) 121 | return ec 122 | 123 | # add eXecute permission on the downloaded file 124 | os.chmod(self.snyk_bin, 0o755) 125 | 126 | # check whether we have eXecute access 127 | if not os.access(self.snyk_bin, os.X_OK): 128 | results.error("snyk binary is not executable: %s" % self.snyk_bin) 129 | return 2 130 | 131 | # query version of snyk 132 | (ec, out) = results.get_cmd_output([self.snyk_bin, 'version'], shell=False) 133 | if 0 != ec: 134 | results.error("failed to query snyk version", ec=ec) 135 | return ec 136 | 137 | # parse and record the version of snyk 138 | ver = out.split(" ")[0] 139 | results.ini_writer.append("analyzer-version-snyk-code", ver) 140 | 141 | # copy snyk binary into the chroot 142 | props.copy_in_files += [self.snyk_bin] 143 | 144 | # get the results out of the chroot 145 | props.copy_out_files += [SNYK_OUTPUT, SNYK_LOG] 146 | return 0 147 | 148 | # fetch snyk binary executable before initializing the buildroot 149 | props.pre_mock_hooks += [fetch_snyk_hook] 150 | 151 | def scan_hook(results, mock, props): 152 | # copy snyk authentication token into the chroot 153 | dst_dir = "/builddir/.config/configstore" 154 | mock.exec_chroot_cmd("mkdir -p %s" % dst_dir) 155 | auth_token_dst = os.path.join(dst_dir, "snyk.json") 156 | ec = mock.exec_mock_cmd(["--copyin", self.auth_token_src, auth_token_dst]) 157 | if 0 != ec: 158 | results.error("failed to copy snyk authentication token", ec=ec) 159 | return ec 160 | 161 | # command to run snyk code 162 | cmd = f"{self.snyk_bin} code test -d {SNYK_SCAN_DIR}" 163 | 164 | if self.snyk_code_test_opts: 165 | # propagate options given to --snyk-code-test-opts 166 | cmd += f" {self.snyk_code_test_opts}" 167 | 168 | cmd += f" --sarif-file-output={SNYK_OUTPUT} >/dev/null 2>{SNYK_LOG}" 169 | 170 | if args.snyk_timeout: 171 | # wrap snyk invocation by timeout(1) 172 | cmd = f"/usr/bin/timeout {args.snyk_timeout} {cmd}" 173 | 174 | # snyk requires network connection in the chroot 175 | mock_cmd = ["--enable-network", "--chroot", cmd] 176 | 177 | # run snyk code 178 | ec = mock.exec_mock_cmd(mock_cmd) 179 | 180 | # remove authentication token from the chroot 181 | mock.exec_chroot_cmd("/bin/rm -fv %s" % auth_token_dst) 182 | 183 | # check exit code of snyk code itself 184 | if ec == 3: 185 | # If there are no supported project, we return no results but no crash. 186 | results.print_with_ts("snyk-code: no supported project for Snyk") 187 | # If no supported project, no results file is generated and no stats are generated 188 | props.copy_out_files.remove(SNYK_OUTPUT) 189 | props.post_process_hooks.remove(write_snyk_stats_metadata) 190 | return 0 191 | if ec not in [0, 1]: 192 | results.error("snyk code returned unexpected exit status: %d" % ec, ec=ec) 193 | 194 | # returning non-zero would prevent csmock from archiving SNYK_LOG 195 | return 0 196 | 197 | # run snyk after successful build 198 | props.post_install_hooks += [scan_hook] 199 | 200 | # convert the results into the csdiff's JSON format 201 | def filter_hook(results): 202 | src = results.dbgdir_raw + SNYK_OUTPUT 203 | if not os.path.exists(src): 204 | # do not convert SARIF results if they were not provided by Snyk 205 | return 0 206 | dst = "%s/snyk-results.json" % results.dbgdir_uni 207 | cmd = FILTER_CMD % (src, dst) 208 | return results.exec_cmd(cmd, shell=True) 209 | 210 | def write_snyk_stats_metadata(results): 211 | raw_results_file = results.dbgdir_raw + SNYK_OUTPUT 212 | return snyk_write_analysis_meta(results, raw_results_file) 213 | 214 | props.post_process_hooks += [write_snyk_stats_metadata, filter_hook] 215 | -------------------------------------------------------------------------------- /csmock/plugins/strace.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | import csmock.common.cflags 19 | import csmock.common.util 20 | 21 | 22 | STRACE_CAPTURE_DIR = "/builddir/strace-capture" 23 | 24 | 25 | class PluginProps: 26 | def __init__(self): 27 | self.description = "A dynamic analysis tool that records system calls associated with a running process." 28 | 29 | # hook this plug-in before "gcc" to make ScanProps:enable_csexec() work 30 | self.pass_before = ["gcc"] 31 | 32 | 33 | class Plugin: 34 | def __init__(self): 35 | self.enabled = False 36 | 37 | def get_props(self): 38 | return PluginProps() 39 | 40 | def enable(self): 41 | self.enabled = True 42 | 43 | def init_parser(self, parser): 44 | parser.add_argument( 45 | "--strace-add-flag", action="append", default=[], 46 | help="append the given flag when invoking strace \ 47 | (can be used multiple times)") 48 | 49 | def handle_args(self, parser, args, props): 50 | if not self.enabled: 51 | return 52 | 53 | # make sure strace is installed in chroot 54 | props.install_pkgs += ["strace"] 55 | 56 | # record version of the installed "strace" tool 57 | csmock.common.util.install_default_toolver_hook(props, "strace") 58 | 59 | # hook csexec into the binaries produced in %build 60 | props.enable_csexec() 61 | 62 | # create directory for strace's results 63 | def create_cap_dir_hook(results, mock): 64 | return mock.exec_mockbuild_cmd("mkdir -pv '%s'" % STRACE_CAPTURE_DIR) 65 | props.post_depinst_hooks += [create_cap_dir_hook] 66 | 67 | # default strace cmd-line 68 | wrap_cmd_list = ["/usr/bin/strace", 69 | "--output=%s/trace" % STRACE_CAPTURE_DIR, 70 | "--output-separately"] 71 | 72 | # append custom args if specified 73 | wrap_cmd_list += args.strace_add_flag 74 | 75 | # configure csexec to use strace as the execution wrapper 76 | wrap_cmd = csmock.common.cflags.serialize_flags(wrap_cmd_list, separator="\\a") 77 | extra_env = {} 78 | extra_env["CSEXEC_WRAP_CMD"] = wrap_cmd 79 | 80 | # install a hook to run %check through strace 81 | def run_strace_hook(results, mock, props): 82 | ec = mock.exec_rpmbuild_bi(props, extra_env=extra_env) 83 | if ec != 0: 84 | results.error("strace plug-in: %%install or %%check failed with exit code %d" % ec) 85 | return ec 86 | props.post_install_hooks += [run_strace_hook] 87 | 88 | # pick the captured files when %check is complete 89 | props.copy_out_files += [STRACE_CAPTURE_DIR] 90 | 91 | # TODO: add filter_hook to transform the captured trace files 92 | -------------------------------------------------------------------------------- /csmock/plugins/symbiotic.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | import csmock.common.util 19 | 20 | from csmock.common.cflags import flags_by_warning_level 21 | 22 | 23 | SYMBIOTIC_CAPTURE_DIR = "/builddir/symbiotic-capture" 24 | 25 | DEFAULT_SYMBIOTIC_TIMEOUT = 30 26 | 27 | 28 | class PluginProps: 29 | def __init__(self): 30 | self.description = "A formal verification tool based on instrumentation, program slicing and KLEE." 31 | 32 | # hook this plug-in before "gcc" to make ScanProps:enable_csexec() work 33 | self.pass_before = ["gcc"] 34 | 35 | 36 | class Plugin: 37 | def __init__(self): 38 | self.enabled = False 39 | self.flags = flags_by_warning_level(0) 40 | 41 | def get_props(self): 42 | return PluginProps() 43 | 44 | def enable(self): 45 | self.enabled = True 46 | 47 | def init_parser(self, parser): 48 | parser.add_argument( 49 | "--symbiotic-add-flag", action="append", default=[], 50 | help="append the given flag when invoking symbiotic \ 51 | (can be used multiple times)") 52 | 53 | parser.add_argument( 54 | "--symbiotic-timeout", type=int, default=DEFAULT_SYMBIOTIC_TIMEOUT, 55 | help="maximal amount of time taken by analysis of a single process [s]") 56 | 57 | def handle_args(self, parser, args, props): 58 | if not self.enabled: 59 | return 60 | 61 | # make sure symbiotic and gllvm are installed in chroot 62 | props.add_repos += ["https://download.copr.fedorainfracloud.org/results/@aufover/symbiotic/fedora-$releasever-$basearch/"] 63 | props.add_repos += ["https://download.copr.fedorainfracloud.org/results/@aufover/gllvm/fedora-$releasever-$basearch/"] 64 | props.install_pkgs += ["symbiotic", "gllvm"] 65 | 66 | # enable cswrap 67 | props.enable_cswrap() 68 | props.cswrap_filters += ["csgrep --mode=json --invert-match --checker CLANG_WARNING --event error"] 69 | 70 | # set dioscc as the default compiler 71 | # FIXME: this is not 100% reliable 72 | props.env["CC"] = "gclang" 73 | props.env["CXX"] = "gclang++" 74 | props.rpm_opts += ["--define", "__cc gclang", "--define", "__cxx gclang++", "--define", "__cpp gclang -E"] 75 | 76 | # assert that gllvm is installed properly 77 | def gllvm_is_working(results, mock): 78 | return mock.exec_mockbuild_cmd("gsanity-check") 79 | props.post_depinst_hooks += [gllvm_is_working] 80 | 81 | # nuke default options 82 | props.rpm_opts += ["--define", "toolchain clang", "--define", "optflags -O0", "--define", "build_ldflags -O0"] 83 | 84 | # record version of the installed "symbiotic" tool 85 | csmock.common.util.install_default_toolver_hook(props, "symbiotic") 86 | 87 | # hook csexec into the binaries produced in %build 88 | props.enable_csexec() 89 | 90 | # create directory for symbiotic's results 91 | def create_cap_dir_hook(results, mock): 92 | cmd = "mkdir -pv '%s'" % SYMBIOTIC_CAPTURE_DIR 93 | return mock.exec_mockbuild_cmd(cmd) 94 | props.post_depinst_hooks += [create_cap_dir_hook] 95 | 96 | # default symbiotic cmd-line 97 | timeout = args.symbiotic_timeout 98 | wrap_cmd_list = [ 99 | "--skip-ld-linux", 100 | "/usr/bin/csexec-symbiotic", 101 | "-l", SYMBIOTIC_CAPTURE_DIR, 102 | "-s", f"--timeout={timeout} --slicer-timeout={timeout} "\ 103 | f"--instrumentation-timeout={timeout} --debug=all "\ 104 | f"--prp=memsafety"] 105 | 106 | # append custom args if specified 107 | # FIXME: what about single arguments with whitespaces? 108 | wrap_cmd_list[-1] += " " + " ".join(args.symbiotic_add_flag) 109 | 110 | # FIXME: multiple runs of %check for multiple dynamic analyzers not yet supported 111 | assert "CSEXEC_WRAP_CMD" not in props.env 112 | 113 | # configure csexec to use symbiotic as the execution wrapper 114 | wrap_cmd = csmock.common.cflags.serialize_flags(wrap_cmd_list, separator="\\a") 115 | props.env["CSEXEC_WRAP_CMD"] = wrap_cmd 116 | 117 | # write all compiler flags to the environment 118 | flags = ['-Wno-unused-command-line-argument', 119 | '-Wno-unused-parameter', '-Wno-unknown-attributes', 120 | '-Wno-unused-label', '-Wno-unknown-pragmas', 121 | '-Wno-unused-command-line-argument', 122 | '-fsanitize-address-use-after-scope', '-O0', '-Xclang', 123 | '-disable-llvm-passes', '-D__inline=', '-g', 124 | '-Wl,--dynamic-linker,/usr/bin/csexec-loader'] 125 | self.flags.append_flags(flags) 126 | self.flags.remove_flags(['-O1', '-O2', '-O3', '-Os', '-Ofast', '-Og']) 127 | self.flags.write_to_env(props.env) 128 | 129 | # run %check (disabled by default for static analyzers) 130 | props.run_check = True 131 | 132 | # pick the captured files when %check is complete 133 | props.copy_out_files += [SYMBIOTIC_CAPTURE_DIR] 134 | 135 | # transform log files produced by symbiotic into csdiff format 136 | def filter_hook(results): 137 | src_dir = results.dbgdir_raw + SYMBIOTIC_CAPTURE_DIR 138 | 139 | # ensure we have permission to read all capture files 140 | results.exec_cmd(['chmod', '-R', '+r', src_dir]) 141 | 142 | # `cd` first to avoid `csgrep: Argument list too long` error on glob expansion 143 | dst = f"{results.dbgdir_uni}/symbiotic-capture.js" 144 | cmd = f"cd '{src_dir}' && touch empty.conv && csgrep --mode=json --remove-duplicates *.conv > {dst}" 145 | return results.exec_cmd(cmd, shell=True) 146 | props.post_process_hooks += [filter_hook] 147 | -------------------------------------------------------------------------------- /csmock/plugins/unicontrol.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | import csmock.common.util 19 | 20 | 21 | UNICONTROL_SCRIPT = "/usr/share/csmock/scripts/find-unicode-control.py" 22 | 23 | UNICONTROL_SCAN_DIR = "/builddir/build/BUILD" 24 | 25 | UNICONTROL_OUTPUT = "/builddir/unicontrol-capture.err" 26 | 27 | UNICONTROL_LOG = "/builddir/unicontrol-capture.log" 28 | 29 | FILTER_CMD = "csgrep --mode=json '%s' > '%s'" 30 | 31 | 32 | class PluginProps: 33 | def __init__(self): 34 | self.description = "A tool that looks for non-printable unicode characters in all text files in a source tree." 35 | 36 | 37 | class Plugin: 38 | def __init__(self): 39 | self.enabled = False 40 | 41 | def get_props(self): 42 | return PluginProps() 43 | 44 | def enable(self): 45 | self.enabled = True 46 | 47 | def init_parser(self, parser): 48 | parser.add_argument( 49 | "--unicontrol-bidi-only", action="store_true", 50 | help="look for bidirectional control characters only") 51 | 52 | parser.add_argument( 53 | "--unicontrol-notests", action="store_true", 54 | help="exclude tests (basically test.* as a component of path)") 55 | 56 | def handle_args(self, parser, args, props): 57 | if args.unicontrol_bidi_only or args.unicontrol_notests: 58 | self.enable() 59 | 60 | if not self.enabled: 61 | return 62 | 63 | # update scan metadata 64 | def write_toolver_hook(results, _props): 65 | results.ini_writer.append("analyzer-version-unicontrol", "0.0.2") 66 | return 0 67 | props.pre_mock_hooks += [write_toolver_hook] 68 | 69 | # dependency of UNICONTROL_SCRIPT 70 | props.install_opt_pkgs += ["python3-magic", "python3-six"] 71 | 72 | if props.mock_profile.startswith("rhel-7"): 73 | # do not break el7 builds by unexpectedly making python3 available 74 | props.env["PYTHON"] = "/usr/bin/python2" 75 | 76 | cmd = "LANG=en_US.utf8 %s -d -v %s" % (UNICONTROL_SCRIPT, UNICONTROL_SCAN_DIR) 77 | 78 | if args.unicontrol_bidi_only: 79 | cmd += " -p bidi" 80 | 81 | if args.unicontrol_notests: 82 | cmd += " --notests" 83 | 84 | cmd += " >%s 2>%s" % (UNICONTROL_OUTPUT, UNICONTROL_LOG) 85 | props.post_build_chroot_cmds += [cmd] 86 | props.copy_out_files += [UNICONTROL_OUTPUT, UNICONTROL_LOG] 87 | 88 | def filter_hook(results): 89 | src = results.dbgdir_raw + UNICONTROL_OUTPUT 90 | dst = "%s/unicontrol-capture.js" % results.dbgdir_uni 91 | cmd = FILTER_CMD % (src, dst) 92 | return results.exec_cmd(cmd, shell=True) 93 | 94 | props.post_process_hooks += [filter_hook] 95 | -------------------------------------------------------------------------------- /csmock/plugins/valgrind.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | import csmock.common.cflags 19 | import csmock.common.util 20 | 21 | 22 | VALGRIND_CAPTURE_DIR = "/builddir/valgrind-capture" 23 | 24 | DEFAULT_VALGRIND_TIMEOUT = 30 25 | 26 | 27 | class PluginProps: 28 | def __init__(self): 29 | self.description = "A dynamic analysis tool for finding memory management bugs in programs." 30 | 31 | # hook this plug-in before "gcc" to make ScanProps:enable_csexec() work 32 | self.pass_before = ["gcc"] 33 | 34 | 35 | class Plugin: 36 | def __init__(self): 37 | self.enabled = False 38 | 39 | def get_props(self): 40 | return PluginProps() 41 | 42 | def enable(self): 43 | self.enabled = True 44 | 45 | def init_parser(self, parser): 46 | parser.add_argument( 47 | "--valgrind-add-flag", action="append", default=[], 48 | help="append the given flag when invoking valgrind \ 49 | (can be used multiple times)") 50 | 51 | parser.add_argument( 52 | "--valgrind-timeout", type=int, default=DEFAULT_VALGRIND_TIMEOUT, 53 | help="maximal amount of time taken by analysis of a single process [s]") 54 | 55 | def handle_args(self, parser, args, props): 56 | if not self.enabled: 57 | return 58 | 59 | # make sure valgrind is installed in chroot 60 | props.install_pkgs += ["valgrind"] 61 | 62 | # record version of the installed "valgrind" tool 63 | csmock.common.util.install_default_toolver_hook(props, "valgrind") 64 | 65 | # hook csexec into the binaries produced in %build 66 | props.enable_csexec() 67 | 68 | # create directory for valgrind's results 69 | def create_cap_dir_hook(results, mock): 70 | cmd = "mkdir -pv '%s' && touch '%s/empty.xml'" % (VALGRIND_CAPTURE_DIR, VALGRIND_CAPTURE_DIR) 71 | return mock.exec_mockbuild_cmd(cmd) 72 | props.post_depinst_hooks += [create_cap_dir_hook] 73 | 74 | # default valgrind cmd-line 75 | wrap_cmd_list = [ 76 | # timeout wrapper 77 | "/usr/bin/timeout", 78 | "--signal=KILL", 79 | "%d" % args.valgrind_timeout, 80 | # valgrind invocation 81 | "/usr/bin/valgrind", 82 | "--xml=yes", 83 | "--xml-file=%s/pid-%%p-%%n.xml" % VALGRIND_CAPTURE_DIR, 84 | "--log-file=%s/pid-%%p-%%n.log" % VALGRIND_CAPTURE_DIR, 85 | "--child-silent-after-fork=yes"] 86 | 87 | # append custom args if specified 88 | wrap_cmd_list += args.valgrind_add_flag 89 | 90 | # configure csexec to use valgrind as the execution wrapper 91 | wrap_cmd = csmock.common.cflags.serialize_flags(wrap_cmd_list, separator="\\a") 92 | extra_env = {} 93 | extra_env["CSEXEC_WRAP_CMD"] = wrap_cmd 94 | 95 | # install a hook to run %check through valgrind 96 | def run_valgrind_hook(results, mock, props): 97 | ec = mock.exec_rpmbuild_bi(props, extra_env=extra_env) 98 | if ec != 0: 99 | results.error("valgrind plug-in: %%install or %%check failed with exit code %d" % ec) 100 | return ec 101 | props.post_install_hooks += [run_valgrind_hook] 102 | 103 | # pick the captured files when %check is complete 104 | props.copy_out_files += [VALGRIND_CAPTURE_DIR] 105 | 106 | # delete empty log files 107 | def cleanup_hook(results): 108 | return results.exec_cmd(["find", results.dbgdir_raw + VALGRIND_CAPTURE_DIR, 109 | "-name", "pid-*.log", "-empty", "-delete"]) 110 | props.post_process_hooks += [cleanup_hook] 111 | 112 | # transform XML files produced by valgrind into csdiff format 113 | def filter_hook(results): 114 | src_dir = results.dbgdir_raw + VALGRIND_CAPTURE_DIR 115 | 116 | # ensure we have permission to read all capture files 117 | results.exec_cmd(['chmod', '-R', '+r', src_dir]) 118 | 119 | # `cd` first to avoid `csgrep: Argument list too long` error on glob expansion 120 | dst = f"{results.dbgdir_uni}/valgrind-capture.js" 121 | cmd = f"cd '{src_dir}' && csgrep --mode=json --quiet --remove-duplicates *.xml > '{dst}'" 122 | return results.exec_cmd(cmd, shell=True) 123 | props.post_process_hooks += [filter_hook] 124 | -------------------------------------------------------------------------------- /doc/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | set(MAN_PAGES "") 19 | 20 | # rules to create symlinks to local python packages 21 | add_custom_command(OUTPUT ${PROJECT_BINARY_DIR}/csmock/common 22 | COMMAND ln -fsv ${PROJECT_SOURCE_DIR}/csmock/__init__.py 23 | ${PROJECT_SOURCE_DIR}/csmock/common 24 | ${PROJECT_SOURCE_DIR}/csmock/plugins 25 | ${PROJECT_BINARY_DIR}/csmock/ 26 | DEPENDS ${PROJECT_BINARY_DIR}/csmock/csmock VERBATIM) 27 | 28 | # macro to generate a man page from the corresponding binary 29 | macro(create_manpage BINARY) 30 | add_custom_command(OUTPUT ${PROJECT_BINARY_DIR}/csmock/${BINARY}.1 31 | COMMAND PYTHONPATH=${PROJECT_BINARY_DIR}/csmock ${HELP2MAN} --no-info 32 | --section 1 --include ${CMAKE_CURRENT_SOURCE_DIR}/${BINARY}.h2m 33 | ${PROJECT_BINARY_DIR}/csmock/${BINARY} 34 | > ${PROJECT_BINARY_DIR}/csmock/${BINARY}.1 35 | || rm -f ${PROJECT_BINARY_DIR}/${BINARY}.1 36 | COMMENT "Generating ${BINARY} man page" 37 | DEPENDS ${PROJECT_BINARY_DIR}/csmock/${BINARY} 38 | ${PROJECT_BINARY_DIR}/csmock/common 39 | VERBATIM) 40 | install(FILES ${PROJECT_BINARY_DIR}/csmock/${BINARY}.1 41 | DESTINATION ${SHARE_INSTALL_PREFIX}/man/man1) 42 | set(MAN_PAGES ${MAN_PAGES} ${PROJECT_BINARY_DIR}/csmock/${BINARY}.1) 43 | endmacro(create_manpage) 44 | 45 | # generate man pages using help2man if available 46 | find_program(HELP2MAN help2man) 47 | if(HELP2MAN) 48 | if(ENABLE_CSBUILD) 49 | create_manpage(csbuild) 50 | endif() 51 | 52 | if(ENABLE_CSMOCK) 53 | create_manpage(csmock) 54 | endif() 55 | 56 | if(NOT "${MAN_PAGES}" STREQUAL "") 57 | add_custom_target(doc ALL DEPENDS ${MAN_PAGES}) 58 | endif() 59 | endif() 60 | -------------------------------------------------------------------------------- /doc/csbuild.h2m: -------------------------------------------------------------------------------- 1 | [NAME] 2 | csbuild - tool for plugging static analyzers into the build process 3 | -------------------------------------------------------------------------------- /doc/csmock.h2m: -------------------------------------------------------------------------------- 1 | [NAME] 2 | csmock - run static analysis of the given SRPM using mock 3 | 4 | [OUTPUT FORMAT] 5 | If not overridden by the --output option, csmock creates an archive 6 | .B NVR.tar.xz 7 | in the current directory for an SRPM named NVR.src.rpm (or NVR.tar.* if the 8 | --shell-cmd option is used). The archive contains a directory named NVR as the 9 | only top-level directory, containing the following items: 10 | 11 | .B scan-results.err 12 | - scan results encoded as plain-text (for source code editors) 13 | 14 | .B scan-results.html 15 | - scan results encoded as HTML (suitable for web browsers) 16 | 17 | .B scan-results.js 18 | - scan results, including scan metadata, encoded using JSON 19 | 20 | .B scan-results-summary.txt 21 | - total count of defects found by particular checkers 22 | 23 | .B scan.ini 24 | - scan metadata encoded in the INI format 25 | 26 | .B scan.log 27 | - scan log file (useful for debugging scan failures) 28 | 29 | .B debug 30 | - a directory containing additional data (intended for csmock debugging) 31 | 32 | Note that external plug-ins of csmock may create additional files (not covered 33 | by this man page) in the directory with results. 34 | -------------------------------------------------------------------------------- /make-srpm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (C) 2012-2022 Red Hat, Inc. 4 | # 5 | # This file is part of csmock. 6 | # 7 | # csmock is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # any later version. 11 | # 12 | # csmock is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with csmock. If not, see . 19 | 20 | SELF="$0" 21 | 22 | PKG="csmock" 23 | 24 | die() { 25 | echo "$SELF: error: $1" >&2 26 | exit 1 27 | } 28 | 29 | match() { 30 | grep "$@" > /dev/null 31 | } 32 | 33 | DST="$(readlink -f "$PWD")" 34 | 35 | REPO="$(git rev-parse --show-toplevel)" 36 | test -d "$REPO" || die "not in a git repo" 37 | 38 | NV="$(git describe --tags)" 39 | echo "$NV" | match "^$PKG-" || die "release tag not found" 40 | 41 | VER="$(echo "$NV" | sed "s/^$PKG-//")" 42 | 43 | TIMESTAMP="$(git log --pretty="%cd" --date=iso -1 \ 44 | | tr -d ':-' | tr ' ' . | cut -d. -f 1,2)" 45 | 46 | VER="$(echo "$VER" | sed "s/-.*-/.$TIMESTAMP./")" 47 | 48 | BRANCH="$(git rev-parse --abbrev-ref HEAD)" 49 | test -n "$BRANCH" || die "failed to get current branch name" 50 | test "main" = "${BRANCH}" || VER="${VER}.${BRANCH//[\/-]/_}" 51 | test -z "$(git diff HEAD)" || VER="${VER}.dirty" 52 | 53 | NV="${PKG}-${VER}" 54 | printf "%s: preparing a release of \033[1;32m%s\033[0m\n" "$SELF" "$NV" 55 | 56 | SPEC="$PKG.spec" 57 | if [[ "$1" != "--generate-spec" ]]; then 58 | TMP="$(mktemp -d)" 59 | trap "rm -rf '$TMP'" EXIT 60 | cd "$TMP" >/dev/null || die "mktemp failed" 61 | 62 | # clone the repository 63 | git clone "$REPO" "$PKG" || die "git clone failed" 64 | cd "$PKG" || die "git clone failed" 65 | 66 | make -j5 distcheck CTEST='ctest -j5' || die "'make distcheck' has failed" 67 | 68 | SRC_TAR="${NV}.tar" 69 | SRC="${SRC_TAR}.xz" 70 | git archive --prefix="$NV/" --format="tar" HEAD -- . > "${TMP}/${SRC_TAR}" \ 71 | || die "failed to export sources" 72 | cd "$TMP" >/dev/null || die "mktemp failed" 73 | xz -c "$SRC_TAR" > "$SRC" || die "failed to compress sources" 74 | 75 | SPEC="$TMP/$SPEC" 76 | fi 77 | 78 | cat > "$SPEC" << EOF 79 | # disable in source builds on EPEL <9 80 | %undefine __cmake_in_source_build 81 | %undefine __cmake3_in_source_build 82 | 83 | Name: $PKG 84 | Version: $VER 85 | Release: 1%{?dist} 86 | Summary: A mock wrapper for Static Analysis tools 87 | 88 | License: GPL-3.0-or-later 89 | URL: https://github.com/csutils/%{name} 90 | Source0: https://github.com/csutils/%{name}/releases/download/%{name}-%{version}/%{name}-%{version}.tar.xz 91 | 92 | BuildRequires: cmake3 93 | BuildRequires: help2man 94 | 95 | %if 0%{?rhel} == 7 96 | %global python3_pkgversion 36 97 | %global __python %{python3} 98 | %endif 99 | 100 | BuildRequires: python%{python3_pkgversion}-GitPython 101 | BuildRequires: python%{python3_pkgversion}-devel 102 | 103 | Requires: csmock-common >= %{version}-%{release} 104 | Requires: csmock-plugin-clang >= %{version}-%{release} 105 | Requires: csmock-plugin-cppcheck >= %{version}-%{release} 106 | Requires: csmock-plugin-gitleaks >= %{version}-%{release} 107 | Requires: csmock-plugin-shellcheck >= %{version}-%{release} 108 | 109 | BuildArch: noarch 110 | 111 | %description 112 | This is a metapackage pulling in csmock-common and basic csmock plug-ins. 113 | 114 | %package -n csbuild 115 | Summary: Tool for plugging static analyzers into the build process 116 | Requires: cscppc 117 | Requires: csclng 118 | Requires: csdiff 119 | Requires: csmock-common 120 | Requires: cswrap 121 | Requires: python%{python3_pkgversion}-GitPython 122 | 123 | %description -n csbuild 124 | Tool for plugging static analyzers into the build process, free of mock. 125 | 126 | %package common 127 | Summary: Core of csmock (a mock wrapper for Static Analysis tools) 128 | Requires: csdiff > 3.5.1 129 | Requires: csgcca 130 | Requires: cswrap 131 | Requires: mock >= 5.7 132 | Requires: tar 133 | Requires: xz 134 | %if 0%{?rhel} != 7 135 | Recommends: modulemd-tools 136 | %endif 137 | 138 | %description common 139 | This package contains the csmock tool that allows to scan SRPMs by Static 140 | Analysis tools in a fully automated way. 141 | 142 | %package plugin-bandit 143 | Summary: csmock plug-in providing the support for Bandit. 144 | Requires: csmock-common 145 | 146 | %description plugin-bandit 147 | This package contains the bandit plug-in for csmock. 148 | 149 | %package plugin-cbmc 150 | Summary: csmock plug-in providing the support for cbmc 151 | Requires: csexec 152 | Requires: csmock-common 153 | 154 | %description plugin-cbmc 155 | This package contains the cbmc plug-in for csmock. 156 | 157 | %package plugin-clang 158 | Summary: csmock plug-in providing the support for Clang 159 | Requires: csclng 160 | Requires: csmock-common 161 | 162 | %description plugin-clang 163 | This package contains the clang plug-in for csmock. 164 | 165 | %package plugin-clippy 166 | Summary: csmock plug-in providing the support for Rust Clippy. 167 | Requires: csmock-common 168 | 169 | %description plugin-clippy 170 | This package contains the Rust Clippy plug-in for csmock. 171 | 172 | %package plugin-cppcheck 173 | Summary: csmock plug-in providing the support for Cppcheck 174 | Requires: cscppc 175 | Requires: csmock-common 176 | 177 | %description plugin-cppcheck 178 | This package contains the cppcheck plug-in for csmock. 179 | 180 | %package plugin-divine 181 | Summary: csmock plug-in providing the support for divine 182 | Requires: csexec 183 | Requires: csmock-common 184 | 185 | %description plugin-divine 186 | This package contains the divine plug-in for csmock. 187 | 188 | %package plugin-gitleaks 189 | Summary: experimental csmock plug-in 190 | Requires: csmock-common 191 | 192 | %description plugin-gitleaks 193 | This package contains the gitleaks plug-in for csmock. 194 | 195 | %package plugin-infer 196 | Summary: csmock plug-in providing the support for Infer 197 | Requires: csmock-common 198 | 199 | %description plugin-infer 200 | This package contains the Infer plug-in for csmock. 201 | 202 | %package plugin-pylint 203 | Summary: csmock plug-in providing the support for Pylint. 204 | Requires: csmock-common 205 | 206 | %description plugin-pylint 207 | This package contains the pylint plug-in for csmock. 208 | 209 | %package plugin-semgrep 210 | Summary: csmock plug-in providing the support for semgrep scan 211 | Requires: csmock-common 212 | 213 | %description plugin-semgrep 214 | This package contains the semgrep plug-in for csmock. 215 | 216 | %package plugin-shellcheck 217 | Summary: csmock plug-in providing the support for ShellCheck. 218 | Requires: csmock-common 219 | Requires: csmock-plugin-shellcheck-core 220 | 221 | %description plugin-shellcheck 222 | This package contains the shellcheck plug-in for csmock. 223 | 224 | %package plugin-shellcheck-core 225 | Conflicts: csmock-plugin-shellcheck < %{version}-%{release} 226 | Summary: script to run shellcheck on a directory tree 227 | %if 0%{?rhel} != 7 228 | Recommends: ShellCheck 229 | %endif 230 | 231 | %description plugin-shellcheck-core 232 | This package contains the run-shellcheck.sh script to run shellcheck on a directory tree. 233 | 234 | %package plugin-smatch 235 | Summary: csmock plug-in providing the support for smatch 236 | Requires: csmatch 237 | Requires: csmock-common 238 | Requires: cswrap 239 | 240 | %description plugin-smatch 241 | This package contains the smatch plug-in for csmock. 242 | 243 | %package plugin-snyk 244 | Summary: csmock plug-in providing the support for snyk 245 | Requires: csmock-common 246 | 247 | %description plugin-snyk 248 | This package contains the snyk plug-in for csmock. 249 | 250 | %package plugin-strace 251 | Summary: csmock plug-in providing the support for strace 252 | Requires: csexec 253 | Requires: csmock-common 254 | 255 | %description plugin-strace 256 | This package contains the strace plug-in for csmock. 257 | 258 | %package plugin-symbiotic 259 | Summary: csmock plug-in providing the support for symbiotic 260 | Requires: csexec 261 | Requires: csmock-common 262 | 263 | %description plugin-symbiotic 264 | This package contains the symbiotic plug-in for csmock. 265 | 266 | %package plugin-valgrind 267 | Summary: csmock plug-in providing the support for valgrind 268 | Requires: csexec 269 | Requires: csmock-common 270 | 271 | %description plugin-valgrind 272 | This package contains the valgrind plug-in for csmock. 273 | 274 | %package plugin-unicontrol 275 | Summary: experimental csmock plug-in 276 | Requires: csmock-common 277 | 278 | %description plugin-unicontrol 279 | This package contains the unicontrol plug-in for csmock. 280 | 281 | %prep 282 | %autosetup 283 | 284 | %build 285 | %cmake3 \\ 286 | -DVERSION='%{name}-%{version}-%{release}' \\ 287 | -DPython3_EXECUTABLE='%{__python3}' 288 | %cmake3_build 289 | 290 | %install 291 | %cmake3_install 292 | 293 | # needed to create the csmock RPM 294 | %files 295 | 296 | %files -n csbuild 297 | %license COPYING 298 | %{_bindir}/csbuild 299 | %{_mandir}/man1/csbuild.1* 300 | %{_datadir}/csbuild/scripts/run-scan.sh 301 | 302 | %files common 303 | %license COPYING 304 | %doc README 305 | %dir %{_datadir}/csmock 306 | %dir %{_datadir}/csmock/scripts 307 | %dir %{python3_sitelib}/csmock 308 | %dir %{python3_sitelib}/csmock/plugins 309 | %{_bindir}/csmock 310 | %{_mandir}/man1/csmock.1* 311 | %{_datadir}/csmock/cwe-map.csv 312 | %{_datadir}/csmock/scripts/enable-keep-going.sh 313 | %attr(755,root,root) %{_datadir}/csmock/scripts/chroot-fixups 314 | %{_datadir}/csmock/scripts/patch-rawbuild.sh 315 | %{python3_sitelib}/csmock/__init__.py* 316 | %{python3_sitelib}/csmock/common 317 | %{python3_sitelib}/csmock/plugins/__init__.py* 318 | %{python3_sitelib}/csmock/plugins/gcc.py* 319 | %{python3_sitelib}/csmock/__pycache__/__init__.* 320 | %{python3_sitelib}/csmock/plugins/__pycache__/__init__.* 321 | %{python3_sitelib}/csmock/plugins/__pycache__/gcc.* 322 | 323 | %files plugin-bandit 324 | %{_datadir}/csmock/scripts/run-bandit.sh 325 | %{python3_sitelib}/csmock/plugins/bandit.py* 326 | %{python3_sitelib}/csmock/plugins/__pycache__/bandit.* 327 | 328 | %files plugin-cbmc 329 | %{python3_sitelib}/csmock/plugins/cbmc.py* 330 | %{python3_sitelib}/csmock/plugins/__pycache__/cbmc.* 331 | 332 | %files plugin-clang 333 | %{python3_sitelib}/csmock/plugins/clang.py* 334 | %{python3_sitelib}/csmock/plugins/__pycache__/clang.* 335 | 336 | %files plugin-clippy 337 | %{_datadir}/csmock/scripts/convert-clippy.py* 338 | %if 0%{?rhel} == 7 339 | %{_datadir}/csmock/scripts/__pycache__/convert-clippy.* 340 | %endif 341 | %{_datadir}/csmock/scripts/inject-clippy.sh 342 | %{python3_sitelib}/csmock/plugins/clippy.py* 343 | %{python3_sitelib}/csmock/plugins/__pycache__/clippy.* 344 | 345 | %files plugin-cppcheck 346 | %{python3_sitelib}/csmock/plugins/cppcheck.py* 347 | %{python3_sitelib}/csmock/plugins/__pycache__/cppcheck.* 348 | 349 | %files plugin-divine 350 | %{python3_sitelib}/csmock/plugins/divine.py* 351 | %{python3_sitelib}/csmock/plugins/__pycache__/divine.* 352 | 353 | %files plugin-gitleaks 354 | %{python3_sitelib}/csmock/plugins/gitleaks.py* 355 | %{python3_sitelib}/csmock/plugins/__pycache__/gitleaks.* 356 | 357 | %files plugin-infer 358 | %{_datadir}/csmock/scripts/filter-infer.py* 359 | %if 0%{?rhel} == 7 360 | %{_datadir}/csmock/scripts/__pycache__/filter-infer.* 361 | %endif 362 | %{_datadir}/csmock/scripts/install-infer.sh 363 | %{python3_sitelib}/csmock/plugins/infer.py* 364 | %{python3_sitelib}/csmock/plugins/__pycache__/infer.* 365 | 366 | %files plugin-pylint 367 | %{_datadir}/csmock/scripts/run-pylint.sh 368 | %{python3_sitelib}/csmock/plugins/pylint.py* 369 | %{python3_sitelib}/csmock/plugins/__pycache__/pylint.* 370 | 371 | %files plugin-semgrep 372 | %{python3_sitelib}/csmock/plugins/semgrep.py* 373 | %{python3_sitelib}/csmock/plugins/__pycache__/semgrep.* 374 | 375 | %files plugin-shellcheck 376 | %{python3_sitelib}/csmock/plugins/shellcheck.py* 377 | %{python3_sitelib}/csmock/plugins/__pycache__/shellcheck.* 378 | 379 | %files plugin-shellcheck-core 380 | %license COPYING 381 | %dir %{_datadir}/csmock 382 | %dir %{_datadir}/csmock/scripts 383 | %{_datadir}/csmock/scripts/run-shellcheck.sh 384 | 385 | %files plugin-smatch 386 | %{python3_sitelib}/csmock/plugins/smatch.py* 387 | %{python3_sitelib}/csmock/plugins/__pycache__/smatch.* 388 | 389 | %files plugin-snyk 390 | %{python3_sitelib}/csmock/plugins/snyk.py* 391 | %{python3_sitelib}/csmock/plugins/__pycache__/snyk.* 392 | 393 | %files plugin-strace 394 | %{python3_sitelib}/csmock/plugins/strace.py* 395 | %{python3_sitelib}/csmock/plugins/__pycache__/strace.* 396 | 397 | %files plugin-symbiotic 398 | %{python3_sitelib}/csmock/plugins/symbiotic.py* 399 | %{python3_sitelib}/csmock/plugins/__pycache__/symbiotic.* 400 | 401 | %files plugin-valgrind 402 | %{python3_sitelib}/csmock/plugins/valgrind.py* 403 | %{python3_sitelib}/csmock/plugins/__pycache__/valgrind.* 404 | 405 | %files plugin-unicontrol 406 | %{_datadir}/csmock/scripts/find-unicode-control.py* 407 | %if 0%{?rhel} == 7 408 | %{_datadir}/csmock/scripts/__pycache__/find-unicode-control.* 409 | %endif 410 | %{python3_sitelib}/csmock/plugins/unicontrol.py* 411 | %{python3_sitelib}/csmock/plugins/__pycache__/unicontrol.* 412 | EOF 413 | 414 | if [[ "$1" != "--generate-spec" ]]; then 415 | rpmbuild -bs "$SPEC" \ 416 | --define "_sourcedir $TMP" \ 417 | --define "_specdir $TMP" \ 418 | --define "_srcrpmdir $DST" 419 | fi 420 | -------------------------------------------------------------------------------- /plans/ci.fmf: -------------------------------------------------------------------------------- 1 | summary: Basic analysis tests 2 | discover: 3 | how: fmf 4 | execute: 5 | how: tmt 6 | -------------------------------------------------------------------------------- /scripts/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Red Hat, Inc. 2 | # 3 | # This file is part of csmock. 4 | # 5 | # csmock is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # any later version. 9 | # 10 | # csmock 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 General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with csmock. If not, see . 17 | 18 | if(ENABLE_CSBUILD) 19 | install(FILES 20 | ${CMAKE_CURRENT_SOURCE_DIR}/run-scan.sh 21 | DESTINATION ${SHARE_INSTALL_PREFIX}/csbuild/scripts 22 | PERMISSIONS ${PERM_EXECUTABLE}) 23 | endif() 24 | 25 | if(ENABLE_CSMOCK) 26 | install(DIRECTORY chroot-fixups 27 | FILE_PERMISSIONS ${PERM_EXECUTABLE} 28 | DESTINATION ${SHARE_INSTALL_PREFIX}/csmock/scripts) 29 | 30 | install(FILES 31 | ${CMAKE_CURRENT_SOURCE_DIR}/convert-clippy.py 32 | ${CMAKE_CURRENT_SOURCE_DIR}/enable-keep-going.sh 33 | ${CMAKE_CURRENT_SOURCE_DIR}/filter-infer.py 34 | ${CMAKE_CURRENT_SOURCE_DIR}/find-unicode-control.py 35 | ${CMAKE_CURRENT_SOURCE_DIR}/inject-clippy.sh 36 | ${CMAKE_CURRENT_SOURCE_DIR}/install-infer.sh 37 | ${CMAKE_CURRENT_SOURCE_DIR}/patch-rawbuild.sh 38 | ${CMAKE_CURRENT_SOURCE_DIR}/run-bandit.sh 39 | ${CMAKE_CURRENT_SOURCE_DIR}/run-pylint.sh 40 | ${CMAKE_CURRENT_SOURCE_DIR}/run-shellcheck.sh 41 | DESTINATION ${SHARE_INSTALL_PREFIX}/csmock/scripts 42 | PERMISSIONS ${PERM_EXECUTABLE}) 43 | endif() 44 | -------------------------------------------------------------------------------- /scripts/chroot-fixups/00-pre-usr-move-shells.sh: -------------------------------------------------------------------------------- 1 | # intentionally no shebang so that rpmbuild does not break this script 2 | # shellcheck shell=sh 3 | 4 | # exit successfully if this is a post-UsrMove chroot 5 | test -L /bin && exit 0 6 | 7 | # if rpmbuild from the host env translated /bin/bash to /usr/bin/bash in the 8 | # shebangs of our scripts, create reverse symlinks to make the scripts work 9 | # in the chroot env again 10 | for sh in bash sh; do 11 | dst=/usr/bin/${sh} 12 | test -e ${dst} && continue 13 | test -x /bin/${sh} && ln -sv ../../bin/${sh} $dst 14 | done 15 | -------------------------------------------------------------------------------- /scripts/chroot-fixups/gdk-pixbuf2-triggers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -x /usr/bin/gdk-pixbuf-query-loaders-64 ]; then 4 | (set -x; /usr/bin/gdk-pixbuf-query-loaders-64 --update-cache) 5 | fi 6 | -------------------------------------------------------------------------------- /scripts/chroot-fixups/glib2-triggers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -x /usr/bin/gio-querymodules-64 ] && [ -d /usr/lib64/gio/modules ]; then 4 | (set -x; /usr/bin/gio-querymodules-64 /usr/lib64/gio/modules) 5 | fi 6 | 7 | if [ -x /usr/bin/glib-compile-schemas ] && [ -d /usr/share/glib-2.0/schemas ]; then 8 | (set -x; /usr/bin/glib-compile-schemas /usr/share/glib-2.0/schemas) 9 | fi 10 | -------------------------------------------------------------------------------- /scripts/chroot-fixups/kpathsea-texhash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -x /usr/bin/texhash ]; then 4 | (set -x; /usr/bin/texhash) 5 | fi 6 | -------------------------------------------------------------------------------- /scripts/chroot-fixups/openssl-public-header-files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if test -w /usr/include/openssl/e_os2.h; then 4 | (set -x 5 | 6 | # behave as if the code was preprocessed with -DDEBUG_UNUSED 7 | sed -i /usr/include/openssl/e_os2.h \ 8 | -e 's|\(# *if\)def DEBUG_UNUSED|\1 1|') 9 | fi 10 | -------------------------------------------------------------------------------- /scripts/chroot-fixups/qt5-core-abi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -x /usr/lib64/libQt5Core.so.5 ]; then 4 | (set -x; strip --remove-section=.note.ABI-tag /usr/lib64/libQt5Core.so.5) 5 | fi 6 | -------------------------------------------------------------------------------- /scripts/chroot-fixups/rpm-build-scripts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | script="/usr/lib/rpm/redhat/brp-mangle-shebangs" 4 | if [ -x $script ]; then 5 | (set -x; sed -e 's/fail=1/fail=0/' -i $script ) 6 | fi 7 | 8 | # skip RPM scripts running in %install that are not needed and break scans 9 | for script in /usr/lib/rpm/{redhat/brp-llvm-compile-lto-elf,brp-strip-static-archive}; do 10 | if [ -f "$script" ]; then 11 | ln -fsvT /bin/true "$script" 12 | fi 13 | done 14 | -------------------------------------------------------------------------------- /scripts/chroot-fixups/rpm-macros.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | file="/usr/lib/rpm/macros.d/macros.pyproject" 4 | if [ -w ${file} ]; then 5 | (set -x; sed -e 's|> */dev/stderr|>\&2|' -i ${file}) 6 | fi 7 | -------------------------------------------------------------------------------- /scripts/chroot-fixups/rpm-python-extras.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | file="/usr/lib/rpm/pythondistdeps.py" 4 | if [ -w ${file} ]; then 5 | (set -x; sed -e 's|print(.*PYTHON_EXTRAS_NOT_FOUND_ERROR.*) *$|continue|' -i ${file}) 6 | fi 7 | -------------------------------------------------------------------------------- /scripts/chroot-fixups/shared-mime-info-triggers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -x /usr/bin/update-mime-database ]; then 4 | (set -x; /usr/bin/update-mime-database -n /usr/share/mime) 5 | fi 6 | -------------------------------------------------------------------------------- /scripts/chroot-fixups/symbiotic-timeout.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for file in /opt/symbiotic/lib/symbioticpy/symbiotic/{transform,verifier}.py; do 4 | [ -w ${file} ] || continue 5 | 6 | # Make sure that symbiotic uses the system-provided timeout(1) for itself. 7 | # Otherwise, when formally verifying the coreutils RPM package by Symbiotic, 8 | # the test-suite puts our instrumented binary of timeout(1) into ${PATH}, 9 | # causing it to recursively invoke whole symbiotic on each invocation of 10 | # timeout within symbiotic. 11 | ( 12 | set -x 13 | sed -e "s|'timeout'|'/usr/bin/timeout'|" -i ${file} 14 | ) 15 | done 16 | -------------------------------------------------------------------------------- /scripts/convert-clippy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import sys 5 | import re 6 | 7 | MESSAGE_PATTERN = r"^(.*?): (.*?)\s+-->\s+(.*?):(\d+):(\d+)(.*)" 8 | # we extract package path from the package_id 9 | # eg -- "package_id":"stratisd 3.6.5 (path+file:///builddir/build/BUILD/stratisd-3.6.5) 10 | PACKAGE_ID_PATTERN = r"path\+file://(.*)\)" 11 | # eg -- "package_id":"path+file:///builddir/build/BUILD/stratisd-3.6.5#stratisd@3.6.5" 12 | PACKAGE_ID_PATTERN2 = r"^path\+file:\/\/([^#]+)(?:#([^@]*))?(?:@(.+))?$" 13 | PACKAGE_ID_PATTERNS = [PACKAGE_ID_PATTERN, PACKAGE_ID_PATTERN2] 14 | 15 | def main(): 16 | for line in sys.stdin: 17 | try: 18 | item = json.loads(line) 19 | except Exception as e: 20 | print("rust-clippy: Error while converting results:", e, file=sys.stderr) 21 | sys.exit(1) 22 | 23 | if item["reason"] != "compiler-message": 24 | continue 25 | 26 | for pattern in PACKAGE_ID_PATTERNS: 27 | match = re.search(pattern, item["package_id"]) 28 | if match: 29 | break 30 | if not match: 31 | continue 32 | 33 | package_path = match.group(1) 34 | package = package_path[len("/builddir/build/BUILD/"):] 35 | package = package.split("/")[0] 36 | # we just need the builddir and package name, the relative file path is in the message 37 | package_path = "/builddir/build/BUILD/" + package 38 | 39 | match = re.search(MESSAGE_PATTERN, item["message"]["rendered"].strip(), re.DOTALL) 40 | if not match: 41 | continue 42 | 43 | message_type, message_content, file_path, line_number, column, rest = match.groups() 44 | 45 | print("Error: CLIPPY_WARNING:") 46 | print(f"{package_path}/{file_path}:{line_number}:{column}: {message_type}: {message_content}") 47 | for x, line in enumerate(rest.split("\n")): 48 | if x == 0 and line.strip() == "": 49 | continue 50 | print(f"# {line}") 51 | 52 | print() 53 | 54 | 55 | if __name__ == "__main__": 56 | main() 57 | -------------------------------------------------------------------------------- /scripts/enable-keep-going.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | for bin in make ninja; do 4 | # check whether ${bin} is installed 5 | bin_fp=/usr/bin/${bin} 6 | test -x ${bin_fp} || continue 7 | 8 | # move original ${bin} if not already moved 9 | bin_fp_orig=${bin_fp}.orig 10 | mv -nv ${bin_fp} ${bin_fp_orig} || continue 11 | 12 | # create a wrapper script named ${bin} 13 | case ${bin} in 14 | make) 15 | opt="-k" 16 | ;; 17 | ninja) 18 | opt="-k0" 19 | ;; 20 | esac 21 | printf "#!/bin/sh\nexec ${bin_fp_orig} ${opt} \"\$@\"\n" \ 22 | > ${bin_fp} || continue 23 | chmod 0755 ${bin_fp} || continue 24 | 25 | # print verbose output on success 26 | (set -x && ls -l ${bin_fp} && cat ${bin_fp}) 27 | done 28 | -------------------------------------------------------------------------------- /scripts/filter-infer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import sys 5 | import re 6 | 7 | 8 | def uninitFilter(bug): 9 | if bug["bug_type"] == "UNINITIALIZED_VALUE": 10 | if re.match("The value read from .*\[_\] was never initialized.", bug["qualifier"]): 11 | return True 12 | 13 | 14 | def biabductionFilter(bug): 15 | if bug["bug_type"] == "NULL_DEREFERENCE" or bug["bug_type"] == "RESOURCE_LEAK": 16 | for bugTrace in bug["bug_trace"]: 17 | if re.match("Skipping .*\(\):", bugTrace["description"]): 18 | return True 19 | if re.match("Switch condition is false. Skipping switch case", bugTrace["description"]): 20 | return True 21 | 22 | 23 | def inferboFilter(bug): 24 | if bug["bug_type"] == "BUFFER_OVERRUN_U5" or bug["bug_type"] == "INTEGER_OVERFLOW_U5": 25 | return True 26 | 27 | bufferOverRunTypes = [ 28 | "BUFFER_OVERRUN_L2", 29 | "BUFFER_OVERRUN_L3", 30 | "BUFFER_OVERRUN_L4", 31 | "BUFFER_OVERRUN_L5", 32 | "BUFFER_OVERRUN_S2", 33 | "INFERBO_ALLOC_MAY_BE_NEGATIVE", 34 | "INFERBO_ALLOC_MAY_BE_BIG"] 35 | 36 | integerOverFlowTypes = [ 37 | "INTEGER_OVERFLOW_L2", 38 | "INTEGER_OVERFLOW_L5", 39 | "INTEGER_OVERFLOW_U5"] 40 | 41 | if bug["bug_type"] in bufferOverRunTypes or bug["bug_type"] in integerOverFlowTypes: 42 | if ("+oo" in bug["qualifier"]) or ("-oo" in bug["qualifier"]): 43 | return True 44 | 45 | 46 | def lowerSeverityForDEADSTORE(bug): 47 | if bug["bug_type"] == "DEAD_STORE": 48 | bug["severity"] = "WARNING" 49 | 50 | 51 | def applyFilters(bugList, filterList): 52 | modifiedBugList = [] 53 | 54 | while bugList: 55 | bug = bugList.pop(0) 56 | bugIsFalseAlarm = False 57 | for filter in filterList: 58 | try: 59 | # if a filter returns true, then this bug is considered a 60 | # false alarm and will not be included in the final report 61 | # NOTE: a bug marked as a false alarm may not actually be 62 | # a false alarm 63 | if filter(bug): 64 | bugIsFalseAlarm = True 65 | break 66 | except: 67 | # if a filter fails on a bug, then the filter behaves as if 68 | # the bug was real 69 | bugIsFalseAlarm = False 70 | if not bugIsFalseAlarm: 71 | modifiedBugList.append(bug) 72 | 73 | return modifiedBugList 74 | 75 | 76 | def main(): 77 | bugList = json.load(sys.stdin) 78 | 79 | if "--only-transform" not in sys.argv: 80 | filterList = [] 81 | 82 | if "--no-biabduction" not in sys.argv: 83 | filterList += [biabductionFilter] 84 | 85 | if "--no-inferbo" not in sys.argv: 86 | filterList += [inferboFilter] 87 | 88 | if "--no-uninit" not in sys.argv: 89 | filterList += [uninitFilter] 90 | 91 | if "--no-dead-store" not in sys.argv: 92 | filterList += [lowerSeverityForDEADSTORE] 93 | 94 | bugList = applyFilters(bugList, filterList) 95 | 96 | 97 | firstBug = True 98 | 99 | for bug in bugList: 100 | if not firstBug: 101 | print() 102 | print("Error: INFER_WARNING:") 103 | for bugTrace in bug["bug_trace"]: 104 | print("%s:%s:%s: note: %s" % (bugTrace["filename"], bugTrace["line_number"], bugTrace["column_number"], bugTrace["description"])) 105 | print("%s:%s:%s: %s[%s]: %s" % (bug["file"], bug["line"], bug["column"], bug["severity"].lower(), bug["bug_type"], bug["qualifier"])) 106 | firstBug=False 107 | 108 | if __name__ == "__main__": 109 | main() 110 | -------------------------------------------------------------------------------- /scripts/find-unicode-control.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Find unicode control characters in source files 3 | 4 | By default the script takes one or more files or directories and looks for 5 | unicode control characters in all text files. To narrow down the files, provide 6 | a config file with the -c command line, defining a scan_exclude list, which 7 | should be a list of regular expressions matching paths to exclude from the scan. 8 | 9 | There is a second mode enabled with -p which when set to 'all', prints all 10 | control characters and when set to 'bidi', prints only the 9 bidirectional 11 | control characters. 12 | """ 13 | from __future__ import print_function 14 | 15 | import sys, os, argparse, re, unicodedata, subprocess 16 | import importlib 17 | import six 18 | from stat import * 19 | 20 | try: 21 | import magic 22 | except ImportError: 23 | magic = None 24 | 25 | def _unicode(line, encoding): 26 | if isinstance(line, str): 27 | return line 28 | return line.decode(encoding) 29 | 30 | import platform 31 | if platform.python_version()[0] == '2': 32 | _chr = unichr 33 | do_unicode = unicode 34 | else: 35 | _chr = chr 36 | do_unicode = _unicode 37 | 38 | scan_exclude = [r'\.git/', r'\.hg/', r'\.desktop$', r'ChangeLog$', r'NEWS$', 39 | r'\.ppd$', r'\.txt$', r'\.directory$'] 40 | scan_exclude_mime = [r'text/x-po$', r'text/x-tex$', r'text/x-troff$', 41 | r'text/html$'] 42 | verbose_mode = False 43 | 44 | # Print to stderr in verbose mode. 45 | def eprint(arg, **kwargs): 46 | if verbose_mode: 47 | six.print_(arg, file=sys.stderr, **kwargs) 48 | 49 | # Decode a single latin1 line. 50 | def decodeline(inf): 51 | return do_unicode(inf, 'utf-8') 52 | 53 | # Make a text string from a file, attempting to decode from latin1 if necessary. 54 | # Other non-utf-8 locales are not supported at the moment. 55 | def getfiletext(filename): 56 | text = None 57 | with open(filename) as infile: 58 | try: 59 | if detailed_mode: 60 | return [decodeline(inf) for inf in infile] 61 | except Exception as e: 62 | eprint('%s: %s' % (filename, e)) 63 | return None 64 | 65 | try: 66 | text = decodeline(''.join(infile)) 67 | except UnicodeDecodeError: 68 | eprint('%s: Retrying with latin1' % filename) 69 | try: 70 | text = ''.join([decodeline(inf) for inf in infile]) 71 | except Exception as e: 72 | eprint('%s: %s' % (filename, e)) 73 | if text: 74 | return set(text) 75 | else: 76 | return None 77 | 78 | def analyze_text_detailed(filename, text, disallowed, msg): 79 | line = 0 80 | warned = False 81 | for t in text: 82 | line = line + 1 83 | subset = [c for c in t if _chr(ord(c)) in disallowed] 84 | if subset: 85 | print('Error: UNICONTROL_WARNING:') 86 | print('%s:%d: warning: %s: %s\n' % (filename, line, msg, subset)) 87 | warned = True 88 | if not warned: 89 | eprint('%s: OK' % filename) 90 | 91 | # Look for disallowed characters in the text. We reduce all characters into a 92 | # set to speed up analysis. FIXME: Add a slow mode to get line numbers in files 93 | # that have these disallowed chars. 94 | def analyze_text(filename, text, disallowed, msg): 95 | if detailed_mode: 96 | analyze_text_detailed(filename, text, disallowed, msg) 97 | return 98 | 99 | if not text.isdisjoint(disallowed): 100 | print('Error: UNICONTROL_WARNING:') 101 | print('%s: warning: %s: %s\n' % (filename, msg, text & disallowed)) 102 | else: 103 | eprint('%s: OK' % filename) 104 | 105 | def get_mime(f): 106 | if magic: 107 | return magic.detect_from_filename(f).mime_type 108 | args = ['file', '--mime-type', f] 109 | proc = subprocess.Popen(args, stdout=subprocess.PIPE) 110 | m = [decodeline(x[:-1]) for x in proc.stdout][0].split(':')[1].strip() 111 | return m 112 | 113 | def should_read(f): 114 | # Fast check, just the file name. 115 | if [e for e in scan_exclude if re.search(e, f)]: 116 | return False 117 | 118 | # Slower check, mime type. 119 | m = get_mime(f) 120 | if not 'text/' in m \ 121 | or [e for e in scan_exclude_mime if re.search(e, m)]: 122 | return False 123 | return True 124 | 125 | # Get file text and feed into analyze_text. 126 | def analyze_file(f, disallowed, msg): 127 | eprint('%s: Reading file' % f) 128 | if should_read(f): 129 | text = getfiletext(f) 130 | if text: 131 | analyze_text(f, text, disallowed, msg) 132 | else: 133 | eprint('%s: SKIPPED' % f) 134 | 135 | # Actual implementation of the recursive descent into directories. 136 | def analyze_any(p, disallowed, msg, dirs_seen): 137 | try: 138 | stat_res = os.stat(p) 139 | except Exception as e: 140 | eprint('%s: %s' % (p, e)) 141 | return 142 | 143 | mode = stat_res.st_mode 144 | if S_ISDIR(mode): 145 | # avoid analyzing the same dir twice 146 | inode = stat_res.st_ino 147 | if inode: 148 | if inode in dirs_seen: 149 | return 150 | dirs_seen.add(inode) 151 | 152 | analyze_dir(p, disallowed, msg, dirs_seen) 153 | elif S_ISREG(mode): 154 | analyze_file(p, disallowed, msg) 155 | else: 156 | eprint('%s: UNREADABLE' % p) 157 | 158 | # Recursively analyze files in the directory. 159 | def analyze_dir(d, disallowed, msg, dirs_seen): 160 | for f in os.listdir(d): 161 | analyze_any(os.path.join(d, f), disallowed, msg, dirs_seen) 162 | 163 | def analyze_paths(paths, disallowed, msg, dirs_seen): 164 | for p in paths: 165 | analyze_any(p, disallowed, msg, dirs_seen) 166 | 167 | # All control characters. We omit the ascii control characters. 168 | def nonprint_unicode(c): 169 | cat = unicodedata.category(c) 170 | if cat.startswith('C') and cat != 'Cc': 171 | return True 172 | return False 173 | 174 | if __name__ == '__main__': 175 | parser = argparse.ArgumentParser(description="Look for Unicode control characters") 176 | parser.add_argument('path', metavar='path', nargs='+', 177 | help='Sources to analyze') 178 | parser.add_argument('-p', '--nonprint', required=False, 179 | type=str, choices=['all', 'bidi'], 180 | help='Look for either all non-printable unicode characters or bidirectional control characters.') 181 | parser.add_argument('-v', '--verbose', required=False, action='store_true', 182 | help='Verbose mode.') 183 | parser.add_argument('-d', '--detailed', required=False, action='store_true', 184 | help='Print line numbers where characters occur.') 185 | parser.add_argument('-t', '--notests', required=False, 186 | action='store_true', help='Exclude tests (basically test.* as a component of path).') 187 | parser.add_argument('-c', '--config', required=False, type=str, 188 | help='Configuration file to read settings from.') 189 | 190 | args = parser.parse_args() 191 | verbose_mode = args.verbose 192 | detailed_mode = args.detailed 193 | 194 | if not args.nonprint: 195 | # Formatting control characters in the unicode space. This includes the 196 | # bidi control characters. 197 | disallowed = set(_chr(c) for c in range(sys.maxunicode) if \ 198 | unicodedata.category(_chr(c)) == 'Cf') 199 | 200 | msg = 'unicode control characters' 201 | elif args.nonprint == 'all': 202 | # All control characters. 203 | disallowed = set(_chr(c) for c in range(sys.maxunicode) if \ 204 | nonprint_unicode(_chr(c))) 205 | 206 | msg = 'disallowed characters' 207 | else: 208 | # Only bidi control characters. 209 | disallowed = set([ 210 | _chr(0x202a), _chr(0x202b), _chr(0x202c), _chr(0x202d), _chr(0x202e), 211 | _chr(0x2066), _chr(0x2067), _chr(0x2068), _chr(0x2069)]) 212 | msg = 'bidirectional control characters' 213 | 214 | if args.config: 215 | spec = importlib.util.spec_from_file_location("settings", args.config) 216 | settings = importlib.util.module_from_spec(spec) 217 | spec.loader.exec_module(settings) 218 | if hasattr(settings, 'scan_exclude'): 219 | scan_exclude = scan_exclude + settings.scan_exclude 220 | if hasattr(settings, 'scan_exclude_mime'): 221 | scan_exclude_mime = scan_exclude_mime + settings.scan_exclude_mime 222 | 223 | if args.notests: 224 | scan_exclude = scan_exclude + [r'/test[^/]+/'] 225 | 226 | dirs_seen = set() 227 | 228 | analyze_paths(args.path, disallowed, msg, dirs_seen) 229 | -------------------------------------------------------------------------------- /scripts/inject-clippy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ORIGINAL_LOCATION="/usr/bin/cargo" 4 | NEW_LOCATION="/usr/bin/cargo_original" 5 | 6 | # create empty or truncate existing (if --skip-init is in effect) capture file 7 | runuser mockbuild -c "truncate --size=0 /builddir/clippy-output.txt" 8 | 9 | if [ -x "$NEW_LOCATION" ]; then 10 | # binary already moved (most likely by a previous run of this script) 11 | exit 0 12 | fi 13 | 14 | mv -v $ORIGINAL_LOCATION $NEW_LOCATION 15 | 16 | sed "s|REPLACE_ME|$NEW_LOCATION|g" > $ORIGINAL_LOCATION << 'EOF' 17 | #!/bin/bash 18 | 19 | # look for "build" in command-line args 20 | for ((i=1; i<$#; i++)); do 21 | if [[ "${!i}" == "build" ]]; then 22 | # found! --> execute the command with "build" substituted by "clippy" 23 | set -x 24 | REPLACE_ME "${@:1:i-1}" clippy "${@:i+1}" --message-format=json >> /builddir/clippy-output.txt 25 | break 26 | fi 27 | done 28 | 29 | # execute the original command in any case 30 | exec REPLACE_ME "$@" 31 | EOF 32 | 33 | chmod +x $ORIGINAL_LOCATION 34 | -------------------------------------------------------------------------------- /scripts/install-infer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # $1 -- Infer archive path 4 | # $2 -- infer-out path 5 | 6 | create_wrapper() 7 | { 8 | # $1 -- name of a compiler 9 | # $2 -- type of a compiler for Infer's --force-integration option 10 | # -- 'cc' for C-like languages 11 | # -- 'javac' for Java 12 | # $3 -- infer-out path 13 | 14 | echo '#!/bin/bash 15 | 16 | trap unlock_on_exit EXIT INT TERM 17 | 18 | compiler="'"$1"'" 19 | compiler_original="${compiler}-original" 20 | all_options=("$@") 21 | infer_dir="'"$3"'" 22 | skip_capture=false 23 | lock_dir="/tmp/infer.lockdir" 24 | pid_file="${lock_dir}/PID" 25 | 26 | function lock { 27 | mkdir "${lock_dir}" > /dev/null 2>&1 && echo $$ > ${pid_file} 28 | } 29 | 30 | # unlocks the lock even if this process isnt the owner 31 | function unlock { 32 | PID=$(cat ${pid_file} 2>&1) 33 | rm -rf ${lock_dir} 34 | } 35 | 36 | # checks if the lock owner still exists -- if not, the lock is removed 37 | function lock_failed { 38 | PID=$(cat ${pid_file} 2>&1) # 2>&1 -- do not print anything if cat fails 39 | 40 | # an error while reading PID file, e.g. PID file wasnt created yet 41 | if [ $? != 0 ]; then 42 | return 43 | fi 44 | 45 | # lock owner doesnt exist anymore -- remove the lock 46 | if ! kill -0 ${PID} &>/dev/null; then 47 | unlock 48 | fi 49 | } 50 | 51 | # checks if the lock owner is this script -- if so, the lock is removed 52 | function unlock_on_exit { 53 | PID=$(cat ${pid_file} 2>&1) # 2>&1 -- do not print anything if cat fails 54 | 55 | # if PID file was loaded correctly 56 | if [ $? == 0 ]; then 57 | # if this script still owns the lock then remove it 58 | if [ $$ == ${PID} ]; then 59 | unlock 60 | fi 61 | fi 62 | 63 | # restore all passed options 64 | set -- "${all_options[@]}" 65 | 66 | # return code is carried back to a caller 67 | ${compiler_original} "$@" 68 | exit $? 69 | } 70 | 71 | if [[ $# -eq 1 && "$1" == *"@/tmp/"* ]] ; 72 | then 73 | skip_capture=true 74 | set -- "/usr/bin/${compiler_original}" 75 | fi 76 | 77 | for var in "$@" 78 | do 79 | if [[ "$var" =~ conftest[0-9]*\.c$ ]] ; 80 | then 81 | skip_capture=true 82 | fi 83 | done 84 | 85 | if [ "${skip_capture}" = false ] 86 | then 87 | # delete incompatible options of Infers clang 88 | for arg do 89 | shift 90 | [ "$arg" = "-fstack-clash-protection" ] && continue 91 | [ "$arg" = "-flto=auto" ] && continue 92 | [ "$arg" = "-flto=jobserver" ] && continue 93 | [ "$arg" = "-ffat-lto-objects" ] && continue 94 | [[ "$arg" =~ "-flto-jobs=[0-9]*" ]] && continue 95 | [ "$arg" = "-flto=thin" ] && continue 96 | [ "$arg" = "-flto=full" ] && continue 97 | [ "$arg" = "-fsplit-lto-unit" ] && continue 98 | [ "$arg" = "-fvirtual-function-elimination" ] && continue 99 | [ "$arg" = "-flto=full" ] && continue 100 | [ "$arg" = "-fwhole-program-vtables" ] && continue 101 | [ "$arg" = "-fno-leading-underscore" ] && continue 102 | [ "$arg" = "-mno-avx256-split-unaligned-load" ] && continue 103 | [ "$arg" = "-mno-avx256-split-unaligned-store" ] && continue 104 | set -- "$@" "$arg" 105 | done 106 | 107 | # critical section 108 | while : 109 | do 110 | if lock 111 | then 112 | # lock acquired 113 | # logging 114 | >&2 echo "" 115 | >&2 echo "NOTE: INFER: ${compiler}-wrapper: running capture phase" 116 | if infer capture --reactive -o ${infer_dir} --force-integration' "$2" '-- ${compiler} "$@" 1>&2 117 | then 118 | >&2 echo "NOTE: INFER: ${compiler}-wrapper: successfully captured: \"${compiler} $@\"" 119 | else 120 | >&2 echo "WARNING: INFER: ${compiler}-wrapper: unsuccessfully captured: \"${compiler} $@\"" 121 | fi 122 | >&2 echo "" 123 | 124 | # the script terminates in the unlock function 125 | unlock 126 | break 127 | #else 128 | # lock_failed 129 | fi 130 | done 131 | fi' > "/usr/bin/$1" 132 | 133 | if ! chmod +x "/usr/bin/$1" 134 | then 135 | echo "ERROR: INFER: install-infer.sh: Failed to add +x permission to /usr/bin/$1" 136 | exit 1 137 | fi 138 | } 139 | 140 | 141 | # install Infer 142 | if ! cd /opt 143 | then 144 | echo "ERROR: INFER: install-infer.sh: Failed to open /opt directory" 145 | exit 1 146 | fi 147 | 148 | if ! tar -xf "$1" -C /opt 149 | then 150 | echo "ERROR: INFER: install-infer.sh: Failed to extract an Infer archive $1" 151 | exit 1 152 | else 153 | echo "NOTE: INFER: install-infer.sh: Infer archive extracted successfully" 154 | fi 155 | 156 | INFER_DIRS=(/opt/infer-linux*) 157 | INFER_DIR=${INFER_DIRS[0]} 158 | 159 | if ! rm "$1" 160 | then 161 | echo "ERROR: INFER: install-infer.sh: Failed to delete an Infer archive $1" 162 | exit 1 163 | fi 164 | 165 | if [ -f /usr/bin/infer ] || ln -sf "${INFER_DIR}/bin/infer" /usr/bin/infer 166 | then 167 | echo "NOTE: INFER: install-infer.sh: Infer symlink created successfully" 168 | else 169 | echo "ERROR: INFER: install-infer.sh: Failed to create a symlink to ${INFER_DIR}/bin/infer" 170 | exit 1 171 | fi 172 | 173 | # test if the symlink works 174 | if ! infer --version > /dev/null 2>&1 175 | then 176 | echo "ERROR: INFER: install-infer.sh: Failed to run 'infer --version' to test a symlink to ${INFER_DIR}/bin/infer" 177 | exit 1 178 | else 179 | echo "NOTE: INFER: install-infer.sh: Infer installed successfully" 180 | fi 181 | 182 | # remove possible leftovers from previous run 183 | rm -rf /tmp/infer.lockdir "$2" > /dev/null 2>&1 184 | 185 | # create wrappers for compilers, this script is executed after all the dependencies are installed, 186 | # so all the necessary compilers should be already installed 187 | declare -a ccompilers=( "8cc" 188 | "9cc" 189 | "ack" 190 | "c++" 191 | "ccomp" 192 | "chibicc" 193 | "clang" 194 | "cproc" 195 | "g++" 196 | "gcc" 197 | "icc" 198 | "icpc" 199 | "lacc" 200 | "lcc" 201 | "openCC" 202 | "opencc" 203 | "pcc" 204 | "scc" 205 | "sdcc" 206 | "tcc" 207 | "vc" 208 | "x86_64-redhat-linux-c++" 209 | "x86_64-redhat-linux-g++" 210 | "x86_64-redhat-linux-gcc" 211 | "x86_64-redhat-linux-gcc-10") 212 | 213 | for c in "${ccompilers[@]}" 214 | do 215 | if [ -f "/usr/bin/${c}-original" ] || mv "/usr/bin/${c}" "/usr/bin/${c}-original" > /dev/null 2>&1 216 | then 217 | create_wrapper "${c}" cc "$2" 218 | echo "NOTE: INFER: install-infer.sh: /usr/bin/${c} wrapper created successfully" 219 | fi 220 | done 221 | -------------------------------------------------------------------------------- /scripts/patch-rawbuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | need_for_build() { 4 | while test -n "$1"; do 5 | if test "--suffix" = "$1"; then 6 | shift 7 | if test "_RAWBUILD" = "$1"; then 8 | return 0 9 | fi 10 | fi 11 | 12 | shift 13 | done 14 | 15 | return 1 16 | } 17 | 18 | if need_for_build "$@"; then 19 | echo "$0: applying a patch annotated by _RAWBUILD" >&2 20 | /usr/bin/patch "$@" 21 | else 22 | echo "$0: ignoring a patch not annotated by _RAWBUILD" >&2 23 | dd of=/dev/null status=noxfer 2>/dev/null 24 | fi 25 | -------------------------------------------------------------------------------- /scripts/run-bandit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export LC_ALL="C" 3 | 4 | # $1 positional argument is security level definition 5 | SEC_LEVEL=$1 6 | shift 7 | 8 | # Debug 9 | echo "[DEBUG] SEC_LEVEL = $SEC_LEVEL" >&2 10 | echo "[DEBUG] TARGET_PATH = $*" >&2 11 | 12 | if [[ -z "${SEC_LEVEL// }" ]]; then 13 | echo "[ERROR] SEC_LEVEL parameter is empty" >&2 14 | exit 1 15 | elif [[ -z "$*" ]]; then 16 | echo "[ERROR] TARGET_PATH parameter is empty" >&2 17 | exit 1 18 | fi 19 | 20 | # Find all python files and run bandit analysis 21 | find "$@" -type f -print0 \ 22 | | xargs -0 sh -c 'for i in "$@"; do { printf "%s\n" "$i" | grep "\\.py\$" \ 23 | || head -n1 "$i" | grep --text "^#!.*python" 24 | } >/dev/null && readlink -f "$i"; done' "$0" \ 25 | | sort -V | tee /dev/fd/2 \ 26 | | xargs -r bandit -n -1 -r $SEC_LEVEL --format custom \ 27 | | tee /dev/fd/2 28 | -------------------------------------------------------------------------------- /scripts/run-pylint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # shellcheck disable=SC2317 3 | 4 | export LC_ALL="C" 5 | 6 | # this output format was used by Prospector 7 | export MSG_TEMPLATE='{abspath}:{line}:{column}: {msg_id}[pylint]: {msg}' 8 | 9 | # implementation of the script that filters python scripts 10 | filter_python_scripts() { 11 | for i in "$@"; do 12 | # match by file name suffix 13 | if [[ "$i" =~ ^.*\.py$ ]]; then 14 | echo "$i" 15 | echo -n . >&2 16 | continue 17 | fi 18 | 19 | # match by shebang (executable files only) 20 | RE_SHEBANG='^#!.*python[0-9.]*' 21 | if test -x "$i" && head -n1 "$i" | grep -q --text "$RE_SHEBANG"; then 22 | echo "$i" 23 | echo -n . >&2 24 | fi 25 | done 26 | } 27 | 28 | # store a script that filters python scripts to a variable 29 | FILTER_SCRIPT="$(declare -f filter_python_scripts) 30 | filter_python_scripts"' "$@"' 31 | 32 | # find all python scripts and run pylint on them 33 | echo -n "Looking for python scripts..." >&2 34 | find "$@" -type f -print0 \ 35 | | xargs -0 /bin/bash -c "$FILTER_SCRIPT" "$0" \ 36 | | { sort -uV && echo " done" >&2; } \ 37 | | xargs -r /bin/bash -xc 'pylint -rn --msg-template "${MSG_TEMPLATE}" "$@"' "$0" \ 38 | | grep --invert-match '^\** Module ' 39 | 40 | # propagage the exit status from the second xargs 41 | exit "${PIPESTATUS[3]}" 42 | -------------------------------------------------------------------------------- /scripts/run-scan.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SELF="$0" 3 | 4 | msg() { 5 | printf "\n%s: %s\n" "$SELF" "$*" >&2 6 | } 7 | 8 | die() { 9 | msg "error: $*" 10 | exit 1 11 | } 12 | 13 | warn() { 14 | msg "warning: $*" 15 | } 16 | 17 | warn_not_in_path() { 18 | warn "$1 not found in \$PATH: $PATH" 19 | } 20 | 21 | RES_DIR="$1" 22 | test -d "$RES_DIR" || die "invalid RES_DIR given: $RES_DIR" 23 | 24 | BUILD_CMD="$2" 25 | test -n "$BUILD_CMD" || die "no BUILD_CMD given" 26 | 27 | FILTER_CMD="$3" 28 | test -n "$FILTER_CMD" || FILTER_CMD="cat" 29 | 30 | BASE_ERR="$4" 31 | if test -n "$BASE_ERR"; then 32 | test -r "$BASE_ERR" || die "invalid BASE_ERR given: $BASE_ERR" 33 | fi 34 | 35 | # plug cswrap 36 | CSWRAP_LNK_DIR="$(cswrap --print-path-to-wrap)" 37 | test -d "$CSWRAP_LNK_DIR" || die "cswrap not found in \$PATH: $PATH" 38 | export PATH="${CSWRAP_LNK_DIR}:$PATH" 39 | export CSWRAP_CAP_FILE="${RES_DIR}/cswrap-capture.txt" 40 | true >"$CSWRAP_CAP_FILE" || die "write failed: $CSWRAP_CAP_FILE" 41 | 42 | if test -z "$CSWRAP_DEL_CFLAGS" && test -z "$CSWRAP_DEL_CXXFLAGS" \ 43 | && test -z "$CSWRAP_ADD_CFLAGS" && test -z "$CSWRAP_ADD_CXXFLAGS" 44 | then 45 | # use the default GCC flags overrides 46 | export CSWRAP_DEL_CFLAGS="-Werror:-fdiagnostics-color:-fdiagnostics-color=always" 47 | export CSWRAP_DEL_CXXFLAGS="$CSWRAP_DEL_CFLAGS" 48 | export CSWRAP_ADD_CXXFLAGS="-Wall:-Wextra" 49 | export CSWRAP_ADD_CFLAGS="$CSWRAP_ADD_CFLAGS:-Wno-unknown-pragmas" 50 | fi 51 | 52 | # plug csclng (if available) 53 | msg "Looking for csclng..." 54 | CSCLNG_LNK_DIR="$(csclng --print-path-to-wrap)" 55 | if test -d "$CSCLNG_LNK_DIR"; then 56 | if (set -x; clang --version); then 57 | PATH="${CSCLNG_LNK_DIR}:$PATH" 58 | else 59 | warn_not_in_path clang 60 | fi 61 | else 62 | warn_not_in_path csclng 63 | fi 64 | 65 | # plug cscppc (if available) 66 | msg "Looking for cscppc..." 67 | CSCPPC_LNK_DIR="$(cscppc --print-path-to-wrap)" 68 | if test -d "$CSCPPC_LNK_DIR"; then 69 | if (set -x; cppcheck --version); then 70 | PATH="${CSCPPC_LNK_DIR}:$PATH" 71 | else 72 | warn_not_in_path cppcheck 73 | fi 74 | else 75 | warn_not_in_path cscppc 76 | fi 77 | 78 | # plug Coverity Analysis (if available in $PATH) 79 | msg "Looking for Coverity Analysis..." 80 | BUILD_CMD_WRAP= 81 | COV_INT_DIR= 82 | if (set -x; cov-build --ident && cov-analyze --ident && cov-format-errors --ident) 83 | then 84 | COV_INT_DIR="${RES_DIR}/cov" 85 | BUILD_CMD_WRAP="cov-build --dir=${COV_INT_DIR}" 86 | fi 87 | 88 | if test -r CMakeCache.txt; then 89 | # if CMakeCache.txt contains absolute path to the compiler, check that it 90 | # matches the path to our wrapper 91 | for i in C CXX; do 92 | echo "message(STATUS \${CMAKE_${i}_COMPILER})" > "${RES_DIR}/print-${i}.cmake" 93 | abs_path="$(cmake -NC CMakeCache.txt -P "${RES_DIR}/print-${i}.cmake" \ 94 | 2>/dev/null | cut -c4-)" 95 | test -n "$abs_path" || continue 96 | rel_path="$(basename "$abs_path")" 97 | exp_path="$(command -v "$rel_path")" 98 | if test "$abs_path" != "$exp_path"; then 99 | die "$(realpath CMakeCache.txt) contains \ 100 | 'CMAKE_${i}_COMPILER=${abs_path}' instead of \ 101 | 'CMAKE_${i}_COMPILER=${exp_path}', which is required to wrap \ 102 | the compiler. Please run cmake through csbuild to fix this." 103 | fi 104 | done 105 | fi 106 | 107 | # run the specified BUILD_CMD 108 | msg "Running the build!" 109 | (set -x; $BUILD_CMD_WRAP "$SHELL" -xc "$BUILD_CMD") 110 | 111 | # check for possible build failure 112 | test "$?" = 0 || exit 125 113 | 114 | # process the resulting capture 115 | RAW_CURR_ERR="${RES_DIR}/raw-current.err" 116 | (set -x; csgrep --quiet --event 'error|warning' \ 117 | --remove-duplicates "${CSWRAP_CAP_FILE}" \ 118 | | csgrep --invert-match --checker CLANG_WARNING --event error \ 119 | | csgrep --invert-match --checker CPPCHECK_WARNING \ 120 | --event 'preprocessorErrorDirective|syntaxError' \ 121 | > "$RAW_CURR_ERR") 122 | 123 | # run Coverity Analysis (if anything has been captured) 124 | if test -n "$COV_INT_DIR" && test -e "${COV_INT_DIR}/emit/"*/emit-db.write-lock 125 | then 126 | msg "Running Coverity Analysis..." 127 | (set -x; cov-analyze "--dir=${COV_INT_DIR}" --security --concurrency \ 128 | && cov-format-errors "--dir=${COV_INT_DIR}" --json-output-v2 /dev/stdout \ 129 | | csgrep --prune-events=1 >> "$RAW_CURR_ERR") 130 | fi 131 | 132 | # process the given filters and sort the results 133 | CURR_ERR="${RES_DIR}/current.err" 134 | (set -x; csgrep --invert-match --path '^ksh-.*[0-9]+\.c$' <"$RAW_CURR_ERR" \ 135 | | csgrep --invert-match --path 'CMakeFiles/CMakeTmp|conftest.c' \ 136 | | cssort --key=path \ 137 | | eval $FILTER_CMD >"$CURR_ERR") 138 | 139 | # compare the results with base (if we got one) 140 | ADDED_ERR="${RES_DIR}/added.err" 141 | FIXED_ERR="${RES_DIR}/fixed.err" 142 | if test -n "$BASE_ERR"; then 143 | csdiff -z "$BASE_ERR" "$CURR_ERR" > "$ADDED_ERR" 144 | csdiff -z "$CURR_ERR" "$BASE_ERR" > "$FIXED_ERR" 145 | else 146 | rm -f "$ADDED_ERR" "$FIXED_ERR" 147 | fi 148 | 149 | # return the according exit code 150 | if grep "^Error" "$ADDED_ERR" >/dev/null 2>&1; then 151 | # new defects found! 152 | exit 7 153 | else 154 | exit 0 155 | fi 156 | -------------------------------------------------------------------------------- /scripts/run-shellcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # shellcheck disable=SC2319 3 | 4 | export LC_ALL="C" 5 | 6 | # how many shell scripts we pass to shellcheck at a time 7 | test -n "$SC_BATCH" || export SC_BATCH=1 8 | test 0 -lt "$SC_BATCH" || exit $? 9 | 10 | # how many shellcheck processes we run in parallel 11 | SC_JOBS=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) 12 | export SC_JOBS 13 | 14 | # how long we wait (wall-clock time) for a single shellcheck process to finish 15 | test -n "$SC_TIMEOUT" || export SC_TIMEOUT=30 16 | test 0 -lt "$SC_TIMEOUT" || exit $? 17 | 18 | # directory for shellcheck results 19 | test -n "$SC_RESULTS_DIR" || export SC_RESULTS_DIR="./shellcheck-results" 20 | 21 | # create the directory for shellcheck results and check we can write to it 22 | mkdir "${SC_RESULTS_DIR}" || exit $? 23 | touch "${SC_RESULTS_DIR}/empty.json" || exit $? 24 | 25 | # check whether shellcheck supports --format=json1 26 | export SC_RESULTS_{BEG,END} 27 | if shellcheck --help 2>/dev/null | grep -q json1; then 28 | SC_OPTS=(--format=json1) 29 | SC_RESULTS_BEG= 30 | SC_RESULTS_END= 31 | else 32 | # compatibility workaround for old versions of shellcheck 33 | SC_OPTS=(--format=json) 34 | SC_RESULTS_BEG='{"comments":' 35 | SC_RESULTS_END='}' 36 | fi 37 | 38 | # check whether shellcheck supports --external-sources 39 | if shellcheck --help 2>/dev/null | grep -q external-sources; then 40 | SC_OPTS+=(--external-sources) 41 | fi 42 | 43 | # check whether shellcheck supports --source-path 44 | if shellcheck --help 2>/dev/null | grep -q source-path; then 45 | sp= 46 | for dir in "$@"; do 47 | # search path works only for directories 48 | test -d "$dir" || continue 49 | 50 | # append a single directory from cmd-line args of this script 51 | sp="${sp}:${dir}" 52 | done 53 | if test -n "$sp"; then 54 | # pass the colon-delimited list of dirs via --source-path to shellcheck 55 | SC_OPTS+=(--source-path="${sp#:}") 56 | fi 57 | fi 58 | 59 | # implementation of the script that filters shell scripts 60 | filter_shell_scripts() { 61 | for i in "$@"; do 62 | # match by file name suffix 63 | if [[ "$i" =~ ^.*\.(ash|bash|bats|dash|ksh|sh)$ ]]; then 64 | echo "$i" 65 | echo -n . >&2 66 | continue 67 | fi 68 | 69 | # match by shebang (executable files only) 70 | RE_SHEBANG='^\s*((#|!)|(#\s*!)|(!\s*#))\s*(/usr(/local)?)?/bin/(env\s+)?(ash|bash|bats|dash|ksh|sh)\b' 71 | if test -x "$i" && head -n1 "$i" | grep -qE --text "$RE_SHEBANG"; then 72 | echo "$i" 73 | echo -n . >&2 74 | fi 75 | done 76 | } 77 | 78 | # store a script that filters shell scripts to a variable 79 | FILTER_SCRIPT="$(declare -f filter_shell_scripts) 80 | filter_shell_scripts"' "$@"' 81 | 82 | # function that creates a separate JSON file if shellcheck detects anything 83 | wrap_shellcheck() { 84 | dst="${SC_RESULTS_DIR}/sc-$$.json" 85 | log="${dst%.json}.log" 86 | 87 | # compatibility workaround for old versions of shellcheck 88 | echo -n "$SC_RESULTS_BEG" > "$dst" 89 | 90 | (set -x && timeout "${SC_TIMEOUT}" shellcheck "${SC_OPTS[@]}" "$@" >> "$dst" 2> "$log") 91 | EC=$? 92 | 93 | # compatibility workaround for old versions of shellcheck 94 | echo -n "$SC_RESULTS_END" >> "$dst" 95 | 96 | case $EC in 97 | 0) 98 | # no findings detected -> remove the output files 99 | rm -f "$dst" "$log" 100 | ;; 101 | 102 | 1) 103 | # findings detected -> successful run 104 | if [ -n "$(<"$log")" ]; then 105 | # something printed to stderr -> record an internal error of shellcheck 106 | sed -re 's|^(shellcheck): ([^:]+): (.*)$|\2: internal error: \3 <--[\1]|' "$log" > "$dst" 107 | else 108 | rm -f "$log" 109 | fi 110 | return 0 111 | ;; 112 | 113 | *) 114 | # propagate other errors 115 | return $EC 116 | ;; 117 | esac 118 | } 119 | 120 | # store a script that filters shell scripts to a variable (and explicitly 121 | # propagate the ${SC_OPTS} shell array, which cannot be easily exported) 122 | SC_WRAP_SCRIPT="$(declare -p SC_OPTS) 123 | $(declare -f wrap_shellcheck) 124 | wrap_shellcheck"' "$@"' 125 | 126 | # find all shell scripts and run shellcheck on them 127 | echo -n "Looking for shell scripts..." >&2 128 | find "$@" -type f -print0 \ 129 | | xargs -0 /bin/bash -c "$FILTER_SCRIPT" "$0" \ 130 | | { sort -uV && echo " done" >&2; } \ 131 | | xargs -rn "${SC_BATCH}" --max-procs="${SC_JOBS}" \ 132 | /bin/bash -c "$SC_WRAP_SCRIPT" "$0" 133 | -------------------------------------------------------------------------------- /tests/simple_build/clang.fmf: -------------------------------------------------------------------------------- 1 | summary: Test analysis using Clang analyzer 2 | test: ./test.sh 3 | framework: beakerlib 4 | environment: 5 | TEST_PACKAGE: units 6 | TEST_TOOL: clang 7 | component: 8 | - csmock-plugin-clang 9 | recommend: 10 | - csmock-plugin-clang 11 | duration: 1h 12 | -------------------------------------------------------------------------------- /tests/simple_build/gcc.fmf: -------------------------------------------------------------------------------- 1 | summary: Test analysis using GCC analyzer 2 | test: ./test.sh 3 | framework: beakerlib 4 | environment: 5 | TEST_PACKAGE: units 6 | TEST_TOOL: gcc 7 | CSMOCK_EXTRA_OPTS: --gcc-analyze 8 | component: 9 | - csmock 10 | recommend: 11 | - csmock 12 | duration: 1h 13 | -------------------------------------------------------------------------------- /tests/simple_build/shellcheck.fmf: -------------------------------------------------------------------------------- 1 | summary: Test analysis using ShellCheck 2 | test: ./test.sh 3 | framework: beakerlib 4 | environment: 5 | TEST_PACKAGE: dracut 6 | TEST_TOOL: shellcheck 7 | component: 8 | - csmock 9 | recommend: 10 | - csmock 11 | duration: 1h 12 | -------------------------------------------------------------------------------- /tests/simple_build/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: dict+=/usr/share/beakerlib/dictionary.vim cpt=.,w,b,u,t,i,k 3 | . /usr/share/beakerlib/beakerlib.sh || exit 1 4 | 5 | CSMOCK_EXTRA_OPTS="${CSMOCK_EXTRA_OPTS:-}" 6 | TEST_PACKAGE="${TEST_PACKAGE:-}" 7 | TEST_TOOL="${TEST_TOOL:-}" 8 | TEST_USER="csmock" 9 | 10 | # Add CS Koji 11 | BEAKERLIB_rpm_fetch_base_url+=( "https://kojihub.stream.centos.org/kojifiles/packages/" ) 12 | BEAKERLIB_rpm_packageinfo_base_url+=( "https://kojihub.stream.centos.org/koji/" ) 13 | 14 | rlJournalStart 15 | rlPhaseStartSetup 16 | # use the latest csutils in the Testing Farm 17 | if test "$PACKIT_FULL_REPO_NAME"; then 18 | # By default the testing-farm-tag-repository has higher priority 19 | # than our development COPR repo. Therefore, we need to flip the 20 | # priorities and update the packages manually. 21 | rlRun "echo 'priority=1' >> /etc/yum.repos.d/group_codescan-csutils-*.repo" 22 | rlRun "yum upgrade -y 'cs*'" 23 | fi 24 | 25 | if [ -z "$TEST_PACKAGE" ]; then 26 | rlDie "TEST_PACKAGE parameter is empty or undefined" 27 | fi 28 | 29 | if [ -z "$TEST_TOOL" ]; then 30 | rlDie "TEST_TOOL parameter is empty or undefined" 31 | fi 32 | 33 | # create a tmpdir 34 | rlRun "tmp=\$(mktemp -d)" 0 "Create tmp directory" 35 | rlRun "pushd \$tmp" 36 | 37 | # fetch the SRPM 38 | rlRun "yum -y install $TEST_PACKAGE" 39 | rlRun "rlFetchSrcForInstalled $TEST_PACKAGE" 40 | rlRun "SRPM=\$(find . -name '$TEST_PACKAGE-*.src.rpm')" 41 | 42 | # add a user for mock 43 | rlRun "userdel -r $TEST_USER" 0,6 44 | rlRun "useradd -m -d /home/$TEST_USER -G mock $TEST_USER" 45 | rlRun "cp $SRPM /home/$TEST_USER" 46 | 47 | if ! rlGetPhaseState; then 48 | rlDie "'$TEST_PACKAGE' sources could not be fetched" 49 | fi 50 | rlPhaseEnd 51 | 52 | rlPhaseStartTest "Analyze $TEST_PACKAGE using $TEST_TOOL" 53 | rlRun "su - $TEST_USER -c 'csmock -t $TEST_TOOL $CSMOCK_EXTRA_OPTS \"$SRPM\" --install pam --gcc-add-flag=-std=gnu11'" 0 \ 54 | "Analyze $SRPM using $TEST_TOOL analyzer" 55 | rlFileSubmit "/home/$TEST_USER/$TEST_PACKAGE"*.tar.xz "$SRPM-$TEST_TOOL.tar.xz" 56 | rlPhaseEnd 57 | 58 | rlPhaseStartCleanup 59 | rlRun "userdel -r $TEST_USER" 60 | rlRun "popd" 61 | rlRun "rm -r \$tmp" 0 "Remove tmp directory" 62 | rlPhaseEnd 63 | rlJournalEnd 64 | -------------------------------------------------------------------------------- /upload-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | SELF="$0" 4 | TAG="$1" 5 | TOKEN="$2" 6 | 7 | NAME="csmock" 8 | NV="${TAG}" 9 | 10 | usage() { 11 | printf "Usage: %s TAG TOKEN\n" "$SELF" >&2 12 | exit 1 13 | } 14 | 15 | die() { 16 | printf "%s: error: %s\n" "$SELF" "$*" >&2 17 | exit 1 18 | } 19 | 20 | # check arguments 21 | test "$TAG" = "$(git describe --tags "$TAG")" || usage 22 | test -n "$TOKEN" || usage 23 | 24 | # dump a tarball 25 | SRC_TAR="${NV}.tar" 26 | git archive --prefix="$NV/" --format="tar" HEAD -- . > "$SRC_TAR" \ 27 | || die "failed to export sources" 28 | 29 | # produce .tar.gz 30 | TAR_GZ="${NV}.tar.gz" 31 | gzip -c "$SRC_TAR" > "$TAR_GZ" || die "failed to create $TAR_GZ" 32 | 33 | # produce .tar.xz 34 | TAR_XZ="${NV}.tar.xz" 35 | xz -c "$SRC_TAR" > "$TAR_XZ" || die "failed to create $TAR_XZ" 36 | 37 | # sign the tarballs 38 | for file in "$TAR_GZ" "$TAR_XZ"; do 39 | gpg --armor --detach-sign "$file" || die "tarball signing failed" 40 | test -f "${file}.asc" || die "tarball signature was not created" 41 | done 42 | 43 | # file to store response from GitHub API 44 | JSON="./${NV}-github-relase.js" 45 | 46 | # create a new release on GitHub 47 | curl "https://api.github.com/repos/csutils/${NAME}/releases" \ 48 | -o "$JSON" --fail --verbose \ 49 | --header "Authorization: token $TOKEN" \ 50 | --data '{ 51 | "tag_name": "'"$TAG"'", 52 | "target_commitish": "main", 53 | "name": "'"$NV"'", 54 | "draft": false, 55 | "prerelease": false 56 | }' || exit $? 57 | 58 | # parse upload URL from the response 59 | UPLOAD_URL="$(grep '^ *"upload_url": "' "$JSON" \ 60 | | sed -e 's/^ *"upload_url": "//' -e 's/{.*}.*$//')" 61 | grep '^https://uploads.github.com/.*/assets$' <<< "$UPLOAD_URL" || exit $? 62 | 63 | # upload both .tar.gz and .tar.xz 64 | for comp in gzip xz; do 65 | file="${NV}.tar.${comp:0:2}" 66 | curl "${UPLOAD_URL}?name=${file}" \ 67 | -T "$file" --fail --verbose \ 68 | --header "Authorization: token $TOKEN" \ 69 | --header "Content-Type: application/x-${comp}" \ 70 | || exit $? 71 | done 72 | 73 | # upload signatures 74 | for file in "${TAR_GZ}.asc" "${TAR_XZ}.asc"; do 75 | curl "${UPLOAD_URL}?name=${file}" \ 76 | -T "$file" --fail --verbose \ 77 | --header "Authorization: token $TOKEN" \ 78 | --header "Content-Type: text/plain" \ 79 | || exit $? 80 | done 81 | --------------------------------------------------------------------------------