diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index a7ce91e..a700f69 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -28,7 +28,7 @@ jobs: - name: Verify the Docker image # profile for language 'web' is the last; assume everything is working if we got this far - run: docker logs -f ci |& sed '/Current profile for language web is Sonar way/ q' + run: docker logs -f ci |& sed "/Current profile for language 'web' is 'Sonar way'/ q" timeout-minutes: 3 - name: Stop the Docker image diff --git a/Dockerfile b/Dockerfile index df2b8d8..e9cba9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,39 @@ ARG IMAGE_NAME=sonarqube ARG IMAGE_VERSION=10.1.0 ARG IMAGE_EDITION=community + FROM $IMAGE_NAME:$IMAGE_VERSION-$IMAGE_EDITION + +LABEL org.opencontainers.image.authors="open-source-projects@ictu.nl" +LABEL org.opencontainers.image.url="https://github.com/ICTU/sonar" +LABEL org.opencontainers.image.documentation="https://raw.githubusercontent.com/ICTU/sonar/master/README.md" +LABEL org.opencontainers.image.source="https://raw.githubusercontent.com/ICTU/sonar/master/Dockerfile" +LABEL org.opencontainers.image.vendor="ICTU" +LABEL org.opencontainers.image.title="ICTU SonarQube" +LABEL org.opencontainers.image.description="A SonarQube image containing plugins, profiles and config used at ICTU" + USER root -RUN apt-get update && apt-get install -y wget curl ca-certificates-java jq postgresql-client \ + +RUN apt-get update \ + && apt-get install -y wget curl ca-certificates-java jq postgresql-client \ && rm -rf /var/lib/apt/lists/* + ADD ./plugins /tmp/plugins RUN rm -rf ./extensions/plugins/* && \ cat /tmp/plugins/plugin-list && \ + ls -l /tmp/plugins && \ chmod +x /tmp/plugins/install-plugins.sh && \ - ls /tmp/plugins -l && \ /tmp/plugins/install-plugins.sh + WORKDIR /opt/sonarqube + COPY ./start-with-profile.sh . ADD ./rules /tmp/rules ADD sonar.properties /opt/sonarqube/conf/sonar.properties -RUN chown -R sonarqube:sonarqube . && chmod +x start-with-profile.sh + +RUN chown -R sonarqube:sonarqube . \ + && chmod +x start-with-profile.sh + USER sonarqube + CMD ["./start-with-profile.sh"] diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml new file mode 100644 index 0000000..f0f5e2d --- /dev/null +++ b/docker/docker-compose.ci.yml @@ -0,0 +1,17 @@ +--- +version: "3.7" + +services: + www: + build: + args: + IMAGE_EDITION: "developer" + environment: + SONAR_JDBC_USERNAME: "sonar_user" + SONAR_JDBC_PASSWORD: "sonar_pass" + SONARQUBE_PASSWORD: "admin123" + + db: + environment: + POSTGRES_USER: "sonar_user" + POSTGRES_PASSWORD: "sonar_pass" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..d718ff3 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,32 @@ +--- +version: "3.7" + +services: + www: + build: + context: .. + environment: + SONAR_JDBC_URL: "jdbc:postgresql://db:5432/sonar" + ports: + - mode: ingress + target: 9000 + published: 9001 + volumes: + - type: "volume" + source: "plugins" + target: "/opt/sonarqube/extensions/plugins" + depends_on: + - db + + db: + image: postgres:16.0-alpine3.18 + environment: + POSTGRES_DB: "sonar" + POSTGRES_HOST_AUTH_METHOD: "scram-sha-256" + POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256" + volumes: + - "dbdata:/var/lib/postgresql/data:rw" + +volumes: + dbdata: + plugins: diff --git a/plugins/plugin-list b/plugins/plugin-list index 557d6a6..d45b3f5 100644 --- a/plugins/plugin-list +++ b/plugins/plugin-list @@ -1,7 +1,7 @@ https://github.com/checkstyle/sonar-checkstyle/releases/download/10.12.3/checkstyle-sonar-plugin-10.12.3.jar https://github.com/dependency-check/dependency-check-sonar-plugin/releases/download/4.0.0/sonar-dependency-check-plugin-4.0.0.jar https://github.com/jborgers/sonar-pmd/releases/download/3.4.0/sonar-pmd-plugin-3.4.0.jar +https://github.com/sbaudoin/sonar-ansible/releases/download/v2.5.1/sonar-ansible-plugin-2.5.1.jar https://github.com/sbaudoin/sonar-yaml/releases/download/v1.7.0/sonar-yaml-plugin-1.7.0.jar -https://github.com/spotbugs/sonar-findbugs/releases/download/4.2.3/sonar-findbugs-plugin-4.2.3.jar +https://github.com/spotbugs/sonar-findbugs/releases/download/4.2.4/sonar-findbugs-plugin-4.2.4.jar https://github.com/vaulttec/sonar-auth-oidc/releases/download/v2.1.1/sonar-auth-oidc-plugin-2.1.1.jar -https://github.com/sbaudoin/sonar-ansible/releases/download/v2.5.1/sonar-ansible-plugin-2.5.1.jar diff --git a/rules/swift.txt b/rules/swift.txt new file mode 100644 index 0000000..dc2e457 --- /dev/null +++ b/rules/swift.txt @@ -0,0 +1,7 @@ ++types=SECURITY_HOTSPOT,VULNERABILITY # Enable these types by default ++swift:S1541|Threshold=10 # Used by Quality-time (https://github.com/ICTU/quality-time/blob/master/docs/METRICS_AND_SOURCES.md#complex-units-from-sonarqube) ++swift:S1067 # Expression too complex; NOT used by Quality-time ++swift:S138|max=20 # Methods with too many lines; used by Quality-time (https://github.com/ICTU/quality-time/blob/master/docs/METRICS_AND_SOURCES.md#long-units-from-sonarqube) ++swift:S107|functionMax=5 # Too many parameters; used by Quality-time (https://github.com/ICTU/quality-time/blob/master/docs/METRICS_AND_SOURCES.md#many-parameters-from-sonarqube) ++swift:S125 # Used by Quality-time (https://github.com/ICTU/quality-time/blob/master/docs/METRICS_AND_SOURCES.md#commented-out-code-from-sonarqube) +#end please ensure every rule ends with a new line character diff --git a/start-with-profile.sh b/start-with-profile.sh index 5376262..f2eb902 100644 --- a/start-with-profile.sh +++ b/start-with-profile.sh @@ -1,35 +1,34 @@ #!/bin/bash -if [[ -n $SONARQUBE_TOKEN ]]; then - BASIC_AUTH="$SONARQUBE_TOKEN:" +if [[ -n "${SONARQUBE_TOKEN}" ]]; then + BASIC_AUTH="${SONARQUBE_TOKEN}:" else BASIC_AUTH="${SONARQUBE_USERNAME:-admin}:${SONARQUBE_PASSWORD:-admin}" fi # Access SonarQube api with admin credentials function curlAdmin { - curl -v -u "$BASIC_AUTH" "$@" + curl -s -u "${BASIC_AUTH}" "$@" | awk 1 } # Check if the database is ready for connections function waitForDatabase { # get HOST:PORT from JDBC URL - if [[ $SONAR_JDBC_URL =~ postgresql://([^:/]+)(:([0-9]+))?/ ]]; then + if [[ "${SONAR_JDBC_URL}" =~ postgresql://([^:/]+)(:([0-9]+))?/ ]]; then local host=${BASH_REMATCH[1]} local port=${BASH_REMATCH[3]:-5432} else echo "Only PostgreSQL databases are supported" return fi - echo "Waiting for database connection on $host:$port" + echo "Waiting for database connection on ${host}:${port}" local count=0 local sleep=5 local timeout=${DB_START_TIMEOUT:-60} - until pg_isready -h "$host" -p "$port" ${SONAR_JDBC_USERNAME:+-U "$SONAR_JDBC_USERNAME"} + until pg_isready -h "${host}" -p "${port}" ${SONAR_JDBC_USERNAME:+-U "$SONAR_JDBC_USERNAME"} do - if [[ count -gt timeout ]] - then - echo "ERROR: Failed to start database within $timeout seconds" + if [[ count -gt timeout ]]; then + echo "ERROR: Failed to start database within ${timeout} seconds" exit 1 fi echo "Waiting for database connection..." @@ -37,7 +36,7 @@ function waitForDatabase { sleep $sleep count=$((count+sleep)) done - echo "Database listening on ${HOSTPORT}" + echo "Database listening on ${host}:${port}" } # Wait until SonarQube is operational @@ -46,15 +45,14 @@ function waitForSonarUp { local sleep=5 local timeout=${SONAR_START_TIMEOUT:-600} # Wait for server to be up - until [[ "$status" == "UP" ]] + until [[ "${status}" == "UP" ]] do - if [[ count -gt timeout ]] - then - echo "ERROR: Failed to start Sonar within $timeout seconds" + if [[ count -gt timeout ]]; then + echo "ERROR: Failed to start Sonar within ${timeout} seconds" exit 1 fi - status=$(curl -s -f "$BASE_URL/api/system/status" | jq -r '.status') - echo "Waiting for sonar to come up: $status" + status=$(curl -s -f "${BASE_URL}/api/system/status" | jq -r '.status') + echo "Waiting for sonar to come up: ${status}" sleep $sleep count=$((count+sleep)) done @@ -63,16 +61,16 @@ function waitForSonarUp { # Try to change the default admin password to the one provided in SONARQUBE_PASSWORD function changeDefaultAdminPassword { - if [ -n "$SONARQUBE_PASSWORD" ]; then + if [ -n "${SONARQUBE_PASSWORD}" ]; then echo "Trying to change the default admin password" - curl -s -X POST -u "admin:admin" -f "$BASE_URL/api/users/change_password?login=admin&password=${SONARQUBE_PASSWORD}&previousPassword=admin" + curl -s -X POST -u "admin:admin" -f "${BASE_URL}/api/users/change_password?login=admin&password=${SONARQUBE_PASSWORD}&previousPassword=admin" fi } # Test admin credentials function testAdminCredentials { - authenticated=$(curl -s -u "$BASIC_AUTH" -f "$BASE_URL/api/system/info") - if [ -z "$authenticated" ]; then + authenticated=$(curl -s -u "${BASIC_AUTH}" -f "${BASE_URL}/api/system/info") + if [ -z "${authenticated}" ]; then echo "################################################################################" echo "No or incorrect admin credentials provided. Shutting down SonarQube..." echo "################################################################################" @@ -84,10 +82,12 @@ function testAdminCredentials { function getProfileKey { local searchProfileName=$1 local searchLanguage=$2 - local getProfileKeyUrl="$BASE_URL/api/qualityprofiles/search?qualityProfile=$searchProfileName&language=$searchLanguage" - local json=$(curl -u "$BASIC_AUTH" "$getProfileKeyUrl") - local searchResultProfileKey=$(echo "$json" | grep -Eo '"key":"([_A-Z0-9a-z-]*)"' | cut -d: -f2 | sed -r 's/"//g') - echo "$searchResultProfileKey" + local getProfileKeyUrl json searchResultProfileKey + + getProfileKeyUrl="${BASE_URL}/api/qualityprofiles/search?qualityProfile=${searchProfileName}&language=${searchLanguage}" + json=$(curl -s -u "${BASIC_AUTH}" "${getProfileKeyUrl}") + searchResultProfileKey=$(echo "${json}" | grep -Eo '"key":"([_A-Z0-9a-z-]*)"' | cut -d: -f2 | sed -r 's/"//g') + echo "${searchResultProfileKey}" } function processRule { @@ -95,56 +95,50 @@ function processRule { local profileKey=$2 local language=$3 - echo "Optimize rule" - rule=$(echo $rule |tr -d ' ' | cut -d "#" -f 1) + rule=$(echo "${rule}" |tr -d ' ' | cut -d "#" -f 1) + echo "Processing rule '${rule}'" # The first character is the operation # + = activate # - = deactivate - local operationType=${rule:0:1} + local operationType="${rule:0:1}" # After the operation comes the SonarQube ruleSet which contains ruleId and ruleParams - local ruleSet=${rule:1} + local ruleSet="${rule:1}" # Enable rules by group (types) - if [[ $ruleSet =~ types=.* ]]; then + if [[ "${ruleSet}" =~ types=.* ]]; then - IFS='=' read -r typekey ruleTypes <<< "$ruleSet" - if [ "$operationType" == "+" ]; then + IFS='=' read -r typekey ruleTypes <<< "${ruleSet}" + if [ "${operationType}" == "+" ]; then echo "Activating rules ${ruleTypes} for ${language}" - curlAdmin -X POST "$BASE_URL/api/qualityprofiles/activate_rules?targetKey=${profileKey}&languages=${language}&types=${ruleTypes}&statuses=READY" - fi - if [ "$operationType" == "-" ]; then + curlAdmin -X POST "${BASE_URL}/api/qualityprofiles/activate_rules?targetKey=${profileKey}&languages=${language}&types=${ruleTypes}&statuses=READY" + elif [ "${operationType}" == "-" ]; then echo "Deactivating rules ${ruleTypes} for ${language}" - curlAdmin -X POST "$BASE_URL/api/qualityprofiles/deactivate_rules?targetKey=${profileKey}&languages=${language}&types=${ruleTypes}" + curlAdmin -X POST "${BASE_URL}/api/qualityprofiles/deactivate_rules?targetKey=${profileKey}&languages=${language}&types=${ruleTypes}" fi else - IFS='|' read -r ruleId ruleParams <<< "$ruleSet" - ruleParams=${ruleParams/|/,} + IFS='|' read -r ruleId ruleParams <<< "${ruleSet}" + ruleParams="${ruleParams/|/,}" - echo "*** Processing rule ***" - echo "Rule ${rule}" - echo "Operation ${operationType}" - echo "RuleId ${ruleId}" - echo "RuleParams ${ruleParams}" + echo "Rule: '${rule}', Operation: '${operationType}'" + echo "RuleId: '${ruleId}', RuleParams: '${ruleParams}'" - if [ "$operationType" == "+" ]; then + if [ "${operationType}" == "+" ]; then echo "Activating rule ${ruleId}" - if [ "$ruleParams" == "" ]; then - curlAdmin -X POST "$BASE_URL/api/qualityprofiles/activate_rule?key=$profileKey&rule=$ruleId" + if [ "${ruleParams}" == "" ]; then + curlAdmin -X POST "${BASE_URL}/api/qualityprofiles/activate_rule?key=${profileKey}&rule=${ruleId}" else - curlAdmin -X POST "$BASE_URL/api/qualityprofiles/activate_rule?key=$profileKey&rule=$ruleId¶ms=$ruleParams" + curlAdmin -X POST "${BASE_URL}/api/qualityprofiles/activate_rule?key=${profileKey}&rule=${ruleId}¶ms=${ruleParams}" fi - fi - - if [ "$operationType" == "-" ]; then + elif [ "${operationType}" == "-" ]; then echo "Deactivating rule ${ruleId}" - curlAdmin -X POST "$BASE_URL/api/qualityprofiles/deactivate_rule?key=$profileKey&rule=$ruleId" + curlAdmin -X POST "${BASE_URL}/api/qualityprofiles/deactivate_rule?key=${profileKey}&rule=${ruleId}" fi - fi # [[ $ruleSet =~ types.* ]]; + fi } # Create a new SonarQube profile with custom activated rules, inheritance and set as default @@ -162,18 +156,18 @@ function createProfile { # create profile # curlAdmin -X POST "$BASE_URL/api/qualityprofiles/create?name=$profileName&language=$language" # curlAdmin -X POST --data "qualityProfile=$1&parentQualityProfile=$2&language=$3" "$BASE_URL/api/qualityprofiles/change_parent" - echo "Copying the profile $baseProfileName $language to $profileName" - baseProfileKey=$(getProfileKey "$baseProfileName" "$language") - copyProfileUrl="$BASE_URL/api/qualityprofiles/copy?toName=$profileName&fromKey=$baseProfileKey" - echo "Posting to $copyProfileUrl" - curlAdmin -X POST "$copyProfileUrl" + echo "Copying the profile ${baseProfileName} ${language} to ${profileName}" + baseProfileKey=$(getProfileKey "${baseProfileName}" "${language}") + copyProfileUrl="${BASE_URL}/api/qualityprofiles/copy?toName=${profileName}&fromKey=${baseProfileKey}" + echo "Posting to ${copyProfileUrl}" + curlAdmin -X POST "${copyProfileUrl}" - profileKey=$(getProfileKey "$profileName" "$language") - echo "The profile $profileName $language has the key $profileKey" + profileKey=$(getProfileKey "${profileName}" "${language}") + echo "The profile '${profileName}' of '${language}' has the key '${profileKey}'" - if [[ -f "$rulesFilename" ]]; then + if [[ -f "${rulesFilename}" ]]; then # activate and deactivate rules in new profile - while read ruleLine || [ -n "$line" ]; do + while read ruleLine || [ -n "${line}" ]; do # Each line contains: # (+|-)types=comma-seperated,list-of-types # comment @@ -182,56 +176,55 @@ function createProfile { # +cs:1032 # some comment # +types=SECURITY_HOTSPOT,VULNERABILITY # some comment IFS='#';ruleSplit=("${ruleLine}");unset IFS; - rule=${ruleSplit[0]} - comment=${ruleSplit[1]} + rule="${ruleSplit[0]}" - processRule "$rule" "$profileKey" "$language" + processRule "${rule}" "${profileKey}" "${language}" - done < "$rulesFilename" + done < "${rulesFilename}" fi # if the PROJECT_RULES environment variable is defined and not empty, create a custom project profile - echo "Project specific rules = $PROJECT_RULES" - if [[ -n "$PROJECT_RULES" ]]; then + echo "Project specific rules = ${PROJECT_RULES}" + if [[ -n "${PROJECT_RULES}" ]]; then echo "Creating custom project profile" - local projectProfileName=$PROJECT_CODE-$profileName - echo "Project custom profile name is $projectProfileName" + local projectProfileName="${PROJECT_CODE}-${profileName}" + echo "Project custom profile name is '${projectProfileName}'" # create project specific profile # curlAdmin -X POST "$BASE_URL/api/qualityprofiles/create?name=$projectProfileName&language=$language" # curlAdmin -X POST --data "qualityProfile=$projectProfileName&parentQualityProfile=$profileName&language=$3" "$BASE_URL/api/qualityprofiles/change_parent" - echo "Copying the profile $baseProfileName $language to $profileName" - curlAdmin -X POST "$BASE_URL/api/qualityprofiles/copy?fromKey=$profileKey&toName=$projectProfileName" + echo "Copying the profile '${baseProfileName}' of '${language}' to '${profileName}'" + curlAdmin -X POST "${BASE_URL}/api/qualityprofiles/copy?fromKey=${profileKey}&toName=${projectProfileName}" # retrieve the new profile key - profileKey=$(getProfileKey "$projectProfileName" "$language") - echo "The profile $projectProfileName $language has the key $profileKey" + profileKey=$(getProfileKey "${projectProfileName}" "${language}") + echo "The profile '${projectProfileName}' of '${language}' has the key '${profileKey}'" - IFS=';' read -ra projrules <<< "$PROJECT_RULES" + IFS=';' read -ra projrules <<< "${PROJECT_RULES}" for rule in "${projrules[@]}"; do - echo "Processing project custom rule $rule" - processRule "$rule" "$profileKey" "$language" + echo "Processing project custom rule ${rule}" + processRule "${rule}" "${profileKey}" "${language}" done # mark this profile to be activated - profileName=$projectProfileName + profileName="${projectProfileName}" fi # get current default profile name - currentProfileName=$(curl -u "$BASIC_AUTH" -s "$BASE_URL/api/qualityprofiles/search?defaults=true" | jq -r --arg LANGUAGE "$3" '.profiles[] | select(.language==$LANGUAGE) | .name') - echo "Current profile for language $3 is $currentProfileName" + currentProfileName=$(curl -s -u "${BASIC_AUTH}" "${BASE_URL}/api/qualityprofiles/search?defaults=true" | jq -r --arg LANGUAGE "$3" '.profiles[] | select(.language==$LANGUAGE) | .name') + echo "Current profile for language '$3' is '${currentProfileName}'" # set profile as default only when name does not end in DEFAULT or default shopt -s nocasematch - if [[ $currentProfileName =~ .*DEFAULT$ ]]; then - echo "Keeping current default profile $currentProfileName for language $3" + if [[ "${currentProfileName}" =~ .*DEFAULT$ ]]; then + echo "Keeping current default profile '${currentProfileName}' for language '$3'" else - if [[ $currentProfileName =~ .*EXTENDED$ ]]; then - echo "Changing parent of extended profile $currentProfileName for language $3 to $profileName" - curlAdmin -X POST "$BASE_URL/api/qualityprofiles/change_parent?qualityProfile=$currentProfileName&parentQualityProfile=$profileName&language=$3" + if [[ "${currentProfileName}" =~ .*EXTENDED$ ]]; then + echo "Changing parent of extended profile '${currentProfileName}' for language '$3' to '${profileName}'" + curlAdmin -X POST "${BASE_URL}/api/qualityprofiles/change_parent?qualityProfile=${currentProfileName}&parentQualityProfile=${profileName}&language=$3" else - echo "Setting profile $profileName for language $3 as default" - curlAdmin -X POST "$BASE_URL/api/qualityprofiles/set_default?qualityProfile=$profileName&language=$3" + echo "Setting profile '${profileName}' for language '$3' as default" + curlAdmin -X POST "${BASE_URL}/api/qualityprofiles/set_default?qualityProfile=${profileName}&language=$3" fi fi } @@ -239,10 +232,10 @@ function createProfile { ########################################################################################################################### # Main ########################################################################################################################### -BASE_URL=http://127.0.0.1:9000 +BASE_URL="http://127.0.0.1:9000" # waitForDatabase -if [ "$SONAR_JDBC_URL" ]; then +if [ "${SONAR_JDBC_URL}" ]; then waitForDatabase fi @@ -250,8 +243,8 @@ fi function shutdown { echo "Shutdown" if [[ -n $PID ]]; then - kill $PID - wait $PID + kill "${PID}" + wait "${PID}" fi } trap "shutdown" EXIT @@ -268,14 +261,22 @@ testAdminCredentials # (Re-)create the ICTU profiles RULES_VERSION=20230619 +echo "*** Start processing rules for version ${RULES_VERSION} ***" createProfile "ictu-ansible-profile-v2.5.1-${RULES_VERSION}" "Sonar%20way" "yaml" # custom sonar-ansible-plugin createProfile "ictu-cs-profile-v9.3.0-${RULES_VERSION}" "Sonar%20way" "cs" # image csharp-plugin createProfile "ictu-java-profile-v7.20.0-${RULES_VERSION}" "Sonar%20way" "java" # image java-plugin createProfile "ictu-js-profile-v10.3.1-${RULES_VERSION}" "Sonar%20way" "js" # image javascript-plugin createProfile "ictu-kotlin-profile-v2.15.0-${RULES_VERSION}" "Sonar%20way" "kotlin" # image kotlin-plugin createProfile "ictu-py-profile-v4.3.0-${RULES_VERSION}" "Sonar%20way" "py" # image python-plugin +createProfile "ictu-swift-profile-v4.9.0-${RULES_VERSION}" "Sonar%20way" "swift" # image swift-plugin createProfile "ictu-ts-profile-v10.3.1-${RULES_VERSION}" "Sonar%20way" "ts" # image javascript-plugin createProfile "ictu-vbnet-profile-v9.3.0-${RULES_VERSION}" "Sonar%20way" "vbnet" # image vbnet-plugin createProfile "ictu-web-profile-v3.8.0-${RULES_VERSION}" "Sonar%20way" "web" # image html-plugin +echo "*** Finished processing rules ***" + +echo "" +echo "" +echo "SonarQube started with ICTU profiles" +echo "" wait $PID