├── .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 |
--------------------------------------------------------------------------------